Skip to content

fix(spec): hook condition row declares expression, not javascript - #20475

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20439-hook-condition-expression-row
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20439-hook-condition-expression-row

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20439

Clause-②: no

What

packages/spec/src/data/hook.form.ts's condition row declared language: 'javascript' over HookSchema.condition, which is EvaluatedExpressionInputSchema — 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's visibleWhen / readonlyWhen / requiredWhen, and the formula expression row) already declares language: 'expression'. A consumer keyed on the row's declared language (objectui#10963's CodeWidget fix) could not tell this row apart from a real script row (body.source, action.source, both genuinely 'javascript').

  • condition row now declares language: 'expression', helpText: 'CEL predicate — the hook runs only when TRUE' (triage's wording, matching the sibling rows' phrasing).
  • Pinned in 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): asserts hook.condition declares type: 'code' / language: 'expression', with a control against field.visibleWhen (a sibling predicate row already correct) so the assertion is shown to actually discriminate.
  • en.metadata-forms.generated.ts regenerated (node scripts/check-i18n-bundles.mjs --write) to pick up the new helpText. zh-CN / ja-JP / es-ES kept their existing translated values under the tool's merge behaviour (now stale relative to the new English source — the tool does not auto-translate).
  • Changeset: @objectstack/spec patch.

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), including metadata-forms-vocabulary.test.ts and hook-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.
  • Gate derivation: 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-loads exits its own PREREQUISITE NOT MET (3) because this worktree never ran a full pnpm build across every package in the monorepo (studio, client-react, several connectors/plugins/services have no dist/); 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

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
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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): node scripts/docs-audit/affected-docs.mjs --json 9bf5e67affab69ce740037f33b003f5faf45d205 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ce6d3e9666a1153b68255452b5092a73c4a8e2d3
Local-runs: none

① Derived judgments

Inputs: card #20439 (body; triage grade 5871322775; claim 5872433320; os-dev-report 5874241898), PR #20475 (body; its one comment 5874198509, the docs-drift check; file list; net diff origin/main...refs/os-seat2/pr20475 — 4 files, +47/-2, three commits over merge-base acd009521e6e), and the 33 check-runs on the head as read at 2026-09-28T16:41Z.

  • Row change packages/spec/src/data/hook.form.ts:77 — the condition row: language: 'javascript' to 'expression', helpText to CEL predicate — the hook runs only when TRUE. RIGHT. HookSchema.condition is EvaluatedExpressionInputSchema (hook.zod.ts:293, described "Predicate (CEL); hook runs only when TRUE"), the same slot shape as the seven sibling predicate rows (field.form.ts:331-333, object.form.ts:302,347-349), every one declaring 'expression'. The two 'javascript' rows left (hook.form.ts:43 body.source, action.form.ts:60 source) edit a plain { language, source } body, not an envelope, so they are correctly untouched. The helpText is triage's wording verbatim.
  • Is 'expression' a value the form DSL declares? FormFieldSchema.language (packages/spec/src/ui/view.zod.ts:3273-3274) is z.string().optional() — a free string whose docstring lists 'javascript', 'sql', 'json', 'typescript', 'expression', 'cel' as examples. Declared by docstring and by seven live rows, not enforced by an enum: this diff moves no parse verdict and no accept-set. The changeset's "no key is added, removed, narrowed or widened, and no parse verdict changes" is accurate. RIGHT.
  • Public surface implied: the exported hookForm value in @objectstack/spec, and through packages/metadata-protocol/src/protocol.ts:201 (TYPE_TO_FORM = METADATA_FORM_REGISTRY, served on /meta/types) the form the Studio renders. A value change only — no DTS or type movement; Type Check · source gates (api-surface) is green on the head.
  • Does any consumer in THIS repo branch on a code row's language? NO. Swept every non-test .ts/.tsx under packages/, services/, apps/, examples/ at the head. The registry's readers outside spec are metadata-protocol/src/protocol.ts (verbatim pass-through), packages/lint/src/validate-predicate-path-refs.ts (reads rows' own visibleWhen predicates, never language), packages/cli/src/commands/lint.ts and packages/cli/src/utils/i18n-extract.ts / i18n-coverage.ts (extract labels and helpText, never language). The only language === 'expression' branches in the repo (packages/runtime/src/sandbox/quickjs-runner.ts:129, script-runner.ts:520) read a hook BODY's language — a different field. So the behavioural consumer is objectui's CodeWidget alone; in this repo the change is served data plus the en bundle. RIGHT that no other producer or consumer needed a change.
  • packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts:770 regenerated to the new helpText — RIGHT; Lint & Repo Gates (the job that runs pnpm check:i18n) concluded success on this head. No copy of the old text survives anywhere in the head tree except the changeset's FROM/TO prose (grepped).
  • Pin packages/spec/src/system/metadata-form-declared-rows.pin.test.ts asserts hook.condition is exactly one top-level row with type: 'code' and language: 'expression'. condition IS a section top-level row (Execution section), so rowFor reaches it. The dev's "no existing row-language assertion" holds: the card's pointer hook-body.test.ts:15,22,132 asserts HookBodySchema / ExpressionBodySchema parsing, not a form row, and field.test.ts:1584 is a field schema — so a new pin beside the finding(spec): field.relatedListFilter and object.validations are DECLARED by the served schema and omitted by METADATA_FORM_REGISTRY's forms — the generic metadata form never renders them, so an author's only door is the Source tab #19085 form-row pins is the right landing. The CONTROL is a second lit reading (a sibling 'expression' row), not a dark one; its comment's claim that it catches a helper that stopped reading language is slightly off (that failure mode reds the main assertion itself), but the report's reverse-verification (revert to 'javascript', re-run: exactly the new assertion RED, the other 7 green) supplies the discrimination evidence. Acceptable as is.
  • Docs: content/docs/references/data/hook.mdx:39, content/docs/automation/hooks.mdx:107 and content/docs/automation/hook-bodies.mdx:74 already say the condition is CEL, so no page is falsified; the Docs Drift Check had no opinion (generic names). RIGHT that no docs edit was owed.
  • Scope: the four touched files are exactly the claim's file surface (hook.form.ts, the pin, the regenerated bundle, .changeset/20439-*.md); no breach; no governed path (Governed Surface Queue Guard green); examples/app-showcase/src/data/hooks/index.ts's two authored conditions are untouched and unaffected (the form is an editing surface, not a parser).

② Semver level

.changeset/20439-hook-condition-expression-row.md declares @objectstack/spec: patch. RIGHT — a bug fix in a released package (17.4.0): a declared-row value change with no accept-set movement is patch, never skip-changeset. @objectstack/platform-objects (also 17.4.0, public) carries the regenerated en bundle; both packages sit in the single fixed group of .changeset/config.json, so the spec changeset versions it in lockstep and no second changeset is owed. Clause-②: no, no arm, in both the PR body and the changeset body — RIGHT: language is a free z.string(), nothing widens or narrows. Check Changeset green.

③ Boundary flags

  • Dev flag (PR body and report): zh-CN / ja-JP / es-ES metadata-forms bundles kept their old translated helpText, "now stale relative to the new English source". ANSWERED — that IS the bundle tool's documented behaviour, and no gate or test treats it as a defect. packages/cli/src/commands/i18n/extract.ts:294-303: "Merge (the default) never overwrites an existing non-default-locale entry … a present-but-stale string is not a gap, only a missing one is. This is deliberate, not an oversight"; the generated bundle header says the same (packages/cli/src/utils/i18n-extract.ts:2244); packages/cli/test/i18n-extract.test.ts:414 (check-i18n-bundles merge mode never updates an EXISTING field description, so editing one leaves the en bundle silently stale (and the gate green) #8543) pins that translated locales keep merge while en tracks the source. The --source-hashes companion this package opts into (packages/platform-objects/scripts/i18n-extract.config.ts) records only leaves that are copies of the source, carries zero metadataForms.* entries in all three locales, and its header says a path with no entry is legacy-trusted and never reported stale; source-hash.test.ts records the ruling: staleness is a SERVING rule, not a gate (Option C — fail the build until every locale is re-translated — was rejected). The three old strings still describe the slot correctly (skip when false is the same rule as run only when TRUE), so no author is misled; re-translation is a translator's in-place edit, not this card's.
  • Dev flag: check:dual-build-cjs-loads NOT MEASURED locally (prerequisite: a whole-workspace build). ANSWERED by the head: Build Core and Type Check · consumer gates concluded success.
  • Test Core (3/6) concluded failure — NOT this diff's. Job 109025393814 log, read: the @objectstack/spec#build DTS step died ERR_WORKER_OUT_OF_MEMORY (JS heap) before any test ran — check-test-completeness: 0 of 13 scheduled package(s) reported, 13 never reached — and the shard's 13 packages include neither @objectstack/spec nor @objectstack/platform-objects. The identical spec#build task succeeded on this same head in Build Core, Test Core (2/6), Test Core (4/6) and all three Dogfood Regression Gate shards, and the diff changes one string literal, one test file and one generated bundle — nothing that alters DTS emission. A runner-memory flake; the owning seat re-runs the shard before enqueue, it does not touch the verdict on the diff.
  • Check-runs NOT yet concluded at that reading (2026-09-28T16:41Z): Test Core (1/6), Test Core (5/6), Test Core (6/6), Type Check · workspace. This record does not presume them green; the seat reads them before landing.
  • open_questions: [] — nothing to answer. out_of_scope_findings: [] — I looked for one: hook.form.ts:43 body.source stays 'javascript' even when body.language is 'expression' (an L1 body is JS-highlighted). That row's wire is a plain string, so 'expression' there would route it through the envelope wrongly — not a defect, no card owed.
  • Nothing ESCALATED.

Implemented-by: claude/issue-20439-hook-condition-expression-row
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 20:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 48efe91 Sep 28, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20439-hook-condition-expression-row branch September 28, 2026 20:21
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…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>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…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>
This was referenced Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants