Repository navigation
Commit d0769bf
test(rest): a lookup cell re-imports by a stored computed title through the real import door (#25087)
Fixes #24981
Clause-②: no (pins, and at most one refusal sentence; no accept set
moves, since the formula skip already landed in PR #25033)
This is the close-out of the card under the maintainer's ruling
6308445871 (letter A). The stored-title half landed in PR #25033
(`dfe60e3a0`). This PR adds the pins the ruling's remedy was missing. It
changes no runtime code and ships no changeset: the one new file is a
test, and `@objectstack/rest` publishes `dist` only.
## The ruling, items 1 to 4 (6308445871, verbatim)
1. "The import matches a lookup cell against a STORED title only. After
the id and the lookup's own `displayField`, `resolveRef` asks the
target's declared title field by the one rule (`resolveDisplayField`)
when that field is stored — landed in PR #25033. A target whose declared
title is a formula keeps answering `reference_not_found`; no scan, no
search heuristic, no platform-materialised title column (B-cap, C and D
are not taken)."
2. "The producer stores the title it needs to query: a composite title
that must round-trip through import is a stored computed field (a
non-formula field with an `expression`, written on write — the #24619
shape), the remedy ADR-0079 item 9 already prescribes for sort and
search."
3. "The card's second Wanted bullet … is withdrawn by this ruling. The
fallback key list and the #22739 exposure order are unchanged."
4. "Wording: whether the `reference_not_found` sentence should say why a
formula-titled target cannot match is the dev's to measure against the
existing text and declare in the PR; ⛔ no new error code."
**The card's second Wanted bullet is withdrawn by ruling 6308445871.**
That bullet read: "A formula title is matched by the engine's own
record-title resolution, not by a `where` on a column that does not
exist." Nothing here matches a formula title. The fallback key list
(`name`, `title`, `label`, `full_name`, `email`, `username`) and the
exposure order from #22739 are unchanged.
## What this adds
`packages/rest/src/import-reference-stored-computed-title.test.ts`, two
pins through the REAL import door (`POST /api/v1/data/:object/import`),
a REAL ObjectQL engine and a sqlite `:memory:` driver:
1. **The ruling's remedy resolves.** Each target row is written without
its title, and the engine computes and stores the title on write. Two
lookup cells then link: `Acme (EMEA)` against `nameField:
'display_title'`, a stored `text` field with `expression` `record.name +
" (" + record.region + ")"` (not on the fallback list), and `John Smith`
against a stored `full_name` (on the list). The test reads the stored
ids back. A control row names the same title over the same rows on a
twin target whose `display_title` is a `formula`. It answers
`reference_not_found`, so the stored declaration is what changes the
answer.
2. **A collision is refused.** Two records whose computed titles are
both `Acme (EMEA)` answer `reference_ambiguous`, and neither is linked.
A third record, `Acme (APAC)`, links in the same import.
No change to `import-runner.ts` was needed (the dispatch's H2): the
remedy is matched by the rung PR #25033 landed.
## Pins that already held (H1), named and not repeated
`packages/core/src/utils/import-runner-reference-title.test.ts` (PR
#25033). It was run here at `fe962effe3`: 7 of 7 passed.
- `Wren resolves through a stored displayNameField (the card's cell, as
authored)`, and the same through `nameField`. Each asserts the candidate
order `id`, the lookup's `displayField`, then the title.
- `the stored title stores the matched record's id`: one `getMetaItem`
per target per import.
- `a formula title (xt_contact.full_name)` and
`(xt_account.display_title)` `is never asked by where`:
`reference_not_found`, no `where` over the formula, no recorded refusal.
- `two records sharing a stored title answer the import's ambiguity
refusal, and link neither`.
- The #22739 pin: a target whose exposure refuses `list` is never read.
Beside them, `packages/rest/src/import-integration.test.ts` pins
`reference_ambiguous` for a duplicated `displayField` value.
The gap: every one of those pins runs over a fake `findData`, or over a
target with no computed title. None had a target whose title is a stored
computed field, and none had the engine compute a title on write before
the import matched it. This PR's file closes that gap and only that gap.
It does not repeat what the core suite pins closer to the code (that a
formula is never asked by `where`, the candidate order, the metadata
read count, the exposure check). The formula row in pin 1 is that file's
control leg.
## The `reference_not_found` sentence (ruling item 4): unchanged
Measured at `fe962effe3`. A scratch probe, never committed, called
`runImport` from `@objectstack/core`'s built `dist/` against a target
whose `nameField` is a formula:
- Default locale: `Formula Account: no record matches "Acme (EMEA)"`.
zh-CN: `Formula Account:未找到与“Acme (EMEA)”匹配的记录`. Asked keys: `id`,
`name`, `title`, `label`, `full_name`, `email`, `username` (the formula
is skipped).
- The sentence is keyed. `import_reference_not_found` lives in
`packages/spec/src/system/validation-message.ts`: en `:158`, zh-CN
`:206`, ja-JP `:251`, es-ES `:296`. It is rendered by `coerceError` in
`packages/core/src/utils/import-coerce.ts` (`:743`, `:759`), not in
`import-runner.ts`.
**Declared: the sentence stays as it is.** Reasons:
1. It is true for a formula-titled target. No stored record matches the
cell, nothing is linked, and the row fails loudly.
2. Saying why would take two contract moves this close-out does not own.
First, a reason arm on `RefMatch`, which `@objectstack/core` exports
(today `id`, `ambiguous`, `matchedField`), so the coercion layer can
learn why the resolver missed. Second, a new keyed sentence in four
locales in `packages/spec`. That is no new error code, but it is a
public type change and a new message key, for a fact that holds for
every row of every import against that target.
3. The fact belongs to the target's metadata. The place that sees it
once, before any import runs, is authoring: the ADR-0079 D3 /
`validate-record-title` wording card that the seat files under this
ruling.
The code, the envelope and the i18n key are unchanged.
## Ablation (one leg: the stored title made invisible to `resolveRef`)
The pins were committed before the mutation. `@objectstack/rest`
resolves `@objectstack/core` through its built `dist/` (an unaliased
pair in `KNOWN_UNALIASED_TEST_IMPORTS`), so the leg rebuilt core and
proved the mutation was in `dist/` before reading the run.
- **Mutation**, through `scripts/ablation-replace.mjs`, in
`servesReferenceTarget`. The line `titleField:
resolveDisplayField(item),` became a branch that always yields
`undefined` and carries the marker `ablation-24981-title-invisible`.
Anchor x1 to x0, replacement x0 to x1, blob `d9b249d961d4` to
`79957dd60e4f`. Core build OK. `ablation-dist-preflight`: "marker
present in 2 built files".
- **Predicted before the run:** pin 1 red on `Acme (EMEA)` with
`reference_not_found`, while `John Smith` stays linked (its `full_name`
is on the fallback list). Pin 2 red: `reference_not_found` in place of
`reference_ambiguous`, and `Acme (APAC)` unlinked.
- **Observed, as predicted:** `Tests 2 failed (2)`. Pin 1's diff shows
only row 1 moved (`ok: false, code: reference_not_found, field:
account`), with rows 2 and 3 unchanged. Pin 2's diff shows both rows at
`reference_not_found`.
- **Restore:** the tool's own proof was "blob == HEAD (`d9b249d961d4`)
and `git diff HEAD` is empty". Core was rebuilt, then
`ablation-dist-preflight --absent` read "marker absent from all 14 built
files" and "working tree clean against HEAD". The pins re-ran green:
`Tests 2 passed (2)`. After the full build, the absent reading was taken
again and passed.
## Tests and gates (head `fe962effe3`)
- Build: the `@objectstack/rest` dependency closure (`turbo run build
--filter=@objectstack/rest^...`: "25 successful, 25 total"), then the
full workspace (`turbo run build --filter=!@objectstack/docs`: "76
successful, 76 total"). Both ran under `os-verify-lock`, `VERDICT
command-exit 0`.
- The new pins: `pnpm --filter @objectstack/rest exec vitest run
--project local --maxWorkers=2
src/import-reference-stored-computed-title.test.ts`: "Tests 2 passed
(2)". The core title pins: "Tests 7 passed (7)".
- `pnpm --filter @objectstack/rest typecheck`: exit 0. The output reads
"check:test-typecheck: OK — @objectstack/rest's test layer compiles
under packages/rest/tsconfig.test.json; 0 file(s) / 0 error(s)". The new
file is in that program: `tsc --listFilesOnly -p tsconfig.test.json`
counts it once.
- `pnpm --filter @objectstack/rest exec vitest run --project local
--maxWorkers=2`: "Test Files 310 passed (310)", "Tests 6179 passed | 326
skipped (6505)".
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) derived 54 commands. Each exit
code was captured before any pipe, and all 54 exited 0. Two of them
first read exit 3 (PREREQUISITE NOT MET), so neither was a measurement.
`check:dual-build-cjs-loads` had no full build yet.
`check:type-check-debt` did not get the shared lock within its budget.
Both were re-run on the same head after the full build. They read "112
published require entry point(s) across 69 package(s) load" and "1
ledger entr(ies) re-measured … none above its recorded number". `--ran`
reconciliation: "54 derived famil(ies) accounted for — 54 run, 0
NOT-MEASURED (a DERIVED zero — all 54 recorded an exit code and none of
them is 3)".
- `check:platform-checklist --changed-from` was not run: no exported
symbol was renamed or removed. The repo-wide `pnpm lint`, `test:repo`
(the diff touches none of its files) and the full farm are CI's.
## Acceptance notes
- **File surface.** The dispatch placed the pins beside the import
runner's reference tests in `packages/core`, "through the real import
path and a real engine, the way PR #25033's pins do". Measured: PR
#25033's pins run over a fake `findData`, and `packages/core` cannot
build a real engine (`@objectstack/objectql` depends on
`@objectstack/core`; core has no objectql dependency). The ruling's
remedy is an engine behaviour (computed on write), so the end-to-end
pins sit in `packages/rest`, beside `import-integration.test.ts`, which
carries the door's other reference pins. A core-side fake pin of the
same remedy would only repeat the core suite's stored-title pin with an
`expression` key added, so none was written.
- **Changeset.** None. The diff is one test file in a package that
publishes `dist` only. `skip-changeset` is the PM's decision.
- **Carried, not filed (prior dev reports on the card).** When the
declared title misses, the fallback key list can still link a cell by an
unrelated column (`VP of Engineering` links a contact by its job
`title`). The ruling keeps the list unchanged.
- **HotCRM and the ADR-0079 D3 / `validate-record-title` wording** are
the seat's cards under the ruling. Neither is touched here.
- **Base.** Cut from `72518d20f5`. `origin/main` was merged at
`688052e147` before the gate battery and the full build. `main` has
since moved 7 commits (`objectql`, `spec` and others). The branch was
not merged again, because a second merge would need a second battery and
the diff overlaps none of those files. CI and the merge queue rebuild
against current `main`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4f97aeb commit d0769bf
1 file changed
Lines changed: 171 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
0 commit comments