Repository navigation
Commit 9af0005
feat(spec): ObjectSchema.attachedOnRead declares per-caller read attachments, and the shared validator judges record.BLOCK.LEAF against them (#22425)
Fixes #22386
Clause-②: yes (widening: a new optional `ObjectSchema` key)
The spec half of #22211's ruling A (`6070963704`), built under the claim
`6073245703` as amended by the seat order `6074312931`. **#22387
follows**: the plugin-approvals declaration of `viewer` on
`sys_approval_request` and its conformance test. That card is blocked on
this PR, and it is the one that finishes #22211; this PR leaves #22211
open, and its 8 shipped `record.viewer.*` predicates stay refused until
#22387 declares the block.
## What changes
- **`ObjectSchema.attachedOnRead`**
(`packages/spec/src/data/object.zod.ts`, beside `fields`). An optional
map from block name to that block's leaves, each naming its value type:
`attachedOnRead: { viewer: { can_act: 'boolean', can_override:
'boolean', is_submitter: 'boolean' } }`. These are the blocks a service
attaches to each row it serves, computed per caller and never stored.
- Both key levels take the field-name grammar. A leaf type is one of
`number | text | boolean | date`, taken from `FieldType` (the four
`Field.returnType` value types).
- **Refused at parse**, each located at the offending key: a leaf type
outside the four, a leaf that is a nested block or a field definition, a
block or leaf name outside the grammar, a block that names no leaf, and
a block that repeats a declared field name. The last two live in the
object's existing `superRefine`, so no new check node reaches the JSON
Schema projection.
- **Not a field.** No driver, form, list view, export, write path or
translation bundle reads it. `git grep -c attachedOnRead` at the head
finds it only in `packages/spec` (the schema, its pins, the ledger row,
the generated surface), `packages/lint/src/validate-expressions*.ts`,
`packages/formula/src/{validate,expression-refusal}*.ts`,
`packages/metadata-core/src/object-schema-fls-references.ts` (classified
`keep`), the generated reference docs and the changesets.
- **Reader 1, the shared validator.**
- `@objectstack/lint` `buildFieldIndex` adds each declared block name to
the field-existence set, and both call sites that pass the field index
hand the shared validator the block's leaves.
- `@objectstack/formula` `checkFieldExistence` judges the SECOND segment
of `record.BLOCK.LEAF` (and `previous.`) against the declared leaves,
through the new optional `ExprSchemaHint.attachedOnRead`, under the
EXISTING `unknown-field` code. No new rule id, no new refusal code,
nothing keyed on `viewer`.
- The refusal: `field` is the dotted path as written, `suggestion` the
nearest declared leaf, and the new optional `block` + `leaves` params
carry the declared leaves, present together exactly when the message
names them: ``unknown field `viewer.can_actt` on `approval` (the read
attachment `viewer` declares `can_act`, `is_submitter`) — did you mean
`viewer.can_act`?``
- The head scan `RECORD_REF_RE` is untouched (the second segment is read
by a separate sticky regex), and the judgement runs only for a declared
block, so an object without the key takes exactly today's path.
- **The leaf types' reader** is #22387's conformance test, which pins
the keys `attachViewers` emits and the runtime type of each value
against the declaration (seat order `6074312931`, recorded on #22387 as
`6074324160`). The liveness row says so.
- **Closed-world pins the new key moves** (seat order `6074312931`, Q1):
the ADR-0106 D1 FLS position `attachedOnRead: keep`; the object form
ledger's root `omit` row (the ruling says no form reads it); the
composeStacks collection lists in the two pins, plus one row pinning
that two stacks declaring different `attachedOnRead` for one object are
refused under `objectConflict: 'merge'`. No `stack.zod.ts` change: the
composition derives the set from the shape walk.
- **Ledger and generated artifacts.** `liveness/object.json` grades the
key `live` on an authoring consumer (the `action.execution` precedent),
with `producer`, `evidenceScope` and `verifiedAt`. Generated by the
repo's generators only: `authorable-surface/data.json` (build),
`liveness/state-counts/object.md` (`gen:liveness-counts`),
`content/docs/references/**` (`gen:docs`).
- **Changesets.** `@objectstack/spec` minor (carries the Clause-② line);
`@objectstack/formula` minor (additive public members);
`@objectstack/lint` and `@objectstack/metadata-core` patch.
## Merge round at `a5104d854`
After the contract review at `e19c1e349` (**PASS** `6077844912`; `Test
Core (6/6)` was red only on #22415's timing-drift grade, which `main`
reverted in #22435), `origin/main` was merged twice through
`os-regen-merge.sh`, never rebased, with no conflict:
- the first merge, up to `da159f74e`, brought the revert;
- the second, up to `2b61f2d9d`, brought #22315, this card's named
serial predecessor on `validate-expressions.ts`.
The PR's delta is unchanged: `git diff origin/main...HEAD` is 22 files,
+753 / −11, line for line.
Measured at `a5104d854`, after `pnpm install --frozen-lockfile` and a
full build (73 / 73):
- lint 130 / 130 files, 5909 passed;
- formula 45 / 45, 1278;
- spec 630 / 630, 18791 + 1 todo;
- gates: 111 derived, 111 run, 0 NOT-MEASURED, all exit 0.
## Merge round at `e19c1e349`
After the contract review **PASS** `6076679507` at `a0545b853`,
`origin/main` `05c7c3fa3` was merged through `os-regen-merge.sh`, never
rebased.
- **The one conflict:** the #5017 `PLUMBING` hunk in
`validate-expressions.test.ts`. Both sides are kept: #22394's five
names, then `attachedOnReadIndex`. `validate-expressions.ts`
auto-merged.
- **F4 fixed:** the form-ledger omit row's `why` now names the shared
build validator.
- **Measured at `e19c1e349`:**
- lint 129 / 129 files, 5899 passed (#22394's 13 included);
- formula 45 / 45, 1277;
- spec 628 / 628, 18769 + 1 todo;
- lint typecheck 0;
- gates: 111 derived, 111 run, 0 NOT-MEASURED, all exit 0 after a full
turbo build.
- `metadata-protocol` and `metadata-core` are as below; the merge did
not touch them.
## Measured at `a0545b853`
The patch round after the contract review `6075426209` (FAIL on the
sixth closed-world pin) merged `main` at `3054516ef` cleanly through
`os-regen-merge.sh`. All runs went through `os-verify-lock` (each
`VERDICT command-exit 0`; shared-box seconds), after the
`metadata-protocol`, `lint` and `metadata-core` closures were built:
| tier | result |
|---|---|
| `@objectstack/metadata-protocol` | 223 / 226 files (3 env-gated
live-DB skips), 28325 passed, 19 skipped.
`protocol.meta-types-degenerate-derivation.test.ts` passes. |
| `@objectstack/spec` unit (`--project local`) | 628 / 628 files, 18769
passed, 1 todo |
| `@objectstack/lint` | 129 / 129 files, 5886 passed |
| `@objectstack/formula` | 45 / 45 files, 1277 passed |
| `@objectstack/metadata-core` | 17 / 17 files, 421 passed |
Typecheck exits 0 for `metadata-protocol` and `spec` at `a0545b853`.
`lint`, `formula` and `metadata-core` exited 0 at `4ba858882`, and their
sources are unchanged since.
**Gates.** `dispatch-gates --commands` at `a0545b853` derived 111
commands: the previous 110 plus `check:durability-log-level`. All 111
exited 0 on the first pass, after a full `turbo run build`. `--ran`
reconciles **111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN**.
**The closed-world inventory of `ObjectSchema`'s top-level keys**,
searched before the push. Every list or count found is visited:
- `metadata-core` `OBJECT_REFERENCE_POSITIONS`;
- the form-reconciliation ledger;
- the two `composeStacks` pins;
- `metadata-protocol` `CARD_PROPERTY_COUNTS.object` (44 → 45, this
round);
- `validate-expressions.test.ts`'s #5017 read surfaces;
- the liveness row and its state counts;
- the authorable surface and the reference docs.
Derived sets needed no edit: the `composeStacks` shape walk,
`ObjectSchema.create()`'s known keys, and the object form, where the key
is an omit row.
**Ablation: the misspelt-leaf pin bites.** `node
scripts/ablation-replace.mjs` on `packages/formula/src/validate.ts`,
anchor `if (leaves.includes(leaf)) return;` replaced by `return;` plus a
marker:
- the anchor went x1 to x0, and the blob `f7e24675` to `ac2f0590`;
- `validate-attached-on-read.test.ts`: 3 failed, 6 passed. The
misspelt-leaf, no-near-leaf and `previous`-root pins went red; the
accept and control pins stayed green, which is the expected direction;
- restore: blob == HEAD `f7e24675`, `git diff HEAD` empty, marker count
0; the restored run passed 9/9.
The ablation ran at `37711ce70`. The file's blob at `4ba858882` is the
same `f7e24675`. Formula's tests import `./validate` from source, so the
ablation has no dist leg.
## Acceptance notes
- **Doors that do not read the key, carried by #22387 (contract review
`6075426209`, F1).** `packages/mcp`'s expression tool and
`service-automation`'s flow-registration resolver build their own
`record.*` field sets from `fields`. The wording here names only the
shared build validator. #22387 threads the key into both, or shows that
they are unreachable (`6075461181`).
- **A block named like an injected system column is not refused at parse
(F2), measured.** `object.zod.ts` cannot reach those names:
`resolveInjectedSystemColumns` imports from `./object.zod`, which would
make a cycle, and its column constants are module-private. Widening the
refusal needs those constants exported below `object.zod.ts`. Carrier:
none; the docblock records it.
- **`ObjectExtensionSchema` cannot carry the key (F3).** That is
consistent with an in-code declaration beside the service that attaches
the block.
- **Out of scope, carrier #22387 (noted, not filed).** This is the
boundary of the ruling's literal "field-existence set". A declared block
resolves at every surface the lint field index serves on that object:
validation rules, hooks, field formulas, flow conditions. Yet only rows
the declaring service serves carry the block. No producer writes such a
predicate today, so this reaches nothing and is not a card. The
field-formula pin in this PR covers the refusal half only.
- **Where the judgement lives, measured.**
`validate-predicate-path-refs.ts:55`–`:63` (#7010) scopes itself to the
`data.*` layer and leaves `record.*` to the sibling rules. Judging there
would be a new rule id, so the judgement lives in formula's
`checkFieldExistence`, the existing `unknown-field` site.
- **Name kept: `attachedOnRead`.**
- The producer's own verb is "attach" (`attachViewers` /
`attachDecisionProgress` / `attachFlowSteps`), and "OnRead" follows the
spec's `enforceOnRead`.
- The name has zero collisions repo-wide.
- `readAttachments` would read as file attachments, and "virtual" or
"computed" would read as fields.
- **Unjudged, deliberately.** Index access (`record.viewer['x']`), a
method call on a block, and a third segment are not judged. Each is a
missed catch, never a false refusal.
- **Merges.** `origin/main` was merged twice through
`scripts/pm/os-regen-merge.sh`, never rebased. #22392 (#22274) landed on
`validate-expressions.ts` in between; its three symbols are verified
present at the head.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d87dff6 commit 9af0005
22 files changed
Lines changed: 753 additions & 11 deletions
File tree
- .changeset
- content/docs/references
- api
- data
- system
- packages
- formula/src
- lint/src
- metadata-core/src
- metadata-protocol/src
- spec
- authorable-surface
- liveness
- state-counts
- src
- data
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
948 | 948 | | |
949 | 949 | | |
950 | 950 | | |
| 951 | + | |
951 | 952 | | |
952 | 953 | | |
953 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| 155 | + | |
155 | 156 | | |
156 | 157 | | |
157 | 158 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
329 | 329 | | |
330 | 330 | | |
331 | 331 | | |
| 332 | + | |
332 | 333 | | |
333 | 334 | | |
334 | 335 | | |
| |||
616 | 617 | | |
617 | 618 | | |
618 | 619 | | |
| 620 | + | |
619 | 621 | | |
620 | 622 | | |
621 | 623 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
235 | 235 | | |
236 | 236 | | |
237 | 237 | | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
238 | 258 | | |
239 | 259 | | |
240 | 260 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
100 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
101 | 114 | | |
102 | 115 | | |
103 | 116 | | |
| |||
| 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 | + | |
0 commit comments