fix(spec): hook condition row declares expression, not javascript - #20475
objectstack-fleet[bot] merged 4 commits into
Conversation
HookSchema.condition is a CEL predicate (EvaluatedExpressionInputSchema), but hook.form.ts declared its `condition` row `language: 'javascript'`, same as the real script rows (body.source, action.source). A consumer keyed on the declared language cannot tell the predicate apart from a script. Change the row to `language: 'expression'`, matching every sibling predicate row, and pin it beside the existing form-row pins. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
en.metadata-forms.generated.ts picks up the new condition helpText (node scripts/check-i18n-bundles.mjs --write). The other 8 packages' bundles regenerated byte-identical; zh-CN/ja-JP/es-ES kept their existing translated-locale values under merge mode (now stale relative to the new English source, per the tool's documented behaviour). --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
…ok-condition-expression-row
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 2 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: ① Derived judgmentsInputs: card #20439 (body; triage grade
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ok-condition-expression-row
…d inlineColumns form rows (objectstack-ai#19332, flight G2b) (objectstack-ai#20485) Part of objectstack-ai#19332 Flight G2b of ruling 5861442317. Clause-②: no ## Status: draft, no open gap The first round stopped at one red test outside the claim's surface, `packages/lint/src/validate-predicate-path-refs.test.ts`, the lint census of every predicate the shipped metadata forms carry. The claim both admitted mechanically moved population pins and forbade `packages/lint/**`. The seat answered A and amended claim `5873857698` in place (its "Amended 2026-09-28T17:27Z" line): that one file joined the surface for its census pin only. The patch round landed it in `16037890` (**Pins moved** below), and the file reads 54 of 54. Under the claim's second amendment ("Amended 2026-09-28T17:51Z"), `2bcad436` corrects one G2a text: the `indexes.fields` sub-row's help text (and its code comment and four catalogue leaves) now reads "Saving does not check them; publishing and os validate refuse a name that is not a field of this object. A field that is not a stored column (a formula, say) makes the SQL driver skip the whole index, with a warning in the server log." Each clause was measured on this tree after `4b2d9041` (objectstack-ai#20479): `ObjectSchema.safeParse` accepts a misspelt column; `os validate`'s rules and the runtime publish gate (`runRuntimeAuthoringRules`, type `object`) both return `object-field-ref-unknown` at `error` on it; and an in-memory SQLite `SqlDriver` sync skips an index on a formula field (and one on a misspelt name) with `[sql-driver] skipping declared index … column(s) not materialized` at `warn`, while the index on a stored column is created. ## What Four live keys had no form row, so an author could reach them only through the Source tab. Each is now a row with hand-written sub-rows, as the ruling says: 「**G2 (…) — hand-written curated sub-rows**, plus … one nested `subset` row for `inlineColumns`」. The four-locale catalogue rows are in this PR. | key | form, section | row, sub-rows (face) | the row each copies | |:--|:--|:--|:--| | `object.activityMilestones` | `object.form.ts`, Advanced, after `validations` | `type: 'repeater'`: `field` (`widget: 'text'`, required), `value`, `summary` (text, required), `type` (text) | the `fieldGroups` repeater face (declared, labelled sub-rows); the text sub-rows copy the plain text rows in Basics | | `object.publicSharing` | `object.form.ts`, Advanced, after `requiredPermissions` | `type: 'composite'`: `enabled` (switch), `allowedAudiences` and `allowedPermissions` (`widget: 'multiselect'` with inline options), `maxExpiryDays` (number, `min: 1`), `redactFields` (`widget: 'string-tags'`), `eligibility` (`type: 'code'`, `language: 'expression'`) | the `access` / `lifecycle` composite face; `enabled` the `enable` toggles; the two lists the multiselect objectui derives for an array of enum (the derived `appearance.allowedVisualizations` on the view and page forms); `redactFields` the `highlightFields` row; `eligibility` the `fields.visibleWhen` predicate rows | | `object.userActions` | `object.form.ts`, Advanced, under `managedBy` | `type: 'composite'`: `create`, `import`, `edit`, `delete` (`widget: 'json'`), `exportCsv` (switch) | the composite face; the four union keys the G1a `requiredPermissions` row (`json` on a union); `exportCsv` the `enable` toggles | | `field.inlineColumns` | `field.form.ts`, Configuration, between `inlineTitle` and `inlineAmountField`, gated `data.type == 'master_detail'` like both | `type: 'repeater'` over a curated subset: `name` (text, required), `label` (text), `width` (number), `defaultHidden` (switch) | the `fieldGroups` repeater face; the gate copies its two sibling rows | **Shapes (dispatch assumption 2), confirmed on this base:** `activityMilestones` is `z.array(strictObject(…))` at `object.zod.ts:2093`, four keys; `publicSharing` is a `strictObject` at `:2286`, six keys; `userActions` a `strictObject` at `:1769`, five keys; `inlineColumns` is `z.array(InlineGridColumnSchema)` at `field.zod.ts:1460`, the item schema at `:890`, twenty keys. The gate's own `keysOf` read the same sets. **Ledger:** one nested `subset` row at `field` / `inlineColumns` in `metadata-form-zod-reconciliation.test.ts`. Its shape is the `object` / `fields` subset row at the top of the ledger (and its two depth-two children `fields.options`, `fields.summaryOperations`), which is the ledger's `subset` precedent. The other three keys need no row: every key they declare is offered. **Row titles:** the two new repeaters' row schemas carry a JSON Schema `title` on every property (**Row titles** below). ## Faces that needed a reason - **`activityMilestones.field` pins `widget: 'text'`.** Read at the `.objectui-sha` pin on `main`, `dd3f7e1b`: with no `widget`, `SchemaForm`'s `resolveFieldWidget` runs its name conventions, and `detectFieldRefWidget` turns a string property named `field` into the `field-ref` picker whenever `widgetContext.objectFields` is present. `ResourceEditPage` always hands that over as a load state, and on an object draft it is `idle` (the draft names no `object` / `objectName` / `data.object` / `interfaceConfig.source`). `FieldRefWidget` then renders a select offering only "None", so a new milestone could not name its field. An explicit `widget` skips the conventions, and `text` is a passthrough hint, so the face is a plain input. - **`redactFields` pins `widget: 'string-tags'`** for the same reason: a string list named `...Fields` becomes `field-multi` by the same convention. - **`userActions.create` / `import` / `edit` / `delete` take `widget: 'json'`**, the ruling's union rule (「Union-typed values take `json`」). Each is a boolean or a strict `{ enabled, visibleWhen, disabledWhen }` object. At the pin, `json` is a passthrough hint: `resolveFieldFace` picks the stored value's union branch. A new entry or a stored boolean renders the switch (the first arm), and a stored object renders its three keys as a nested form, whose `setField` merges each edit into it. No face writes one arm over the other. The object arm is written in source and edited here once stored. - **`allowedAudiences` / `allowedPermissions` take `widget: 'multiselect'`** with inline options. Every member is a spellable option value. `MultiSelectWidget` writes `undefined` when every choice is cleared, so the form cannot store the empty list `getPolicy` reads as "any audience". - **The objectui faces re-checked here are the ones G2a did not use**, read at `dd3f7e1b` (G2a read the repeater face at `f8a9d0fb`): the declared composite (`CompositeField`, `pickSubSchema` reading `properties[NAME]` after `inlineSchemaRefs`), the multiselect widget, the switch and number branches of the scalar chain, and the `json` hint on a union sub-row. Code readings only, no browser run. ## `inlineColumns`: the curated subset - **Offered:** `name`, plus the three keys that apply to a column of any type, `label`, `width` and `defaultHidden`. An entry that names only a field is what the key's own describe recommends, because objectui's `hydrateColumns` completes it from the child field. - **Deliberately not offered (recorded in the `subset` row):** - `type`: declaring it opts the column out of that hydration. - `options`, `reference`, `displayField`, `idField`, `autofill`, `multiple`, `accept`, `prefix`, `step`, `scale`, `computed`, `expr`: each applies to one cell type only. A column takes its type from the child field at render, and no sub-row `visibleWhen` here can see it, so each would be offered on every column. - `required`, `readonlyWhen`, `requiredWhen`: hydration copies them from the child field, where the rule the server enforces lives. - The nested reading below shows the row is load-bearing: without it, the gate's `zodOnly` for `field.inlineColumns` is exactly those sixteen keys. ## Where a misspelt field name is refused, read from the code The ruling's 「a misspelling is refused loudly at parse」 does not hold for three of this flight's four name positions. Each help text claims only what is measured. | position | parse | publish door / `os validate` | runtime with a miss | help text claims | |:--|:--|:--|:--|:--| | `publicSharing.redactFields[]` | accepts | **refused**: `validate-object-field-refs` owns it at `error` (`runtimeTypes` includes `object`); probe below | the redaction never binds (fails open) | "refused at publish" | | `activityMilestones[].field` | accepts (probe) | not judged: `validate-object-field-refs` leaves it out by name; probe below | `matchMilestone` compares `after[field] === value`, so the milestone never fires | "Nothing checks it when you save or publish … never fires" | | a `{token}` in `activityMilestones[].summary` | accepts | not judged | `renderMilestoneSummary` renders it empty | "a token that names no field renders empty" | | `inlineColumns[].name` | accepts (probe) | not judged; probe below | `hydrateColumns` leaves an unknown name unhydrated, a plain text column | "Nothing checks it when you save or publish … renders a plain text column" | Seat 2's objectstack-ai#20479 (for objectstack-ai#20432), which landed on `main` as `4b2d9041` during this flight, extends `validate-object-field-refs` to four field-level lists and `indexes[].fields`. Read on `origin/main`, its list positions still do not include `activityMilestones[].field` or `inlineColumns[].name`, so these texts stay true (**Out-of-scope finding** below). ## Other help-text claims, each read from its consumer - `activityMilestones`: an update that moves the field into the value writes the summary in place of the field-change entry, and the first match wins (`audit-writers.ts` `matchMilestone`). The comparison is strict, and `value` is a string, so a milestone on a number or boolean field never fires. A lookup, master-detail or user token shows the referenced title (`REFERENCE_FIELD_TYPES`). An unset `type` is `updated`: the update branch starts from `activityTypeFor('update')` and a milestone replaces it only when it names one. (The schema's describe says the default is "completed"; see Acceptance notes.) - `publicSharing` (`share-link-service.ts`): `enabled` is re-read on every redemption; unset audiences default to `['link_only']` and permissions to `['view']`; `createLink` refuses any other with 422. Every audience still needs the token: `resolveToken` adds a signed-in check for `signed_in` and an allowlist check for `email`. `maxExpiryDays` defaults to 365, and a link created without an expiry is stored with none (`expiresAt ?? null`), so the cap does not force one. `eligibility` binds `record`, is checked at mint and at every redemption, and a predicate that does not compile or faults refuses. - `userActions` (`resolveCrudAffordances`): the per-bucket defaults in the help text are `CRUD_AFFORDANCE_DEFAULTS` verbatim. On an `engine-owned` or `append-only` object, turning a verb on also passes plugin-security's `assertEngineOwnedWriteAllowed`, so users can make that write through the data API. - `inlineColumns`: read only when the field sets `inlineEdit` (`attachInlineSubforms`). Unset, `deriveColumns` curates past six columns into the column chooser. `defaultHidden` never hides a required column (`GridField`: `c.defaultHidden && !c.required`). ## Row titles (admitted by the claim from the start) - **The guard.** `repeater-item-titles.test.ts` (objectstack-ai#17232) requires a JSON Schema `title` on every authorable property of every repeater's row schema, and forbids a ledger entry. Both new repeaters are new carriers: `object:activityMilestones` and `field:inlineColumns`. - **The change.** 24 `.meta({ title })` calls, and nothing else in either file: - `object.zod.ts`, the `activityMilestones` entry: `field` 'Field', `value` 'Value', `summary` 'Summary', `type` 'Type'. - `field.zod.ts`, `InlineGridColumnSchema`, all twenty properties: Name, Label, Type, Width, Required, Options, Prefix, Step, Reference, Display Field, ID Field, Multiple, Accept, Default Hidden, Computed, Expression, Scale, Autofill, Read-only When, Required When. - The four offered sub-rows' titles equal their declared labels. - **Byte proof.** Stripping exactly the added calls line by line gives each file's base blob byte for byte: `object.zod.ts` sha256 prefix `8979b5feea7ed0ff` both ways (4 removed), `field.zod.ts` `24713de3b50d5f71` both ways (20 removed). - **Reverse verification.** Run through `scripts/ablation-replace.mjs` in wrap mode, on the committed state. Deleting the `Summary` title reads `object:activityMilestones … expected [ 'summary' ] to deeply equal []`, 1 failed of 29. Deleting the `Default Hidden` title reads the same for `field:inlineColumns` with `[ 'defaultHidden' ]`. The tool proved each mutation landed (anchor 1 → 0, blob changed) and each restore (blob equals HEAD, `git diff HEAD` empty). - **No accept set moves.** `check:generated` reads all 15 artifacts up to date on this head, `check:authorable-surface` and `check:api-surface` included. ## Residue of the reconciliation gate (dispatch assumption 1) The test file's own helper block was copied verbatim into a probe that was never committed, and run with the gate's own functions. At base it is lines 1-838, sha256 prefix `5e04d44fc5c5edb3`, the prefix G2a read. On this branch it is lines 1-851, and it differs from the base block only by the 13 inserted ledger lines. Residue = offerable root keys − offered − root `omit` rows, per type, with `view` apart. Controls, asserted inside the probe: lit, `name` is offered by 17 of 17 forms; dark, `object.zzFabricated19332G2b` and `object.name` are in no residue. | tree | residue | per type | view | |:--|:--|:--|:--| | base `e956924e` | **4** | object 3, field 1 | 42 | | this branch (`82c5b111`, forms and ledger as on the head) | **0** | none | 42 | Removed: `object.activityMilestones`, `object.publicSharing`, `object.userActions`, `field.inlineColumns`. Added: none. Nested reading on the branch, through the gate's own `reconcileNestedLists`: - `field.inlineColumns` reads `zodOnly = []` with the `subset` row, and without it `zodOnly` = `accept, autofill, computed, displayField, expr, idField, multiple, options, prefix, readonlyWhen, reference, required, requiredWhen, scale, step, type`. - `object.activityMilestones`, `object.publicSharing` and `object.userActions` read `formOnly = [] · retired = [] · zodOnly = []` with or without any row of their own. ## Pins moved (measured, mechanical) | file | pin | from → to | why | |:--|:--|:--|:--| | `object-collapsed-sections-echo-decisions.test.ts` | collapsed-section leaves / `advanced` | 69 → 105 / 60 → 96 | three new Advanced rows with fifteen sub-rows: 18 rows, 36 leaves | | `object-lifecycle-panel-echo-decisions.test.ts` | translated `.label` control, per locale | 634 → 657 | 23 new row labels | | `field-panel-echo-decisions.test.ts` | the field form's repeater row properties / walked parents | 6 → 10 / `['options']` → `['options', 'inlineColumns']` | the new `inlineColumns` repeater and its four children, all translated | | `packages/lint/src/validate-predicate-path-refs.test.ts` (admitted by the claim's 17:27Z amendment) | predicates / literal comparisons | 81 → 82 / 56 → 57 | the one new predicate, `field :: inlineColumns` on `data.type == 'master_detail'`. Measured, not inferred: the shipped corpus, keyed `FORM::FIELD::SOURCE`, was enumerated at the merge base `e956924e` (81 predicates, 56 comparisons) and on this branch (82, 57), and the difference is exactly that one entry added and none removed | ## Verification Test runs went through `scripts/pm/os-verify-lock.sh`. The table is the first round's, at `66b73be5`, and the lint row is the patch round's, at `16037890`. After the merge and the G2a text correction, these re-ran at the final head `2bcad436`: `pnpm --filter @objectstack/spec test` `Test Files 572 passed (572)` · `Tests 16791 passed \| 1 todo (16792)`; `pnpm --filter @objectstack/platform-objects test` `Test Files 55 passed (55)` · `Tests 911 passed (911)` (the text change moves no pin); lint `validate-predicate-path-refs.test.ts` + `validate-object-field-refs.test.ts` 2 files, 114 passed; `pnpm check:i18n` OK (9 packages in sync) after the three translated leaves were authored and a second `--write` left no source-hash row; `pnpm --filter @objectstack/spec check:generated` `All 15 generated artifacts are up to date`. | run | result | |:--|:--| | `pnpm --filter @objectstack/spec test` | `Test Files 569 passed (569)` · `Tests 16698 passed \| 1 todo (16699)` | | `pnpm --filter @objectstack/spec test:repo` | `Test Files 38 passed (38)` · `Tests 690 passed (690)` | | `repeater-item-titles.test.ts` + `metadata-form-zod-reconciliation.test.ts` | `repeater-item-titles.test.ts` 29 + `metadata-form-zod-reconciliation.test.ts` 57: `Test Files 2 passed (2)` · `Tests 86 passed (86)` | | `pnpm --filter @objectstack/platform-objects test` | `Test Files 55 passed (55)` · `Tests 911 passed (911)` (before the pin moves: 4 failed, the three pins above) | | `pnpm --filter @objectstack/spec typecheck` / platform-objects `typecheck` | exit 0 both; `check:test-typecheck: OK` (53 file(s) / 251 error(s) / 138 pinned; 1 / 3 / 2) | | `pnpm check:i18n` | `check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys)` | | `pnpm --filter @objectstack/spec check:generated` | `All 15 generated artifacts are up to date` | | metadata-protocol `src/protocol.meta-types-*.test.ts` | 4 files, 58 passed | | cli unit `test/i18n-coverage.test.ts`, `test/i18n-duplicate-demand.test.ts` | 2 files, 27 passed | | lint `src/validate-predicate-path-refs.test.ts` at `16037890` | `Test Files 1 passed (1)` · `Tests 54 passed (54)` (2 failed before the pin move, the two pins above) | Catalogues: `node scripts/check-i18n-bundles.mjs --write` regenerated the 46 `en` leaves (23 rows, a label and a help text each). The 138 translated leaves were then authored in zh-CN, ja-JP and es-ES, with no `en` echo. A second `--write` kept every translated value and left no source-hash row. Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 87 commands at the final head `2bcad436` (change set vs merge base `9801da12`, after the merge `e809f0bd`: the 14 files of this diff). That is the first round's 86 plus `check:docs-transcript-drift`, which the lint test file brings in. All 87 ran on that head, and each exit code went to disk before it was read. In every round `pnpm check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: nine packages had no `dist/` in the fresh worktree); later gates in the same run built them, and the rerun exited 0 (`104 published require entry point(s) across 66 package(s) load`), which is the code `ran.list` records. `--ran` reports: `87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED (a DERIVED zero — all 87 recorded an exit code and none of them is 3)`. Reach probe (built `@objectstack/spec` and `@objectstack/lint` of this tree, never committed): an object with `activityMilestones: [{ field: 'statsu', … summary: 'Done: {titel}' }]` and `publicSharing.redactFields: ['titel']`, and a child `master_detail` field with `inlineColumns: [{ name: 'quantiy' }]`. `ObjectSchema.safeParse` and `FieldSchema.safeParse` both succeed. The reference-integrity suite, which the publish door and `os validate` run, returns exactly one finding, `object-field-ref-unknown @ objects[0].publicSharing.redactFields[0]` (the lit control), and none for the milestone field, the token or the column. ## Acceptance notes - **`activityMilestones[].type`'s describe says the default is "completed".** The runtime writes `updated`: the update branch's `activityTypeFor('update')`, replaced only by a milestone that names a type. The help text states the runtime. The showcase milestone names `type: 'completed'` explicitly, so no measured author relies on the describe. Carrier: none. - **The `public` audience's TSDoc (`object.zod.ts`) says "search engines may index; no token check".** `resolveToken` has no branch for `public`: it redeems like `link_only`, token required. The option label says only "Public", and the help text says every audience needs the link. Carrier: none. - **`maxExpiryDays` does not force an expiry.** A link created without one never expires. That matches the key's describe ("Reject links with expiry beyond this many days"), and the help text says it outright. Whether a capped object should require an expiry is a product question. Carrier: none. - **An untouched `userActions` switch reads off** even where the `managedBy` default offers the entry, a switch having no unset state. The composite's help text names the defaults. Carrier: none. - **Existing object-form rows named `field` meet the same `field-ref` convention.** `lifecycle.ttl.field` has `type: 'text'` and no `widget`, so by the reading above it renders the "None"-only picker on an object draft. This is a code reading at `dd3f7e1b`, not browser-run, and it is outside this flight's rows. (`fields.summaryOperations.field` sits inside the `fields` row, which the Studio object page hides as canvas-owned.) Carrier: none. - **Concurrency.** `origin/main` was merged once, with `scripts/pm/os-regen-merge.sh`, at `9801da12` (`e809f0bd`), because objectstack-ai#20456's `e967cbd2` edited three `view` `why` texts in the reconciliation ledger. It merged without conflict, `main`'s side was taken for every generated artifact it moved, and `check:generated` then read all 15 up to date. `origin/main` has moved since (to `3062e500`), not onto a file of this diff. Seat 2's objectstack-ai#20475 regenerates `en.metadata-forms.generated.ts` too and is not on `main` yet: ordinary concurrency. - **G2a's `indexes.fields` help text went stale when objectstack-ai#20479 landed, and is corrected here** (Status, second paragraph), under the claim's 17:51Z amendment. No other G2a row changes. ## Out-of-scope finding (folded into objectstack-ai#20432 by the seat; not filed by this run) - **class c · reach: the save door and the publish door, measured (probe above).** `activityMilestones[].field` and `inlineColumns[].name` name fields of the owning object, and no authoring door judges them. A misspelt milestone field silently never fires, and a misspelt column renders as plain text. `validate-object-field-refs` leaves the first out by name. Its extension objectstack-ai#20479, landed as `4b2d9041`, reaches four field-level lists and `indexes[].fields`, but neither of these. - Dedupe words: `activityMilestones field unknown` · `inlineColumns name unknown field` · `milestone never fires misspelt field` · `inline grid column reference integrity`. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…its that decided them (stage 3) (objectstack-ai#20533) Part of objectstack-ai#20234 Clause-②: no ## What changed This is stage 3 of the staged sweep. It covers `packages/spec/src/data/**` and nothing else. It leaves out the files an open PR or an in-flight claim holds: `data-engine.zod.ts`, `data-engine.test.ts`, `hook.form.ts`, `analytics*.ts`, `cube-member-inner-name-retirement.test.ts`, `driver/turso.zod.ts` and `filter-subtree-provenance.ts`, as the claim names them. It also leaves out four files that open PRs started editing after the claim: `driver/turso.test.ts` (PR objectstack-ai#20504, objectstack-ai#20437's, opened 2026-09-28T20:08Z), `object.form.ts` (PR objectstack-ai#20519, objectstack-ai#20432's, 21:55Z), `object.zod.ts` (PR objectstack-ai#20521, objectstack-ai#20494's, 22:10Z) and `filter-logic-conformance.ts` (PR objectstack-ai#20523, objectstack-ai#20444's, 22:39Z). See Acceptance notes. Later stages cover the other areas, so this PR says `Part of`. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123). That is **163 sites on 161 lines in 44 files, covering 40 numbers**. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and it says in its own words what that commit decided. No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of the 43 dead numbers in scope. ADR-0104 names objectstack-ai#12380 only as a reference, and ADR-0055 states the rule that objectstack-ai#8772's ruling enforced, not the ruling itself. So every anchor is a commit: **38 distinct shas**. One number was dropped rather than anchored: objectstack-ai#17286, a tracking card that recorded an axis as undecided, under which no commit landed. The sentence keeps its reason in words. Three comment sites in scope are left on purpose (see Acceptance notes). Two are the `[objectstack-ai#6259]` marker in `api-derivation.ts:163`, which a test string reads, and the test comment that names that marker. The third is `field.zod.ts:370`, whose `objectstack-ai#6111` is objectui's number. Only comments changed. Every source file keeps its line count (174 lines out, 174 in, over 45 files), so no line citation into these files moves. Thirteen of those 174 lines held no dead citation. Eleven are the other half of a sentence that had to be reflowed or rewritten. One is a table header (`value-roundtrip-conformance.ts:20`, 「card」 to 「card or commit」, because its row now holds a commit). One is `api-derivation.ts:164`, which now carries the `[objectstack-ai#6259]` sentence's commit. No code token moves (see the guard below). The 41 string-literal sites that carry a dead number are tokens, so they are left as they were and listed below. **No citation number is added.** Every tracker number on an added line was already on the line it replaces. No PR number stands on an added line. Two more kinds of file change, both mechanical: - **One regenerated reference page.** Two of the rewritten docblock lines (`feed.zod.ts:15`, `:18`) project into `content/docs/references/data/feed.mdx`. `check:docs` proved that page stale, and `pnpm --filter @objectstack/spec check:generated --fix` regenerated only it. The diff is two lines, each the same substitution as its source line. No page a held file projects into (`analytics.mdx`, `data-engine.mdx`, `hook.mdx`, `driver-turso.mdx`) moved. - **A `patch` changeset** for `@objectstack/spec` (see Changeset below). ## Census: `data/`, before and after **Instrument.** This is the instrument of stages 1 and 2. It sends REST `GET /repos/objectstack-ai/objectstack/issues/N` without following redirects, for every distinct number cited in `packages/spec/src/data`. The population is: - the citation gate's own exported `CITATION_RE` and `NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`, `objectstack-ai/objectstack`, `framework`, `pre-` or `post-`; - widened here to the capitalised spellings of those qualifiers (`Pre-`, `POST-`, `Framework`: 7 sites, one of them dead), which stage 2's case-sensitive set did not read; - N of 100 or more, excluding `summon` heads. Each site is classified by the TypeScript parser as a line comment, a docblock, a block comment or a string. **Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at the start, after every 100 numbers and at the end. They read 24 of 24 lit (200) and 24 of 24 dead (404) over 8 checkpoints in both runs. | reading | tree | numbers probed | 200 | 404 | 301 or other | dead sites, all of `data/` | in scope | excluded (held files) | in-scope lines | in-scope files | dead numbers in scope | |---|---|---|---|---|---|---|---|---|---|---|---| | before | base `9bf5e67af`, probed 2026-09-28T19:32Z to 19:36Z | 618 | 571 | 47 | 0 | **240** | 207 | 33 | 204 | 47 | 43 | | after | head `96fd49caa2`, probed 2026-09-28T23:19Z to 23:23Z | 600 | 571 | 29 | 0 | **77** | 44 | 33 | 43 | 16 | 21 | **Before, in scope, by class.** 92 non-test docblock sites and 13 non-test line comments. 16 test docblock sites and 45 test line comments. 39 test string sites. 2 non-test string sites. **After, in scope.** 41 string sites and 3 comment sites remain, all three deliberate. The head probe found no number newly dead since the base probe: the same 571 numbers answer 200. PR objectstack-ai#20226's area table read `data` 239 at an earlier base; this census reads 240 at `9bf5e67af`. The 33 excluded sites sit in `object.zod.ts` (15), `analytics.zod.ts` (3), `analytics-strictness-batchd.test.ts` (2), `analytics-date-range-two-bound-window.test.ts` (1), `driver/turso.zod.ts` (2), `driver/turso.test.ts` (3), `filter-subtree-provenance.ts` (3), `filter-logic-conformance.ts` (3) and `object.form.ts` (1). `data-engine.*` and `hook.form.ts` carry none. ## Per-number table The counts are in-scope sites and files at the base. `rewritten / left` gives comment sites rewritten and sites left. Every anchor was read in its diff or message, not only in its subject: it is the commit that made the change the line now describes, and its own diff or message names the number it replaces. | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#6111` (objectui) | 1/1 | 0/1 | objectui's number, left: see Acceptance notes | | `objectstack-ai#6259` | 5/2 | 1/4 | `6968885ef`: retires the producer-less `batch: 'bulk'` row of `DATA_ACTION_TO_API_OPERATION` and the prose calling `batch` a runtime action. The marker and 2 test strings stay (see Acceptance notes) | | `objectstack-ai#6345` | 18/5 | 17/1 | `e2798fab7`: one driver vocabulary; both boot hosts read the shared table; `mongo` to `mongodb`; turso a builtin; the fork-1 and fork-2 refusals | | `objectstack-ai#6571` | 10/2 | 8/2 | `2f3e79351`: `$between` endpoints accept the ISO/clock strings the platform produces, as a bare string (rider ①) | | `objectstack-ai#8495` | 9/2 | 6/3 | `4bfe1a539`: refuses `${…}` placeholders in memory `persistence.path` / `persistence.key` at publish | | `objectstack-ai#8656` | 1/1 | 0/1 | a test title only | | `objectstack-ai#8696` | 20/8 | 17/3 | `90a12fb18`, the card's mongodb arm: a bound secret rides beside an unmodified url as MongoClient `auth`. Its own pins carry the multi-host form `new URL()` cannot parse and the bound secret outranking `options.auth` | | `objectstack-ai#8772` | 3/2 | 3/0 | `75b7c240a`: Direction 2 of the 2026-08-16 maintainer ruling. The builder forces `required: true` on a `master_detail` under `controlled_by_parent`, and raw parse stays tolerant. ADR-0055 stays cited beside it | | `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2`: stamp-only `tenancy.organizationField`, declared by `sys_api_key` | | `objectstack-ai#8794` | 2/1 | 2/0 | `1850ebbb0`: corrects the reuse-safety claim on the filter-subtree mark from the survey's measurement, and routes a mechanism change to a spec-seat ruling (stage 1's anchor too) | | `objectstack-ai#8836` | 2/1 | 2/0 | `1850ebbb0`: the same commit, which pins the invariant (one line carries both numbers) | | `objectstack-ai#8873` | 6/3 | 6/0 | `096106522`: a bound `credentialsRef` reaches the postgres server on the DSN branch. Its diff records that `pg` sends a password only when the server asks | | `objectstack-ai#8874` | 1/1 | 1/0 | `d70428ae7`: a declared mysql `ssl` reaches `mysql2` as its own TLS options object, because `mysql2` rejects a bare boolean | | `objectstack-ai#8876` | 9/5 | 6/3 | `d634e665b`: exports `urlUserinfoUsername`, and its diff states the asymmetry that a username is not credential material | | `objectstack-ai#9040` | 20/6 | 14/6 | `24206416a`: refuses a credential in the mongo options passthrough at publish, and redacts the passthrough secret paths on read | | `objectstack-ai#9041` | 22/2 | 17/5 | `d491625c1`: refuses a bound `credentialsRef` with a user-less mongo `config.url`, with the triage's fences | | `objectstack-ai#10165` | 5/1 | 1/4 | `801296050`: `ttl.onlyWhen` with the canonical null predicate (maintainer ruling 2026-08-20, option A) | | `objectstack-ai#10274` | 1/1 | 1/0 | `d1ba685ec`: re-measures the objectui pin citations and gates the class | | `objectstack-ai#10329` | 6/2 | 6/0 | `15d58dbf1`: retires the import lookup transform's steering params (ADR-0049) | | `objectstack-ai#10347` | 2/1 | 2/0 | `530c1df65`: the Archiver honours a declared `ttl` (maintainer ruling 2026-08-20) | | `objectstack-ai#10527` | 2/1 | 1/1 | `5649efbf9`: refuses a diverging retention + ttl + archive triple at parse time | | `objectstack-ai#11065` | 7/3 | 5/2 | `20950404c`: a boolean aggregand counts as 1 or 0 in `avg` and `sum`, the first face aligned. No commit message names the card; this is where the number first entered the tree | | `objectstack-ai#11195` | 3/1 | 2/1 | `b37231883`: `UserActionsConfigSchema` adopts `group` / `hideFields` / `rowColor` | | `objectstack-ai#11215` | 1/1 | 1/0 | `42a117b88`: documents `NoSQLIndexSchema.unique`'s deliberate scope-vocabulary omission | | `objectstack-ai#11350` | 1/1 | 1/0 | `ece4dad31`: records the 2026-08-23 maintainer ruling on entry nameability (stage 1's anchor too) | | `objectstack-ai#11408` | 2/1 | 1/1 | `f11fc61c5`: declares `editMode` (maintainer ruling 2026-08-24) | | `objectstack-ai#11507` | 5/2 | 5/0 | `88b9d749a`: declares `sys_activity.type` an open, author-extensible vocabulary (maintainer ruling 2026-08-24, direction 4) | | `objectstack-ai#11658` | 1/1 | 1/0 | `1a6a19c31`: opens `RecordActivityProps.types` to author-contributed kinds | | `objectstack-ai#12380` | 4/2 | 4/0 | `4045b954d`: makes the SQLite `Field.json` codec injective; its message carries the measured boundary | | `objectstack-ai#12868` | 1/1 | 0/1 | a test title only. Its comment site sits in `object.form.ts`, now held by PR objectstack-ai#20519; its deciding commit is `c459da6bc` (see Acceptance notes) | | `objectstack-ai#13156` | 1/1 | 1/0 | `fd289be45`: strips tracker ids from function-declaration-built refusal prose (the card's A half) | | `objectstack-ai#13644` | 3/2 | 2/1 | `34ce8e7db`: declares `ctx.referentialFieldClear` on `HookContextSchema` | | `objectstack-ai#14426` | 2/2 | 1/1 | `40a44b91b`: the undefined-comparand refusal prescribes the null predicate by its ruled spellings, position-safe | | `objectstack-ai#14676` | 1/1 | 1/0 | `13c48c2a5`: retires `connector.errorMapping`; its test states the same assertion-set reasoning | | `objectstack-ai#16126` | 2/2 | 2/0 | `859ded3ec`: refuses a whitespace-only `reference` on lookup / master_detail | | `objectstack-ai#16685` | 4/2 | 4/0 | `ed7243d52`: accepts boolean / toggle for sum / avg / min / max (decision batch objectstack-ai#80) | | `objectstack-ai#16867` | 3/2 | 2/1 | `0ee32edef`: `notNull` / `not_null` prescribe `storage.notNull`, not `required` | | `objectstack-ai#17014` | 3/2 | 2/1 | `80aef8032`: the one-day date-range presets prescribe a one-day window, and the table states its end-token convention | | `objectstack-ai#17286` | 1/1 | 1/0 | dropped: a tracking card with no landing. The sentence now says the card is gone and to measure `driver-memory` for the open set | | `objectstack-ai#17348` | 1/1 | 1/0 | `51efbf116`: pins the `driver-memory` temporal text-operator divergence by name in that driver's conformance suite | | `objectstack-ai#17590` | 1/1 | 1/0 | `e04a0aff2`: `$contains` on a JSON column is a per-dialect membership test (director-seat ruling 2026-09-12) | | `objectstack-ai#18012` | 8/3 | 7/1 | `176b03582`: `$between` requires two non-blank endpoints (decision batch objectstack-ai#146 item 5, letter A) | | `objectstack-ai#19377` | 6/2 | 6/0 | `a60c913de`: refuses a `{ $field }` reference as a `$between` endpoint at the runtime filter door | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0). That is 38 distinct shas. Wordings to check, each true of its commit: - `datasource.zod.ts:352` names only the card's mongo arm (`90a12fb18`) for "the defect class … closed", because the paragraph is about mongo. The card's mysql arm (`72050cc47`) is not cited anywhere in this stage. - `datasource.zod.ts:354`: 「the triage's, as commit d491625 landed them」. `d491625c1`'s message lists the fences as "per triage". - `filter.zod.ts:1021-1025`: the `objectstack-ai#17286` pointer becomes 「was measured on a tracking card … That card is gone: measure `driver-memory` for the open set, ⛔ not this text.」 The warning that this paragraph is not the authority is kept. ## The 41 string sites left as tokens - **Test titles and test-code strings (39 sites).** `driver/driver-credential-refusal.test.ts` 14, `object.test.ts` 6, `datasource-credential-redaction.test.ts` 3, `driver/driver-placeholder-refusal.test.ts` 3, `filter.test.ts` 3, `api-derivation.test.ts` 2 (the `split('[objectstack-ai#6259]')` literal and its message), `field.test.ts` 2, and 1 each in `date-range-presets.test.ts`, `driver/postgres.test.ts`, `field-rows-option-description.test.ts`, `filter-comparand-type.test.ts`, `hook.test.ts` and `object-strictness-batch20.test.ts`. - **Non-test strings (2 sites).** `aggregation-conformance.ts:398` and `:407`, the `note` of two exported `AGGREGATION_CASES` rows (`objectstack-ai#11065`, `objectstack-ai#11151`). They ship as data. Their only readers are driver conformance suites, which print a `note` as the assertion message when a case fails, to a driver developer and never to a metadata author. So they are neither comments nor form D author-shown text. This is the same disposition stage 1 gave the two `why` strings and stage 2 the `PROVENANCE_WAIVERS` reason. No author-shown text in `data/` carries a dead number, so nothing here is objectstack-ai#20233's form D. ## Mechanical guard: no code token moves The check compares leaf tokens with comments stripped, base `9bf5e67af` against head `96fd49caa2`. It uses the TypeScript parser's leaf tokens, so template literals are scanned in context, and it excludes JSDoc nodes. It ran over all 45 touched `.ts` files. - Real run: 140,379 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control: 0 files changed, as expected (exit 0). - Positive control (a declaration inserted into `feed.zod.ts`): 1 file reads DIFFER (exit 1). - Positive control (one digit changed inside the `split('[objectstack-ai#6259]')` string in `api-derivation.test.ts`): 1 file reads DIFFER (exit 1). ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/spec` is included. It says only that the provenance comments were re-anchored. Measured on the built package: 14 of the touched sources are `src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten docblocks also reach `dist`. `88b9d749a`, `e2798fab7` and `24206416a` each appear in 1 declaration file. `24206416a` appears in 20 bundled `.js` files and `2f3e79351` in 28. The positive control, a pre-existing `feed.zod.ts` docblock sentence, appears in `dist/data/index.d.ts`. ## Gates (head `96fd49caa2`) - **Citation judging pass, run as CI runs it:** `pnpm check:issue-citations && node scripts/check-issue-citations.mjs` exits 0. The self-test passes 73 cases in 7 batteries. The live run judged 11 citations across 25 files, and all 11 resolve. - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at the final head derived 108 families, and all 108 exit 0. `--ran` reports 108 run, 0 NOT MEASURED, 0 unrun, and exits 0. (`check:i18n` was derived at the earlier heads from `object.form.ts`, and left the set when that file went back to base.) - At an earlier head, four gates first exited 3 (PREREQUISITE NOT MET) because the workspace was unbuilt: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples` and `check:docs-transcript-drift`. At the final head a full `turbo run build` of `./packages/*` ran first (71 tasks, exit 0, under the shared verify lock), and every gate exited 0 on its first run. - `check:generated` was run under the lock against that build: all 15 artifacts are up to date. - **Build, tests, typecheck and lint:** - `pnpm --filter @objectstack/spec build` exits 0. - `vitest run --maxWorkers=2 src/data` in `packages/spec` at the final head: 107 files and 3,517 tests pass (1 todo), covering every touched test file. - The 12 spec suites outside `src/data` that read `data/` source text pass at the final head: 12 files, 503 tests. These are `scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`, `src/api/api-entry-graph.pin.test.ts`, `src/contracts/scoped-context.test.ts`, `src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`, `src/system/constants/platform-object-names.test.ts`, `src/type-alias-convention.pin.test.ts` and `src/ui/dashboard.test.ts`. - `pnpm --filter @objectstack/spec typecheck` at the final head exits 0, including `check:test-typecheck` (53 files, 251 errors, 138 pinned signatures held). - Lint, as a proven narrowing at the final head: `eslint --no-inline-config --format json` over the 45 touched `.ts` files gives 45 files, 0 errors and 0 warnings. All 45 are in eslint's own population (`isPathIgnored` is false for each). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, which its own line 328 states), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. ## Acceptance notes - **The `[objectstack-ai#6259]` marker.** `api-derivation.test.ts:236` splits `DATA_ACTION_TO_API_OPERATION`'s TSDoc on the literal `[objectstack-ai#6259]`, and a test string may not change here. So the marker line `api-derivation.ts:163` is byte-identical to the base, and the test comment at `:232` that names the marker stays too. The sentence's deciding commit sits on the next line instead: 「(both by commit 6968885)」. A first attempt wrote the commit onto the marker line itself. The diff-scoped `check-issue-citations` then read the kept `objectstack-ai#6259` as an added citation and exited 1, so it was moved one line down (commit `b93f08f8d0`). - **objectui's `objectstack-ai#6111`.** `field.zod.ts:370` reads 「objectui#6110 + objectstack-ai#6111 (section)」. The qualifier covers only the first number, so the citation grammar reads `objectstack-ai#6111` as this repository's (404 here). It is objectui's number: its introducing commit `f887e5249` writes `(objectui#6111)` in the same diff, and `objectstack-ai/objectui` answers REST 200 for objectstack-ai#6111 to this session (and for objectstack-ai#6110 and objectstack-ai#10264). objectui has no `refs/pull/6111/head`, so it is an issue there, not a PR. The line is left unchanged. This is objectstack-ai#20330's grammar family, the same as stage 2's `objectui PR objectstack-ai#10264`, and it is noted there, not filed. - **Capitalised qualifiers.** `CITATION_RE` classes `Pre-#N`, `POST-#N` and `Framework#N` (7 sites in `data/`) as cross-repo and never judges them. This census read them as this repository's. One was dead and is rewritten here (`object.test.ts:223`, `POST-objectstack-ai#10347`). This is the same objectstack-ai#20330 family as stage 1's `pre-` / `post-` finding. - **Four files held after the claim.** Each joined the exclusions and went back to the base bytes (hypothesis 2 of the dispatch). Each PR's hunks were disjoint from this PR's lines, but the dispatch's rule is file-level. - `driver/turso.test.ts`: PR objectstack-ai#20504 (objectstack-ai#20437's) opened at 2026-09-28T20:08Z and edits it. Its two comment sites (`:4`, `:58`, both `objectstack-ai#6345`) went back to blob `7fe99ebf9` in commit `86463ed0a1`. A no-driver `merge-tree` of that head with PR objectstack-ai#20504's head `5dfa45e9f` exits 0. - `object.form.ts`: PR objectstack-ai#20519 (objectstack-ai#20432's) opened at 21:55Z and edits it. Its one comment site (`:256`, `objectstack-ai#12868`, whose deciding commit is `c459da6bc`) went back to blob `60713e06f` in commit `3479600dda`. - `object.zod.ts`: PR objectstack-ai#20521 (objectstack-ai#20494's) opened at 22:10Z and edits one line at `:2123`. Its 15 comment sites (`objectstack-ai#8772`, `objectstack-ai#10165`, `objectstack-ai#10347`, `objectstack-ai#10527`, `objectstack-ai#11195`, `objectstack-ai#11408`, `objectstack-ai#13608`) went back to blob `befde04ca` in commit `96fd49caa2`. Their deciding commits are `75b7c240a`, `801296050`, `530c1df65`, `5649efbf9`, `b37231883`, `f11fc61c5` and `fc9ba76a5`, all read for this stage. - `filter-logic-conformance.ts`: PR objectstack-ai#20523 (objectstack-ai#20444's) opened at 22:39Z. Its 3 comment sites (`objectstack-ai#13195`) went back to blob `c9b32acba` in the same commit. Their deciding commit is `9dac1ae01`, with `PR objectstack-ai#13529` as the link. - **What stays for later stages.** - The 33 dead sites in the held files listed above. The later stage can reuse the deciding commits named for them here. - The 41 string sites and the 3 deliberate comment sites above. - The `data/` numbers that also appear in `packages/spec/src/migrations/**`. Those are objectstack-ai#20233's form D, or the migrations stage. - **The rung.** Several anchored changes also have ADR-0087 entries in `packages/spec/src/migrations`. Examples are `cbp-master-detail-required-forced` for objectstack-ai#8772, `filter-between-blank-endpoint-refused` for objectstack-ai#18012, the `datasource-*` entries for objectstack-ai#9040, objectstack-ai#9041 and objectstack-ai#8873, and the `mapping-lookup-params-removed` conversion for objectstack-ai#10329. This PR takes the commit rung, as stages 1 and 2 did, so it is precedent-consistent. The D3 id is the more durable in-repo record, if the ruling's first rung is later read to include those entries. - **The citation gate's reach.** It defers `packages/**/*.test.ts`, so 20 of the 45 touched `.ts` files never enter its judging population. The added-minus-removed count over the whole diff covers them: 0 numbers added. - **Base.** The branch is 22 commits behind `origin/main` (`1378ec7c0c`, read at 2026-09-29T00:18Z). Four of those commits touch `data/`, all in excluded files: objectstack-ai#20475's `hook.form.ts`, objectstack-ai#20487's `data-engine.*`, and, since this stage excluded them, PR objectstack-ai#20521's `object.zod.ts` (`9e1689f8e2`) and objectstack-ai#20444's `filter-logic-conformance.ts` (`fb386074f5`). None touches a file in this diff, and a no-driver `merge-tree` of the head onto `1378ec7c0c` exits 0. So there was no merge. The open-PR file lists were re-read at 00:18Z: 11 open PRs, none touching a file in this diff. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20439
Clause-②: no
What
packages/spec/src/data/hook.form.ts'sconditionrow declaredlanguage: 'javascript'overHookSchema.condition, which isEvaluatedExpressionInputSchema— a CEL predicate, not a script (hook.zod.ts:'Predicate (CEL); hook runs only when TRUE …'). Every sibling predicate row (field.form.ts/object.form.ts'svisibleWhen/readonlyWhen/requiredWhen, and the formulaexpressionrow) already declareslanguage: 'expression'. A consumer keyed on the row's declared language (objectui#10963'sCodeWidgetfix) could not tell this row apart from a real script row (body.source,action.source, both genuinely'javascript').conditionrow now declareslanguage: 'expression',helpText: 'CEL predicate — the hook runs only when TRUE'(triage's wording, matching the sibling rows' phrasing).packages/spec/src/system/metadata-form-declared-rows.pin.test.ts(no existing row-language assertion was found, so the pin was added beside that file's existing form-row pins): assertshook.conditiondeclarestype: 'code'/language: 'expression', with a control againstfield.visibleWhen(a sibling predicate row already correct) so the assertion is shown to actually discriminate.en.metadata-forms.generated.tsregenerated (node scripts/check-i18n-bundles.mjs --write) to pick up the new helpText.zh-CN/ja-JP/es-ESkept their existing translated values under the tool's merge behaviour (now stale relative to the new English source — the tool does not auto-translate).@objectstack/specpatch.Why this landing site
Matches the constraint's expected surface exactly:
hook.form.ts, the pin, the regenerated translation bundles, and the changeset. No other producer or consumer needed a change.Tests
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/metadata-form-declared-rows.pin.test.ts src/system/metadata-form-zod-reconciliation.test.ts src/data/hook.test.ts src/data/hook-body.test.ts— 173 passed.pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2 src/apps/translations— 399 passed (21 files), includingmetadata-forms-vocabulary.test.tsandhook-execution-panel-echo-decisions.test.ts.pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated— all 15 generated artifacts up to date.pnpm check:i18n— all 9 packages in sync, no undeclared authoring keys.pnpm --filter @objectstack/spec typecheck— clean.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands→ 85 derived families, all run and reconciled (--ran): 84 green, 1 honestly NOT-MEASURED —pnpm check:dual-build-cjs-loadsexits its ownPREREQUISITE NOT MET(3) because this worktree never ran a fullpnpm buildacross every package in the monorepo (studio, client-react, several connectors/plugins/services have nodist/); that whole-workspace build is disproportionate to this one-row change and is CI's own "Build Core" job to run. Nothing about this diff is implicated in that gate.Acceptance notes
None — no out-of-scope findings surfaced while making this change.
Generated by Claude Code