Repository navigation
Commit a675ad4
Refuse an unreadable
Fixes #18550
Clause-②: yes
Ruling letter E item 2 on #18095 asked for the loud refusal at **every
reader** of `FieldSchema.reference`. PR #18503 delivered it at the
arbiter (`referenceCarrierOf`) and the lint target readers, and named
the remainder itself under "What this PR does not close". This is that
remainder, routed — with a per-site decision about **absence vs
unreadability**, because in this design they are deliberately different
answers.
---
## JOB ONE — the count, re-derived per line. ⛔ Nothing inherited.
Triage's instruction was 「⭐ 数是 9,⛔ 不是 PR 正文写的 10 —— 契约复核重新数过。⭐
这正是本班刚立的那条:**计数是会腐烂的读数** ⇒ 派发时再数一次,⛔ 不要把 9 或 10 任何一个往前抄。」 So neither
number is carried forward here. What the re-derivation found is that **9
is not reachable at any single granularity** — it mixes two.
Every cited line was printed at the card's stated merge base `2f11e2db7`
and re-located on `origin/main@0ec8185`:
| card's cited line (at `2f11e2db7`) | current line | text | moved? |
|---|--:|---|---|
| `objectql/src/engine.ts:13052` | `13096` | `const ref =
fdef.reference;` | +44 |
| `objectql/src/engine.ts:13491` | `13544` | `const ref =
fdef.reference;` | +53 |
| `rest/src/rest-server.ts:10835` | `10889` | `referenceObject =
def?.reference;` | +54 |
| `metadata-protocol/src/seed-loader.ts:701` | `701` |
`fieldDef.reference` | — |
| `lint/src/validate-expressions.ts:380` | `380` | `const ref =
def.reference;` | — |
| `lint/src/validate-field-consumers.ts:552` | `552` | `const reference
= strName(field.reference);` | — |
| `lint/src/validate-object-references.ts:297` | `297` |
`strName(field.reference),` | — |
| `lint/src/validate-object-references.ts:316` | `316` |
`strName(param.reference),` | — |
| `lint/src/validate-sharing-rule-enforceability.ts:261` | `261` |
`const ref = f.reference;` | — |
| `verify/src/derive.ts:136` | `136` | `const ref = f?.reference;` | — |
**That is 10 cited LINES in 8 FILES.** Neither is 9. The 9 is reachable
only by mixing granularities, and triage's own parenthesis shows the
mix: 「objectql ×2、rest、metadata-protocol、lint ×4、verify」 = 2 objectql
LINES + 3 non-lint FILES + 4 lint FILES = 9, counting objectql per line
and lint per file. PR #18503's own "10 sites" is its 11-line C3 list
(the 10 above plus `validate-preset-comparands.ts:431`) with one pair
merged. Both numbers are arithmetic over the same list; **the list is
the fact, the count was the rotting reading.**
- ⭐ `validate-preset-comparands.ts:431` (now `:450`) is confirmed **not
residue**, on the same reading the contract review gave: it reads
`verdict.meta?.reference`, and `meta` is the `GraphField` slice
`graphFieldOf` builds — which PR #18503 routed. It is covered
transitively, and so are `object-graph.ts:383` and `:386`.
- ⛔ Not routed, and flagged rather than widened:
`objectql/src/engine.ts:9033` (`cd?.reference === parent.name` in
`buildSummaryIndex`) produces the same silent skip — "can't resolve the
relationship — skip", so a `summary` field never recomputes. It is
**already classified**, as `engine.ts:8989` in PR #18503's **C2** list,
a boundary that PR drew deliberately. Routing it would widen this card
past its scope; it goes to the seat as a finding instead.
### The "four routed lint read sites", re-derived
The card paraphrases PR #18503 as delivering the refusal "at the arbiter
plus the four lint read sites". The PR's own words are **"A — changed
here (6 read sites, 4 files)"**, and one of those four files is
`packages/spec/src/data/field-value.zod.ts` — the arbiter itself, not
lint. Measured on `origin/main`:
| reading | value |
|---|--:|
| files holding a `referenceCarrierOf` call | **3** — all lint
(`data-model-rules.ts`, `object-graph.ts`,
`validate-security-posture.ts`) |
| arbiter CALL sites in those files | **3** (one per file: inside
`refOf`, `graphFieldOf`, `refOf`) |
| lint READ sites those 3 calls cover | **9** — `refOf(` ×4 in
`data-model-rules.ts`, ×3 in `validate-security-posture.ts`,
`graphFieldOf(` ×2 in `object-graph.ts` |
So **four lint read sites is not a reading at any granularity**: it is 3
files / 3 calls / 9 read sites, plus the arbiter's own file as the
fourth FILE. The seat's lit control reproduces exactly (2 grep hits per
file = 1 import + 1 call).
### The C1 set, re-derived — the card is right, and the PR body is wrong
in the other direction too
C1 was "explicit `typeof === 'string'` narrowing — the same silence
spelled differently". Re-derived repo-wide over `packages/*/src`:
**The card's C1 list, checked one by one:**
| site | silence? |
|---|---|
| `plugin-audit/src/audit-writers.ts:472, 527, 629, 693` | ✅ yes ×4 —
unreadable answers `''`/`undefined`, audit rows lose the target |
| `rest/src/export-format.ts:170` | ✅ yes — `reference: undefined` in
the export meta, so `referenceFieldNames` omits the column and it
exports raw ids with no `$expand` |
| `cli/src/commands/doctor.ts:792` | ✅ yes |
| `spec/src/kernel/functional-completeness.ts:167` (now `:177`) | ❌
**NOT silence — the card is right.** It REPORTS:
`FIELD_RELATIONSHIP_WITHOUT_REFERENCE`, severity error. Measured, not
read: registering a fixture with an object carrier printed `[Registry]
Object "task" registered with 1 functionally-incomplete field(s) …
account: [error] field/relationship-without-reference` |
⇒ **6 genuine C1 sites, not 7.**
**And what the card's list gets wrong in the other direction** — the
thing it asked to be told:
1. ⭐ **C1 and the residue are NOT disjoint.** Six of the ten residue
lines ARE `typeof === 'string'` narrowings, inline or through a
`strName` helper that is one: `validate-expressions.ts:380`,
`validate-field-consumers.ts:552` (`strName` = `typeof v === 'string' &&
v.length > 0`), `validate-object-references.ts:297` and `:316`,
`validate-sharing-rule-enforceability.ts:261`, `verify/derive.ts:136`.
The PR body's C1/C3 split reads as two populations; at six of ten sites
it is one code shape sorted into two classes. Only four residue lines
are a genuinely different shape: two truthiness gates (`engine.ts` ×2),
one truthiness-plus-cast (`seed-loader.ts`), one bare assignment with no
narrowing at all (`rest-server.ts`).
2. **A second C1-shaped site that REPORTS, which the list omits:**
`spec/src/automation/builtin-node-config.zod.ts:527` — `typeof
field.reference === 'string' && field.reference.trim() !== ''` inside a
`superRefine`, falling through to
`ctx.addIssue(SCREEN_FIELD_LOOKUP_REFERENCE_REQUIRED)`. So
`functional-completeness.ts` is not the lone misclassification; there
are two C1-SHAPED reporters, and ⛔ neither may be made to throw — a
throw inside a refinement makes `safeParse` throw instead of returning
`{success: false}`.
3. **Four truthiness/equality sites outside the C1 list that answer
worse than `undefined`** (C2 by the PR's classification, ⛔ not touched
here, reported as findings):
`plugin-approvals/src/approval-service.ts:5773` does
`String(f.reference)`, which turns an object carrier into the literal
target name `[object Object]`; `service-analytics/src/plugin.ts:736`
returns the unnarrowed carrier; `cli/src/commands/doctor.ts:709` and
`:895` push it into a dependency graph.
---
## The judgement per site — absence or unreadability, and the pin
`referenceCarrierOf` is the whole of the contract used here: `undefined`
/ `null` / `''` answer `undefined` (ABSENCE — a field is allowed to name
no target; `FieldSchema.reference` is `.optional()` and `StrictField`
declares it nullable), and any other shape throws a `TypeError` naming
the shape and the fix. ⛔ No site throws on a falsy carrier. Every
refusal pin has an absence partner and a positive control.
| site | case | what changed | pin |
|---|---|---|---|
| `objectql` `planCascadeAtomicity` | unreadability | truthy object
carrier passed `if (!ref)` and then failed both name comparisons, so the
child dropped out of the referencing set and `'none'` — the one verdict
asserting *nothing references this object* — could be returned over a
schema nobody could read | `engine-cascade-reference-carrier.test.ts`
"seam 1" |
| `objectql` `cascadeDeleteRelations` | unreadability | same shape, and
the one with the measured end-to-end consequence (below) | same file,
"seam 2", plus an array-carrier case |
| `rest` public-form lookup picker | unreadability | the carrier was
read INSIDE the metadata fetch's `catch {}`; routing alone would have
been swallowed into `LOOKUP_TARGET_MISSING`, so the field def is hoisted
out and read after it. An object carrier was also forwarded verbatim as
`query.object` into `findData` | `public-form-lookup-picker.test.ts`, 4
cases |
| `metadata-protocol` `buildDependencyGraph` | unreadability | retires
an `as string` cast that asserted exactly what the truthiness guard had
not checked | `seed-loader-reference-carrier.test.ts` |
| `lint` `masterDetailCount` | unreadability | an object carrier made a
declared `master_detail` invisible to the count, so `parent` was
reported unbound from metadata that declares a master |
`validate-expressions.test.ts` |
| `lint` `walkObject` displayField edge | unreadability | the
`displayField` consumer edge was never recorded, so a field a lookup
DOES display was reported carrier-only |
`validate-field-consumers.test.ts` |
| `lint` field target + action-param target | unreadability | `check`
returns early on `undefined`, so the declaration reported NOTHING — not
unknown-object, not missing-reference |
`validate-object-references.test.ts` |
| `lint` `masterOf` | unreadability | a `controlled_by_parent` detail
whose master IS declared answered `undefined` |
`validate-sharing-rule-enforceability.test.ts` |
| `verify` `relationTarget` | unreadability | degraded to the generic
"has no `reference` target" — the exact trade this reader already
refused to make for a rejected alias | `derive.test.ts` |
| `validate-preset-comparands.ts:450` | **already covered** | reads the
routed `GraphField` slice — no edit, measured not assumed | (existing) |
| `engine.ts:9033` `buildSummaryIndex` | **same silence, ⛔ not in
scope** | equality read; already recorded as C2 in PR #18503 | reported
to the seat |
### `ActionParamSchema.reference` — one caveat, stated rather than
buried
`validate-object-references.ts:316` reads an ActionParam, not a
FieldSchema field. The two are one contract by the spec's own words —
`ActionParamSchema.reference`'s docblock: *"Key name deliberately
mirrors `FieldSchema.reference` so the same spelling"*, declared
`SnakeCaseIdentifierSchema.optional()`. The refusal text it now produces
says "FieldSchema declares it as an optional STRING", which names the
sibling schema rather than this one. ⛔ Widening the arbiter's message
would mean editing `packages/spec`, outside this card's declared file
surface, so it is reported instead of done.
---
## Evidence
### The silence, reproduced at the representative site, then refusing
`objectql` cascade delete — the hot path, and the one whose wrong answer
is a production behaviour change. Same probe, before and after the
routing:
```text
BEFORE
PROBE before-delete rows: acct=1 task=1
PROBE delete outcome: RESOLVED true
PROBE after-delete rows: acct=0 task=1 <- an ORPHANED master_detail row
AFTER
PROBE before-delete rows: acct=1 task=1
PROBE delete outcome: THREW TypeError: ObjectQL.planCascadeAtomicity: `reference` is an
object, and FieldSchema declares it as an optional STRING …
PROBE after-delete rows: acct=1 task=1 <- nothing touched
```
No `restrict` refusal, no `set_null`, nothing logged, and the caller
told the delete succeeded. The refusal now fires before any row is
touched, because `delete()` calls `planCascadeAtomicity` first.
⭐ Two facts the probe forced, both worth recording: the engine's own
WRITE path already refuses this shape (`insert` runs
`assertReferencesResolve` → `referenceTargetOf` → the same arbiter), so
the child row had to be written straight through the driver — which is
exactly the provenance the arbiter's docblock names (a raw
`registerObject`, a stored row rehydrated past its schema). And the
registry's own completeness check printed
`field/relationship-without-reference` while the cascade stayed silent:
the platform reported the shape in one channel and mis-read it in
another.
### Ablation — the pins can fail, proved on disk by content hash
One routed read reverted through `scripts/ablation-replace.mjs` (⛔ not
`sed -i`), on the committed tree:
```text
anchor "const ref = referenceCarrierOf(fdef, 'ObjectQL.planCascadeAtomicity');" x1 -> x0
replace "const ref = fdef.reference;" x0 -> x1
blob 4ca2839 -> 6121f5c9bf429db31f959af2e43711506db8baf0
ok mutation landed: anchor 1 -> 0, blob 4ca2839 -> 6121f5c9bf42
x seam 1 (planCascadeAtomicity): an unreadable carrier refuses the delete BEFORE any row is touched
Test Files 1 failed (1)
Tests 1 failed | 5 passed (6)
restore blob after restore 4ca2839
blob at HEAD 4ca2839
git diff HEAD empty
```
Restored with the explicit form, verified independently afterwards: `git
diff HEAD` empty and `git hash-object` equal to the HEAD blob. The
redness is targeted — exactly the mutated seam's pin failed while seam 2
and all three absence controls stayed green, so the pins are per-site
rather than one shared assertion.
### Tests
| package | result |
|---|---|
| `@objectstack/objectql` | 300 files / **5009 passed** |
| `@objectstack/rest` | 194 files / **3245 passed**, 1 skipped |
| `@objectstack/metadata-protocol` | 180 files / **2585 passed**, 19
skipped |
| `@objectstack/lint` | 104 files / **3935 passed** |
| `@objectstack/verify` | 15 files / **116 passed** |
| `typecheck` (all five, incl. test layers) | exit 0; `objectql`'s
shrink-only test ledger held at 40 files / 234 errors / 65 pinned
signatures — not raised |
⭐ `check:type-check-debt` re-measured all 4 DEBT ledger entries at the
final head: 53 raw errors, **none above its recorded number**.
### Repo-wide lint — run, ⛔ not narrowed
`pnpm lint` (`eslint . --no-inline-config`) at `bd1b3dcad`: **exit 0**,
75s. No narrowing is claimed, so no narrowing evidence is owed.
### Gate census — derived from the REAL change set
> reference carrier at the ten residual readers (ruling E item 2 residue) (#19080)1 parent c70581b commit a675ad4
18 files changed
Lines changed: 1011 additions & 21 deletions
File tree
- .changeset
- content/docs/ui
- packages
- lint/src
- metadata-protocol/src
- objectql/src
- rest/src
- verify/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| |||
285 | 285 | | |
286 | 286 | | |
287 | 287 | | |
| 288 | + | |
288 | 289 | | |
289 | 290 | | |
290 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4234 | 4234 | | |
4235 | 4235 | | |
4236 | 4236 | | |
| 4237 | + | |
| 4238 | + | |
| 4239 | + | |
| 4240 | + | |
| 4241 | + | |
| 4242 | + | |
| 4243 | + | |
| 4244 | + | |
| 4245 | + | |
| 4246 | + | |
| 4247 | + | |
| 4248 | + | |
| 4249 | + | |
| 4250 | + | |
| 4251 | + | |
| 4252 | + | |
| 4253 | + | |
| 4254 | + | |
| 4255 | + | |
| 4256 | + | |
| 4257 | + | |
| 4258 | + | |
| 4259 | + | |
| 4260 | + | |
| 4261 | + | |
| 4262 | + | |
| 4263 | + | |
| 4264 | + | |
| 4265 | + | |
| 4266 | + | |
| 4267 | + | |
| 4268 | + | |
| 4269 | + | |
| 4270 | + | |
| 4271 | + | |
| 4272 | + | |
| 4273 | + | |
| 4274 | + | |
| 4275 | + | |
| 4276 | + | |
| 4277 | + | |
| 4278 | + | |
| 4279 | + | |
| 4280 | + | |
| 4281 | + | |
| 4282 | + | |
| 4283 | + | |
| 4284 | + | |
| 4285 | + | |
| 4286 | + | |
| 4287 | + | |
| 4288 | + | |
| 4289 | + | |
| 4290 | + | |
| 4291 | + | |
| 4292 | + | |
| 4293 | + | |
| 4294 | + | |
| 4295 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| 104 | + | |
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
| |||
377 | 378 | | |
378 | 379 | | |
379 | 380 | | |
380 | | - | |
381 | | - | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
382 | 397 | | |
383 | 398 | | |
384 | 399 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
471 | 471 | | |
472 | 472 | | |
473 | 473 | | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
| 124 | + | |
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
| |||
549 | 550 | | |
550 | 551 | | |
551 | 552 | | |
552 | | - | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
553 | 561 | | |
554 | 562 | | |
555 | 563 | | |
| |||
0 commit comments