Repository navigation
Release v0.3.3 - #146
Merged
hotlong merged 3 commits intoJan 25, 2026
Merged
Release v0.3.3#146hotlong merged 3 commits into
hotlong merged 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
hotlong
marked this pull request as ready for review
January 25, 2026 05:56
Contributor
There was a problem hiding this comment.
Pull request overview
Release PR for v0.3.3, updating package versions/changelogs and publishing collateral (root changelog + release notes), with added Tenant reference artifacts.
Changes:
- Bumped all workspace package versions from 0.3.2 → 0.3.3 (and examples accordingly).
- Updated package-level CHANGELOGs and root
CHANGELOG.md/RELEASE_NOTES.mdfor the v0.3.3 release. - Added Tenant schema artifacts: JSON Schema + docs reference page.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/types/package.json | Bump @objectstack/types to 0.3.3 |
| packages/types/CHANGELOG.md | Add 0.3.3 changelog entry |
| packages/spec/package.json | Bump @objectstack/spec to 0.3.3 (and export formatting) |
| packages/spec/json-schema/hub/Tenant.json | Add Tenant JSON Schema artifact |
| packages/spec/CHANGELOG.md | Add 0.3.3 changelog entry |
| packages/runtime/package.json | Bump @objectstack/runtime to 0.3.3 |
| packages/runtime/CHANGELOG.md | Add 0.3.3 changelog entry (deps bumped) |
| packages/plugin-msw/package.json | Bump plugin-msw to 0.3.3 + peerDep runtime ^0.3.3 |
| packages/plugin-msw/CHANGELOG.md | Add 0.3.3 changelog entry (deps bumped) |
| packages/plugin-hono-server/package.json | Bump plugin-hono-server to 0.3.3 + peerDep runtime ^0.3.3 |
| packages/plugin-hono-server/CHANGELOG.md | Add 0.3.3 changelog entry (deps bumped) |
| packages/objectql/package.json | Bump @objectstack/objectql to 0.3.3 |
| packages/objectql/CHANGELOG.md | Add 0.3.3 changelog entry (deps bumped) |
| packages/driver-memory/package.json | Bump @objectstack/driver-memory to 0.3.3 |
| packages/driver-memory/CHANGELOG.md | Add 0.3.3 changelog entry (deps bumped) |
| packages/client/package.json | Bump @objectstack/client to 0.3.3 |
| packages/client/CHANGELOG.md | Add 0.3.3 changelog entry (deps bumped) |
| examples/todo/package.json | Bump example-todo to 1.0.7 |
| examples/todo/CHANGELOG.md | Add 1.0.7 changelog entry (deps bumped) |
| examples/plugin-bi/package.json | Bump plugin-bi example to 1.0.7 |
| examples/plugin-bi/CHANGELOG.md | Add 1.0.7 changelog entry (deps bumped) |
| examples/msw-demo/package.json | Bump msw-demo example to 0.1.2 |
| examples/msw-demo/CHANGELOG.md | Add 0.1.2 changelog entry (deps bumped) |
| examples/host/package.json | Bump host example to 0.1.7 |
| examples/host/CHANGELOG.md | Add 0.1.7 changelog entry (deps bumped) |
| examples/crm/package.json | Bump crm example to 1.0.7 |
| examples/crm/CHANGELOG.md | Add 1.0.7 changelog entry (deps bumped) |
| examples/ai-support/package.json | Bump ai-support example to 1.0.5 |
| examples/ai-support/CHANGELOG.md | Add 1.0.5 changelog entry (deps bumped) |
| examples/ai-sales/package.json | Bump ai-sales example to 1.0.5 |
| examples/ai-sales/CHANGELOG.md | Add 1.0.5 changelog entry (deps bumped) |
| examples/ai-codegen/package.json | Bump ai-codegen example to 1.0.5 |
| examples/ai-codegen/CHANGELOG.md | Add 1.0.5 changelog entry (deps bumped) |
| examples/ai-analyst/package.json | Bump ai-analyst example to 1.0.5 |
| examples/ai-analyst/CHANGELOG.md | Add 1.0.5 changelog entry (deps bumped) |
| content/docs/references/hub/tenant/Tenant.mdx | Add Tenant docs reference page |
| RELEASE_NOTES.md | Add v0.3.3 release notes section |
| CHANGELOG.md | Add 0.3.3 entry + update compare links |
Comment on lines
+8
to
+9
| | Property | Type | Required | Description | | ||
| | :--- | :--- | :--- | :--- | |
There was a problem hiding this comment.
The markdown table uses a double leading pipe (|| ...) on the header and alignment rows, which is inconsistent with other reference pages (e.g., content/docs/references/hub/space/HubSpace.mdx) and likely breaks table rendering. Use single leading pipes (| ...) like the other docs.
This was referenced Sep 17, 2026
This was referenced Sep 18, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…d an 'end' node (objectstack-ai#18688) Part of objectstack-ai#15646 Clause-②: yes The flow accept set shrinks for five node types inside region bodies — shapes the runtime never honoured. Ruling D clause 4 states it verbatim.⚠️ **The runtime half is NOT in this PR.** A region-contained node that *durably suspends* must fail the run with a named error — only the run can know that — and ruling D assigns it to a separate `domain:services` card. ⇒ this PR lands with `Part of`, ⛔ not `Fixes`; **objectstack-ai#15646 stays open until the runtime half lands.** --- ##⚠️ SEAT BANNER — ruling **D** superseded the route this body argues for Everything below the horizontal rule was written when the card was ruled **C**, and it argues for **route A** ("Recommendation: A, as implemented"). ⛔ **That is no longer what this PR does.** It is kept unedited as the record of how the decision was reached — ⛔ deleting it would erase the evidence the later ruling was made on. **What this PR does NOW**, per ruling D (batch objectstack-ai#153 item 1, comment `5724940095`, maintainer 「其他同意」): - Inside `loop` / `parallel` branch / `try_catch` (try **and** catch) bodies at any depth, `FlowSchema.superRefine` refuses **five** node types: `screen`, `wait`, `approval`, `approval_revise`, and `end`. - ⛔ **`map` and `subflow` are NOT refused by type.** They pause exactly when the child flow their `config.flowName` names pauses — a *different metadata record*, not in hand at parse. Refusing them by type would also refuse `loop { map(synchronous child) }`, which runs correctly today. - `packages/spec` keeps its published identifier `FLOW_PAUSE_CAPABLE_NODE_TYPES`; only its **contents** narrow. - **`packages/services` is untouched by this round** — measured, zero paths. The 5-tests-in-3-files cost the section below describes **does not occur**: the whole package runs **138 files / 1652 tests, all passing**, with a false-green control proving the test read the rebuilt artifact and not a stale `dist`.⚠️ **Corrected by the seat, and it is a DECLARED DEVIATION from ruling D's letter — ⛔ not a clean pass.** 「zero paths」 is true of **this round's commits** and ⛔ NOT of the PR's cumulative diff: `packages/services/service-automation/src/end-node-refused-outcome.test.ts` (+57/−35) is in the diff, from the earlier round's commit `87973cab8d1`. Ruling D clause 1 says 「`packages/services` untouched; **the 5 tests** and objectstack-ai#15616's suite stand」 — and objectstack-ai#15788's region-`end` case **was one of those 5**. ⇒ the ruling's premise 「B breaks nothing」 was **false for that one case**: clause 1 itself orders `end` refused at parse, and `registerFlow` parses, so the old run-time assertion is unreachable by construction. What was done: the fixture is **byte-identical**, case count **12 → 12**, and the assertion is **strengthened** (region path, message text, and that nothing registered) so it fails again the day the shape becomes declarable. ⛔ No test deleted, skipped or quarantined; ⛔ no engine source moved; objectstack-ai#15616's suite and the other three files are untouched and green. The at-tier review measured all of this and ruled it non-blocking — but it is a deviation and it is stated here rather than buried. **CI on `6de9d662f6df`: 32 success, 3 skipped, 0 failure, 0 pending.** ### ⛔ Two corrections the `domain:spec` seat owes on its own record 1. ⭐ **The seat ruled "keep the published name" on a premise that is FALSE.** It told the round that renaming `FLOW_PAUSE_CAPABLE_NODE_TYPES` removes a *published* export and therefore forces a major. Measured since: main's `packages/spec/api-surface/automation.json` greps **0** for that name (lit controls `FLOW_BUILTIN_NODE_TYPES` and `FLOW_STRUCTURAL_NODE_TYPES` = 1 each; dark control = 0), and the branch greps 1. ⇒ **the constant is introduced by this PR and is on no consumer's import path**; the gate's 「1 breaking (removed)」 was computed against the branch's own earlier snapshot. The *decision* stands and costs nothing — a second name would be cost without benefit — but ⛔ the record must not carry 「a removed published export」 as a fact about consumers. The round measured this and told the seat; the seat re-measured and confirms it. 2. ⛔ **THIS CORRECTION WAS ITSELF WRONG, and the seat withdraws it.** It claimed `--pair 18688` re-measured 「exit 0」 at this head. **That reading came from a STALE INSTRUMENT.** The shared checkout's `check-clause2-carriers.mjs` is blob `ccd5ad7c9a00` and contains **0** occurrences of rule **C8**; `origin/main`'s and this head's is blob `3a270ef2eb5f` and contains **18** (lit control `C1`: 50 vs 51, so the reader works). C8 landed on `main` at 01:41Z via objectstack-ai#18859 and the shared checkout never had it. ⇒ every `--pair` reading this seat took today was taken with a script that cannot see C8. Re-taken with `origin/main`'s script: **objectstack-ai#18688 exit 4 on C8** — this seat held **two live `Claim:` comments** on objectstack-ai#15646 (`5722016855`, `5728277407`), which the protocol forbids. Repaired as C8 prescribes: `Release:` (`5729634742`) then ONE fresh `Claim:` (`5729639847`). **`--pair 18688` now exits 0** — `claim.selected` 1, `claim.rejected` 2. The at-tier review caught this; the seat re-measured and confirms it. --- Route **C**, as ruled. Director seat, summon objectstack-ai#24, batch objectstack-ai#145 item 5 — objectstack-ai#15646 (comment) (maintainer 「同意,其他也同意」), with the batch objectstack-ai#146 scope addition — objectstack-ai#15646 (comment) (maintainer 「146 同意」), which attached objectstack-ai#3267's 禁 ruling and absorbed objectstack-ai#18112 into this card. One PR, one changeset, two refusals in one rule family. ## 🛑 Read this first — this PR is NOT ready to land, and the reason is a measured decision, not a bug `packages/spec` is green end to end. **5 tests in 3 `packages/services/service-automation` files now fail**, and every one of them fails for the same reason: the fixture can no longer be REGISTERED, because `AutomationEngine.registerFlow` parses through `FlowSchema.parse` (`engine.ts:3941`) and this rule refuses the shape.⚠️ **Corrected by the `domain:spec` seat after a classification round — the table below replaces one that named 5 tests in 3 files.** That earlier count was taken by running **three named files**; CI runs `pnpm --filter @objectstack/service-automation test`, the whole package, and a named-file subset cannot see this class of breakage. `os-dev.md:56` reserves this body to the PR-open write, so the round named the wording and the seat writes it. | File | Failing | Card | What it pins | | --- | --- | --- | --- | | `src/builtin/contained-failure-rollup.test.ts` | **7** | objectstack-ai#16314 | the contained-failure rollup fold over `loop { subflow }` —⚠️ **absent from the earlier table entirely**; it predates this branch's base (`git merge-base --is-ancestor` exit 0), so this is a measurement gap, ⛔ not drift | | `src/builtin/map-in-loop-iteration-state.test.ts` | 3 | objectstack-ai#15616 | `loop { body: [ map, probe ] }` over a **non-pausing** child: 5 iterations x 2 items ⇒ 10 child runs, `failed = 0` either way, a fresh result set per iteration | | `src/builtin/contained-failure-visibility.test.ts` | 1 | objectstack-ai#14456 | a parent run's row identity does not leak into a `subflow` child; the region shape is the **vehicle**, not the subject | | `src/end-node-refused-outcome.test.ts` | 0 (was 1) | objectstack-ai#15788 | ⭐ **fixed on this branch** — see below | **Measured with the package suite:** at `e10b395cee`, **12 failed / 1627 passed (1639)** across **4 files**. After the fix below, at `87973cab8d1`: **11 failed / 1628 passed**. ⭐ **One of the twelve was never blocked on the open question, and it is repaired here.** objectstack-ai#15788's region-`end` case sits in **both** candidate populations — this body defines route B as the unconditionally pausing types **plus `end`** — so no answer to the question below moves it. It is re-homed to the registration refusal: the fixture is unchanged byte for byte, and the case now asserts the ZodError's located path, its message and prescription, and that **nothing registered**. ⛔ Not a deletion — it fails again the day the shape becomes declarable. **The 11 are mutually exclusive with route A, and that is measured rather than argued.** Ablating `FLOW_PAUSE_CAPABLE_NODE_TYPES` to route B's definition turns **all 11 green with nothing else moving**; route A on the same four files is 11 red.⚠️ Method note that is load-bearing: `service-automation` resolves `@objectstack/spec` through **`dist`**, so the ablation was rebuilt and verified present in 18 built artifacts before anything was read — an unrebuilt ablation would have gone green and proved nothing. Restored afterwards, verified absent from all 216 artifacts, whole-tree porcelain empty. ⭐ **The 11 are NOT one cost.** 3 of them (objectstack-ai#15616) are free: under route A the shape becomes undeclarable, so the defect is unreachable and the regression suite converts to a refusal pin — mechanically, the same conversion performed above for objectstack-ai#15788; that file's second describe (a TOP-LEVEL pausing map) is untouched and green, so the durable-pause half keeps its coverage. The other 8 (objectstack-ai#16314, objectstack-ai#14456) are a genuine re-home onto a top-level delegating node, and `loop { subflow }` over five rows with one failing is the shape objectstack-ai#15617's ruling **named**, so any re-home must record that the measurement no longer runs on it. ⛔ **Not repaired here.** The dispatch fences `packages/services` ("the engine's runtime refusal stays exactly as it is") —⚠️ and note precisely what that fence claims: it is true of the **diff**, which touches no `packages/services` file. Read as a claim about **effect** it is false, because the parse refusal changes what those suites can register. The changeset carries the same correction, and two of these three are other cards' regression suites: deleting or re-homing objectstack-ai#15616's and objectstack-ai#15788's coverage is a decision, not a fixture edit. **Two of them are also evidence about the rule itself**, which is the open question below. ### The open question: does the narrowing take a shape that WORKS with it? The ruling's population is "a node that **can durably pause** (`map` / `subflow` **with a pausing child**, approval-class nodes)". Measured: `map` and `subflow` pause **exactly when the child flow they NAME pauses** — a different metadata record — so "with a pausing child" is **not decidable at parse**. Only two spellings are: - **A — judge the node TYPE** (what this PR implements). Closes this card's own reproduction, covers all three region kinds, and is the only reading under which C is the *complete* fix the ruling's own reasoning requires. **Cost, measured:** it also refuses `loop { map(synchronous child) }` — a shape that runs correctly today and was deliberately fixed 12 days ago by objectstack-ai#15616 / PR objectstack-ai#15648, whose regression suite is 3 of the 5 failures above. - **B — judge only the UNCONDITIONALLY pausing types** (`screen` / `wait` / `approval` / `approval_revise`) plus `end`. Refuses nothing that works today, and the 3 `map` failures disappear. **Cost:** this card's own reproduction — `loop { try_catch { map(pausing child) } }` — stays declarable and stays silently green, so the card is not closed. There is no third reading available to a parse. **Recommendation: A, as implemented** — objectstack-ai#3267 is ruled 禁 ("structured regions do not support durable pause"), and a shape whose legality lives in a record the author is not editing, revocable by editing that record, is not a contract. Under A the five tests are re-homed (a top-level `map`, a top-level `end`) or retired with a statement, in this PR or a follow-up, once the seat says the coverage may move. ## Step Zero — the ruling's precondition, answered before any code was written > **First step, before writing**: prove the nesting is statically decidable at parse/validate time. **Answer: YES for the nesting and for the node vocabulary this rule judges, with two boundaries that are declared rather than discovered.** What was measured, on this branch's base `7f7b8557df`: 1. **The nesting is decidable, and a refusing layer already exists.** `collectFlowGraphs` (`packages/spec/src/automation/control-flow.zod.ts`) yields the top-level graph plus every region body, depth first, with a `scope` label and a `path` that anchors a Zod issue where the author wrote the node. `FlowSchema`'s `superRefine` already walks exactly that and refuses on it — the objectstack-ai#16134 one-node-id-space rule. The PM seat's clue held: there is no *refusing* layer for this shape, but the walk and the refusal machinery are both live and in the same file. 2. **The pausing vocabulary is statically declared for the built-in set.** Derived by reading the shipped `defineActionDescriptor` literals, not by recall: `supportsPause: true` appears on `screen` / `wait` / `subflow` / `map` (`packages/services/service-automation/src/builtin/`) and `approval` / `approval_revise` (`packages/plugins/plugin-approvals/src/`) — six, the same six the ADR-0044 `resumeAuthority` default-flip migration entry names in its own prose. They are published here as `FLOW_PAUSE_CAPABLE_NODE_TYPES`. 3. **`end` is fully static** — `FLOW_STRUCTURAL_NODE_TYPES`, a node type the engine handles with no executor at all. **What is NOT decidable, and what this rule does about it.** Whether a given node *will* pause is not decidable at parse, in two different ways, and both are stated in the docblock, in the changeset and in the ADR-0087 entry: - **`map` / `subflow` pause exactly when the child flow they NAME pauses** (`map.config.flowName`, an opaque reference to another metadata record). So the rule judges the node **TYPE**, not the run. That is wider than the runs that actually broke — a region-nested `map` over a synchronous child parsed green before and is refused now — and it is deliberate: the old shape's legality lived in a record the author is not editing and could be revoked by editing that record. "Legal until somebody adds a `wait` to the child flow" is not a contract. - **A plugin-registered pausing type is invisible to a parse.** ADR-0018 left the node-type namespace open (`FlowNodeSchema.type` is a validated `string`), and a parse has no registry. Pinned as a boundary test so it moves deliberately. - **`MAX_REGION_DEPTH` (32).** The walk stops there.⚠️ Unlike objectstack-ai#16134's duplicate-id rule, there is **no second spec refusal behind the ceiling** for this rule — `analyzeRegion` says nothing about pausing nodes — so past depth 32 the engine's run-time refusal is the only one. Measured and pinned at nesting 32 (refused) / 33 (not judged), and stated in the changeset rather than left for an author to find. ## What changed `FlowSchema.superRefine` gains one walk over `collectFlowGraphs`, skipping the flow's own graph, that raises a `custom` issue anchored at `[...regionPath, 'nodes', i, 'type']` for: - **a pause-capable node** in a region body — the message names the node, the region scope (`loop 'sweep' body → try_catch 'guard' try`), why a region body cannot host it, and the fix; - **an `end` node** in a region body, whatever its `outcome` — an `end` there was a no-op, and a refusing one was converted into a region error at the same boundary (objectstack-ai#15788). The ruled prescription is the message: a region body cannot end the run; put the `end` on the top-level graph. `FLOW_PAUSE_CAPABLE_NODE_TYPES` is the new export (`api-surface` / `export-origins` regenerated). The two approval entries are the declared constants `APPROVAL_NODE_TYPE` / `APPROVAL_REVISE_NODE_TYPE`, so a rename cannot desynchronise them. ⛔ `packages/services` is untouched — this is authoring-time enforcement only. ⛔ No engine rollback seam (route A, no card filed, per the ruling). ⛔ No runtime detection in `map` (route B, refused). ⛔ objectstack-ai#15617's `failed` fold is not addressed. ## Tests New file `packages/spec/src/automation/flow-region-pause-and-end.test.ts` — every case fails without the rule: - both refusals × all three region kinds: `loop` body, `try_catch` try **and** catch, `parallel` branch. A rule covering `loop` only is route B wearing C's clothes; the `try_catch` catch arm and the `parallel` branch arm are the two route B could never see, and each has its own case. - all six pause-capable types, table-driven off the exported constant. - the card's own reproduction, `loop { try_catch { map } }`, refused with the chained region path. - **negative tests, the over-reach guard**: every pause-capable type and an `end` still parse on the **top-level graph**; every non-pausing type still parses inside a region; a node merely *named* `end` or `wait` in a region still parses (the rule judges `type`, not `id`). - both declared boundaries pinned: the plugin-contributed pausing type, and the depth-32/33 seam. - `defineFlow` and `formatZodError` renderings. Two existing cases pinned the behaviour this rule replaces and were **replaced rather than re-spelled**, each saying so in its own comment: `end-node-outcome.test.ts`'s region-nested `end` (its subject — an `end`-in-region whose *config* is judged one door later — no longer exists) and `flow.test.ts`'s BPMN `waitEventConfig` region case (now asserts the earlier refusal *and* keeps the region-contract half it actually exists to measure). The `requireTypeScopedConfig` docblock that asserted a nested block-less `wait` parses green was corrected in the same edit. ## Verification Measured on `e10b395cee`. Heavy runs go through `scripts/pm/os-verify-lock.sh`; every exit code below is read from the wrapper's own `VERDICT command-exit` line or captured into a variable **before** any pipe — never `$?` after one. | Command | Verdict | | --- | --- | | `pnpm --filter @objectstack/spec build` | `command-exit 0` | | `pnpm --filter @objectstack/spec test` (whole package) | `command-exit 0` — **486 files, 13895 tests, 0 failed** | | `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`) | `command-exit 0` | | `pnpm --filter @objectstack/spec check:generated` | `command-exit 0` — all 15 artifacts up to date | | `pnpm lint` (whole repo, `eslint . --no-inline-config`) | `exit 0` — run in full, so nothing here is a narrowing | | `dispatch-gates.mjs --ran` reconciliation | `exit 0` — **85 derived, 81 run, 4 NOT-MEASURED, 0 UNRUN** | | `@objectstack/service-automation` — the 3 files whose fixtures feed this rule | `exit 1` — **5 failed / 24 passed**, see the section at the top | **Reverse verification (one-shot, restored).** The rule's own early-exit was mutated (`graph.path.length === 0` → `>= 0`), and the mutation was proved on disk before anything was read from the run — anchor grep 1 → 0, marker grep 0 → 1, blob `044bbbba` → `56a1c350`: - **ablated** — `flow-region-pause-and-end.test.ts`: **20 failed / 7 passed**. The 20 are exactly the refusal assertions; the 7 that survive are the over-reach guards and the two boundary pins, which must stay green with the rule absent. That split is itself the reading: a rule that also broke the negative cases would be refusing too much. - **restored** — `git checkout HEAD --` the file, blob back to `044bbbba`, `git diff HEAD` clean, `git status` empty, same file **27/27 passed**. No `dist` preflight applies: the test imports `./flow.zod` by relative source path, so the subject never resolves through `packages/spec/dist`. A restore `trap` was armed for the whole window. **The four NOT-MEASURED gates** are `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` — each exited **3, PREREQUISITE NOT MET**, printing in its own words that nothing was measured. All four read built output across packages this diff does not touch and need a repo-wide build; CI's `Build Core` and `Lint & Repo Gates` are where they run. ⛔ Not green, not red — unrun. **Not measured, stated:** CI convergence on this PR (the report is filed at the end of local verification); the branch has not been merged forward since `7f7b8557df`, so `main`'s newer commits are tested by CI and the queue rather than here. **Review-gate reading, not an action.** `scripts/pm/check-clause2-carriers.mjs --pair 18688` exits **4** on two rows, both belonging to the claiming seat and ⛔ neither touched here: **C1** — card objectstack-ai#15646 carries `needs:contract-review` while this PR does not (the gate is a dual carrier); **C2** — no comment on the card's thread is a machine-legible claim comment (none has a first line beginning `Claim:` carrying the `Clause-②:` line), so the declaration limb has nothing to read. The declaration itself is at the top of this body and in the changeset. ## Acceptance notes Observations from this card's reading, recorded here and **not** filed — none is a reproducible defect, a contract violation, or an authoring trap: - The ruling's own parenthetical, "`map` / `subflow` **with a pausing child**", describes the defect population rather than a decidable rule population, and its "a shape the runtime never honoured" is exact for `end`, `screen`, `wait` and the approval pair but not for a `map` over a synchronous child, which runs today. The PR takes the capability reading — the only one that makes C the complete fix the ruling's own reasoning requires — and the changeset states the cost in the author's own terms. Noted so a reviewer reads the widening deliberately rather than discovering it. - The card body and the batch objectstack-ai#145 ruling both say objectstack-ai#15617 is open; it is **closed / completed**. Nothing here depends on it. - `engine.ts:9937` in the ruling reads `engine.ts:9970` on this tree — line numbers are clues, and this one was re-read rather than trusted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ai#18012) (objectstack-ai#19066) Fixes objectstack-ai#18012 Clause-②: yes Ruling executed: decision batch objectstack-ai#146 item 5, **letter A** — maintainer 「146 同意」 2026-09-17T13:16Z. Carrier: the changeset `.changeset/18012-between-blank-endpoint-refused.md` — `@objectstack/spec` **minor**, body carrying **BREAKING for authored metadata**, ADR-0087 disposition `registered filter-between-blank-endpoint-refused`. ## What changed `$between` now requires two endpoints that are **present and non-empty**. A blank bound at either side is refused at the authoring door, and the refusal **names the blank side** — `MIN` / `MAX` plus the index — because the only measured producer pads a half-typed pair, so both bounds are present and the author is the one person who cannot see which one is empty. ``` FROM FieldOperatorsSchema.safeParse({ $between: [1, ''] }) -> { success: true } TO FieldOperatorsSchema.safeParse({ $between: [1, ''] }) -> { success: false, issues: [{ code: 'custom', path: ['$between', 1], message: 'A blank value is not a valid $between endpoint at index 1 (the MAX bound). …' }] } ``` Three spellings, one rule, but only one of them changes what parses: | spelling | before | after | | --- | --- | --- | | `''` | parsed green | refused — **the only behavioural change**; `''` is a string and the endpoint union accepted it | | `undefined` | refused with zod's bare `Invalid input` | refused with the pointed sentence naming the side | | `null` | refused with the 2026-08-31 ruling's own message | **unchanged** — it prescribes the null predicate, a different remedy for a different intent | The refinement rides the endpoint factory `RangeOperatorSchema` (the documentation copy) and `FieldOperatorsSchema` (the enforced copy) already share, so the two cannot drift. The published endpoint description gained the rule in the same edit — declared = enforced — which is the whole of the regenerated `content/docs/references/data/filter.mdx` diff (5 rows, one per carrier). The empty-string arm is an element-level `superRefine`, deliberately not the tuple-level refinement the factory's docblock rules out: a tuple check does not run once an element has failed, whereas the element check runs exactly when the union accepted the endpoint, which is precisely when there is an `''` to report. ## The ADR-0087 half the ruling left to measurement The ruling asked for a D2 conversion entry and explicitly did not pick the behaviour: 「the dev measures which the load path already does for a refused operator and follows that precedent」 (drop the operator, or refuse at load). **Measured, on `origin/main` before the change: the load path does neither.** `applyConversionsToStoredItem` — the one primitive every stored-row rehydration seam calls — never throws and never validates; it replays only the positively-recognised lossless transforms in the conversion registry. A stored view carrying `{ close_date: { $between: ['2026-01-01', ''] } }` comes back as the **same object reference**. No conversion in the registry drops a filter **operator** either: the three filter-adjacent entries are two key strips and a key rename. So the precedent to follow is the one the two nearest narrowings of this same surface already set — `filter-preset-ordering-comparand-refused` and `analytics-date-range-array-two-bounds-required`, both of which decline a D2 conversion because rewriting would be the platform guessing which bound was meant. **Registered as an ADR-0087 D3 semantic entry, with no D2 conversion and no stored-metadata rewrite.** Dropping the operator would be worse than guessing: it deletes a constraint the author wrote and silently **widens** the result set — the failure mode `$nin` carries in the same file. Consequence, stated rather than left to be discovered: the read path does not re-validate stored rows, so **no stored document becomes unreadable**. What changes is that re-saving one is refused, at the endpoint's own path, with the blank side named. ## `migrations/registry.ts` The ruling's Execution line sequenced this on `registry.ts` after objectstack-ai#18319 / objectstack-ai#18420. The dispatching seat measured that this no longer applies and said so on the card: the file's three tables are generated regions fed one-file-per-entry from `entries/`, and all four PRs said to hold it each add their own entry file. This PR did the same — **one new file under `entries/semantic/`, then `gen:migration-registry`**. Nothing was typed between the markers; the `registry.ts` diff is 86 lines of regenerated output and `check:migration-registry` proves the regeneration faithful. ## Verification Run on `e849c873cd` (the merge of `origin/main` into this branch), heavy runs serialized through the shared verify lock. - `pnpm --filter @objectstack/spec test` — **491 files / 14309 tests passed**. - `pnpm --filter @objectstack/spec typecheck` — clean (`tsc --noEmit` + scripts + test-layer ledger). - `pnpm --filter @objectstack/spec check:generated` — **all 16 generated artifacts up to date** after the merge. Exactly one was proved stale during the change (`content/docs/references/**`) and regenerated with `--fix`, which touched only it. - `pnpm lint` — repo-wide, exit 0. - Targeted gates, all exit 0: `check-adr-0087-registration --base origin/main`, `check-changeset-no-major --base origin/main`, `check-empty-changeset --base origin/main`, `check:nul-bytes`, `check:where-matcher`, `check:query-options-erasure`, `check:test-source-alias`, `check:spec-parsed-alias`, `check:cross-package-test-inputs`, `check:merge-driver`, `check:published-files`, `check:objectui-changeset`, `check:type-check-coverage`, `check:doc-anchors`, `check:docs-single-h1`, `check:docs-spec-enumerations`, `check:quick-reference-counts`, `check-doc-frontmatter`, `check-docs-section-name`, `check-closing-keyword-parity`. - `check:type-check-debt` — **NOT MEASURED**, exit 3 `PREREQUISITE NOT MET`: it needs the whole workspace dist closure built, which `lint.yml` does before the step and this run did not. Not a pass and not a finding. This diff adds no package and moves no ledger entry. ### Reverse verification — the new assertions are not vacuous Ablated through `scripts/ablation-replace.mjs`, which proves the mutation reached disk before the command runs (no `-i` family): ``` anchor "if (endpoint !== '') return;" x1 -> x0 blob d17f958 -> 4450ff1fac67 (the mutation landed) result 5 failed | 162 passed restore blob == HEAD d17f958, `git diff HEAD` empty ``` Predeclared direction: **red**, and exactly the five empty-string cases went red. The `undefined` case, the `null` case and all 162 pre-existing assertions stayed green — which is what separates "this rule is enforced" from "this file's tests pass". No build step is involved: the spec suite resolves `./filter.zod` from source, not from `dist`. ### Fixture sweep Every `$between` array literal in the tree was read for a blank or absent bound: **3 distinct sites, none of them parsing through this schema** — the driver-sql undefined-comparand refusal pin, the service-analytics filter-normalizer pin, and the `parseFilterAST` pin in `filter-comparand-shape.test.ts`. No fixture had to be rewritten. Instrument radius: tracked files this repo's `git grep` matches for `$between`, scanned for array literals; outside it lie the sibling `../objectui` checkout (a different repo, and its half is its own card) and any range built programmatically rather than written as a literal. ## Acceptance notes - **The runtime door is untouched, and it now disagrees with the schema door about `''`.** `parseFilterAST` still reads an empty string as a value — pinned on purpose in `filter-comparand-shape.test.ts` ("refuses ONLY null — falsy and empty-ish members are values, not absence"), and that file is outside this card's file surface and outside the ruling, which scoped the spec half to the schema refinement. Flagged, not filed: the two doors serve two different populations (an author saving a document vs a caller handing a where-clause to the engine) and aligning them is a decision of the same class as this card's, not a seat call. Carrier if it is ever wanted: the same file that carries the null and ordering runtime twins. - **Whitespace-only endpoints still parse.** `{ $between: [' ', 'M'] }` is green, and there is a positive assertion pinning that, so a later reader cannot widen the refusal without noticing they are doing it. The ruling enumerated `''`, `null`, `undefined`; narrowing a published face past what was ruled is the seat call this card's whole history refuses to make. Noted, not filed. - **`FilterConditionSchema` judges no comparand at all** — it is `z.record(z.string(), z.unknown())` at every field position, so it also lets the already-ruled `{ $field }` endpoint through. Standing shape, not a hole this narrowing opened; a test now pins it with that `{ $field }` control beside it so the green reads as a measurement rather than an oversight. Noted, not filed. - **The `Clause-②: yes` line is copied from the dispatch's claim comment, as the ruling set it.** For the record, this diff carries no widening tell: no key, enum member, union arm, export row or registry registration is added, and `check:api-surface` is green with no export delta. Read strictly against the clause's own question (「本卡放宽接受集或扩大公开面吗」) the direction is narrowing-only; the direction is carried in prose and by the changeset's `BREAKING for authored metadata` banner rather than by rewriting the ruling's word. - The objectui half — the builder stops padding a half-typed pair — is objectstack-ai/objectui#9695 and is untouched here. It is safe on its own and may land either side of this PR. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ectstack-ai#19090) Fixes objectstack-ai#18177 Clause-②: yes (narrowing) Executes decision batch objectstack-ai#146 item 4, **letter A** — maintainer 「146 同意」 2026-09-17T13:16Z. `BulkActionParamSchema` becomes strict like its twin and declares the key measured live on the surface; route B is not built, route C is not kept. **Changeset carrier:** `.changeset/18177-bulk-action-param-strict.md` — `@objectstack/spec` **minor**, body carrying 「Breaking for authored metadata」. **ADR-0087 disposition:** `registered ui-bulk-action-param-unknown-keys-refused` — a D3 structured TODO, not a D2 conversion, for the reason the majors-15/16/17 strictness entries give: an arbitrary unknown key has no mapping target. --- ## 1 — Measure first (the ruling's step 1) ### 1a. Which keys the bulk dialog reads off a bulk param after the spread **Instrument.** `bulkParamToField` destructures the eleven declared keys out and spreads the rest onto the field metadata handed to `getLazyFieldWidget`, so the question is: which keys does a widget read off that bag? Enumerated by scanning every form-widget module `getLazyFieldWidget` can return — `objectui@3e4f6324f7`, `packages/fields/src/widgets/**` (74 non-test modules) — for property reads off the `field` prop, following the local aliases those modules assign it (`const config = field as any`, and the chained `lookupField` → `fieldMeta` → `cascadeMeta` unwrap in `LookupField`). **Firing control (so a zero would be a reading).** The positive control is `dependsOn`: the scan returns it at 6 sites across 5 modules, and each was read by hand to confirm it is live code and not a comment. The negative probe (`zzz_nonsense_key_that_no_producer_emits_8755`) returns nothing. **Instrument's reachable radius, and a known target outside it.** The radius is `packages/fields/src/widgets/**` in objectui. It does **not** reach `packages/fields/src/index.tsx`, and a known target lives there: `buildValidationRules` reads `field.min` / `field.max` / `field.pattern` / `field.required_message` and more. That function is excluded deliberately, not by accident — it is the react-hook-form path the object FORM uses, and the bulk dialog does not go through it (`BulkActionDialog` renders the widget directly). Its keys are therefore not evidence about this surface. A first pass of the scan that did include `index.tsx` also over-reported `startsWith` (a string method, not a field key), which is why the alias-following pass was read by hand rather than trusted. **Result — `dependsOn` is live on BOTH widget families reachable from the dialog:** | family | reader | what it does with the key | | --- | --- | --- | | option widgets — `SelectField`, `MultiSelectField`, `RadioField`, `CheckboxesField` | `field?.dependsOn` | gates and re-resolves the offered set through `useCascadingOptions` | | reference-bearing pickers — `LookupField`, and `UserField` through it | `cascadeMeta?.dependsOn` | lowers it into a hard candidate filter and gates the trigger while a named parent is empty | **And a long tail of widget-config keys is read off the same bag** — `min` / `max` / `step` (NumberField, SliderField, CurrencyField, PercentField, RatingField), `accept` / `maxSize` / `crop` / `capture` (FileField, ImageField), `rows` (TextAreaField, RichTextField), `precision` / `scale`, `dimensions` (VectorField), `defaultName` (AvatarField), and the picker knobs `descriptionField` / `idField` / `allowCreate` / `lookupColumns` / `lookupPageSize` / `lookupFilters` / `picker` / `subtitle` / `avatarField` (LookupField). ⭐ **`format` is not among them**, although the module header used to name it beside min/max/step: no form widget reads it. That discrimination is what makes the list a measurement rather than a transcription of the header. ### 1b. Census of authored bulk params for keys the schema does not declare **Instrument.** Bracket-matches every `bulkActionDefs` array in a tree, extracts each `params[]` object literal and lists its top-level keys. **Firing control:** a planted fixture carrying `dependsOn` and the nonsense key — the instrument reports both and leaves the eleven declared keys unflagged. | tree | bulk-param literals | carrying an undeclared key | | --- | --- | --- | | `objectstack-ai/objectstack` @ `176b03582e` | 7 | **0** | | `objectstack-ai/objectui` @ `3e4f6324f7` | 3 | **0** | | `objectstack-ai/hotcrm` | **NOT MEASURED** | **NOT MEASURED** |⚠️ **The hotcrm leg is NOT MEASURED, and is not to be read as clean.** That repository is not reachable from this session, and ⛔ nothing here infers its contents from the excerpts quoted on the card. The ruling asked for a census across the four repos and hotcrm; what was reachable is the two rows above. The migration entry records the same boundary so an upgrader does not inherit the result. **Instrument's radius here too:** it finds params written as object literals inside a `bulkActionDefs` array. A param assembled in a variable and spread in would be outside it. No such site was seen, but that is an absence the instrument cannot certify. ⇒ **No authored unknown key was found, so no ADR-0087 *conversion* entry is owed and there is no stop-and-report.** The registered entry is the D3 structured TODO for the narrowing itself. --- ## 2 — What the change is `BulkActionParamSchema` moves from `z.object({…}).passthrough()` to `strictObject({…})`, and declares `dependsOn`. The rejection is curated rather than bare: - **aliases** — the known-divergence spellings now RENAME instead of riding through: `helpText` → `help`, `description` → `help`, `defaultValue` → `default`, `reference` → `object`, `referenceTo` → `object`, `displayField` → `labelField`, `title` → `label`. These are the same three mappings `toBulkParam` performs when it promotes an ACTION param, so the authored and promoted directions now agree. - **guidance** — `field`, `objectOverride`, `visible`, `visibleWhen`, `carryOver`, `defaultFromRow`, `requiresFeature`, each answered with the layer that really owns it. ⛔ None of them promises the field-backed route, because the bulk surface does not have one — that would be the confidently-wrong prescription this campaign has shipped before. - **guidanceSet `BULK_PARAM_WIDGET_CONFIG_KEYS`** — one prescription for the whole measured widget-config family, naming `FieldSchema` as the shape those keys are real on, and saying in as many words that declaring the key on the object's FIELD does not reach this dialog either. ### Why the declare set is `dependsOn` and not the whole measured tail The tail is measurably READ, so declaring it would be defensible on that half alone. It is not declared because the other half is missing: the census found no author writing one, and a declared key is published contract whose removal costs a full retirement kit, while an over-strict refusal costs one card. The asymmetry decides it. The measured tail is written into the file beside the guidanceSet so the next reader has the evidence without re-deriving it, and the residual question is filed rather than guessed — see Acceptance notes. **⚠️ The cost is real and is not buried:** those keys were honoured, and they are refused now. That is the behaviour change the ruling's own words priced in («a behaviour change for every existing author of a bulk param, not just for this key»), one-shot, no grace window, no dual spelling. The census measures the in-corpus breakage at zero. ### What is deliberately NOT closed `params[].options[]` stays `.passthrough()`. Its openness rests on its own 2026-08-03 measurement (the option entries are spread verbatim into the field metadata, where the widgets read `color` / `icon` / `disabled` / `visibleWhen`), which this change does not disturb. Closing it by symmetry with its parent would delete widget config the renderer honours — the same defect this PR closes one level up. The declared `{ label, value }` pair is still type-checked. --- ## 3 — A brief premise corrected on measurement The dispatch named `packages/spec/src/ui/action.zod.ts` as «the twin whose shape and `.describe()` text you must match». Measured: **`ActionParamSchema` declares no `dependsOn` at all.** The single-record dialog reaches the key through the field-backed route (`resolveActionParams` resolves the object's field definitions), so the spec's only declaration of this key is **`FieldSchema.dependsOn`** (`packages/spec/src/data/field.zod.ts`) — which is also the spelling the card itself names as the one objectui was ruled to honour. So `action.zod.ts` is the twin for **strictness**, and `FieldSchema` is the twin for **this key's shape and description**. Both halves are honoured: the member is byte-for-byte the field-level union (`string` or a strict `{ field, param }` entry, same alias table), and the description is the field-level text with ONE sentence appended — a bulk run holds a selection and not a row, so «other field(s) on the same record» had to say what the record is here (the dialog's own in-progress param values, i.e. a sibling param of the same def). ⛔ `action.zod.ts` is not edited. A parity pin (`accepts exactly what the FieldSchema twin accepts, and refuses exactly what it refuses`, 8 cases, asserted equal as a vector and asserted to contain both verdicts) is what stops the two doors drifting into dialects. --- ## 4 — Verification Run against `837234d86b`, this branch's final commit. | leg | command | result | | --- | --- | --- | | build | `pnpm --filter @objectstack/spec build` | exit 0 | | typecheck + test | `pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test` | exit 0 — **492 test files, 14493 tests passed** | | generated artifacts | `pnpm --filter @objectstack/spec check:generated` | exit 0, all 16 up to date after regeneration | | ADR-0087 | `node scripts/check-adr-0087-registration.mjs --base origin/main` | exit 0 — `[BREAKING+clause-②-narrowing] registered ui-bulk-action-param-unknown-keys-refused (new here)` | | eslint, whole repo | `pnpm lint` (= `eslint . --no-inline-config`) | exit 0 — no narrowing claimed, the full run fits | | control bytes | `grep -naP` over all 14 changed paths, plus `pnpm check:nul-bytes` | no match / exit 0 | | derived gate families | `node scripts/pm/dispatch-gates.mjs` over the real change set, reconciled with `--ran` carrying exit codes | **101 green, 7 NOT MEASURED** | **The 7 NOT MEASURED, every one a `PREREQUISITE NOT MET` refusal that needs a repo-wide build this lane does not own** (exit 3, except the last which exits 1 and says the same thing in words — recorded here rather than counted as a failure): `check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`, `check:skill-examples`. ⛔ None of them read anything about this diff; they are declared to CI, not skipped quietly. **Regenerated, never hand-edited:** `authorable-surface/ui.json` (gains `ui/BulkActionParam:dependsOn`), `api-surface-declarations/*.txt`, `content/docs/references/ui/{bulk-action,view}.mdx`, the strictness-ledger counts, and `migrations/registry.ts` (from the new one-file entry, via `gen:migration-registry`). ⛔ Nothing was typed between the generated markers. `authorable-surface.base.json` is unchanged, as expected — only `gen:authorable-surface-base` writes it. **Strictness ledger moves the right way:** `ui/` passthrough 3 → 2, strict 165 → 167; repo total strict 318 → 320, passthrough 4 → 3. **No in-repo consumer of the narrowed type.** `BulkActionParam` loses its index signature when the shape closes. Measured: no file outside `packages/spec/src` imports that type (`packages/cli` and `packages/spec/scripts` mention it in prose only), so no consumer typecheck is owed. `packages/cli typecheck` was attempted and refuses on unbuilt workspace dependencies — the AGENTS.md section-9 stale-closure signature, unrelated to this diff and left to CI. --- ## Acceptance notes Everything below was found on the way and is deliberately NOT fixed here. 1. **To file — should the measured widget-config family become declared on a bulk param?** After this PR, `min` / `max` / `step` / `precision` / `scale` / `rows` / `accept` / `maxSize` and the picker knobs are refused at parse while the widget one seam over would still honour them, and there is no field-backed route to reach the dialog by. That is a real authoring gap: an author reading the renderer's vocabulary writes a key the runtime now rejects. Sized and located by the measurement in §1a. Dedupe words: `BulkActionParam`, widget config, `min`/`max`/`step`, `bulkParamToField` spread, field-backed bulk param. 2. **To file — the objectui-side half of this landing.** `objectui`'s `packages/plugin-grid/src/__tests__/bulkLookupDependsOnReach-8755.test.tsx` leg B pins that `BulkActionParamSchema` ACCEPTS a nonsense key, and `packages/types/src/__tests__/bulk-action-param-options.test.ts:139` parses an authored param through the same schema. Both are correct against installed 17.4.0 and both turn red the day objectui's spec pin crosses this release; the first has to be re-judged into a refusal pin the way this PR re-judged its own. Nothing here breaks objectui's BUILD — no export is removed or renamed — so this is a coordination note, not a Post-Task-Checklist-4 blocker. Dedupe words: objectui, `bulkLookupDependsOnReach-8755`, leg B null reading, spec pin bump, bulk param strict. 3. **Noted, not filed:** `packages/spec/src/shared/union-author-message-pins.test.ts` carries a hand-maintained table of string-or-object union sites, and this PR adds one (`BulkActionParam.dependsOn`). The file says out loud that nothing mechanically holds that table equal to the tree and that a standing re-scan «is deliberately left to its own card», so the gap is already recorded there. The new site's rendered message is pinned in this PR's own sibling test instead (surface phrase, rename arrow, and the string arm's kind mismatch asserted absent). Carrier: the next PR that touches that table, or the standing-guard card the file already names. 4. **Noted, not filed:** the module header's claim that the catch-all forwarded `min/max/step/format` was 3-for-4 — no form widget reads `format` on this path. The header is corrected in this PR rather than filed, because the sentence lives in the file being edited. Carrier: none needed. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
objectstack-ai#19374) Fixes objectstack-ai#19071 Clause-②: no The runtime filter door now refuses a blank `$between` endpoint, exactly as the authoring schema door already does. ## The split this closes `RANGE_ENDPOINT_DESCRIPTION` — the published endpoint contract shared by both of `$between`'s bounds — has stated since 2026-09-17 (objectstack-ai#18012, batch objectstack-ai#146 item 5 letter A) that "BOTH are required NON-BLANK: an empty string, null and undefined are refused, and the refusal names the blank side". That rule shipped at the schema door only. Re-measured on this branch's base `2277d1fcd` at 2026-09-20T13:22Z, all three of the card's probes reproduce, control included: | probe | before this PR | |:--|:--| | `parseFilterAST({ at: { $between: ['', ''] } })` | returned unchanged — same object reference | | `parseFilterAST({ at: { $between: ['2026-01-01', ''] } })` | returned unchanged — same object reference | | control `parseFilterAST({ at: { $between: [null, 1] } })` | threw `Operator "$between" on field "at" requires two non-null bounds` | One published sentence, two truth values. The door that passed it is the one an embedder reaches by handing a lowered filter straight to a driver, where the range stops bounding on the blank side while still reading as a complete two-element range. ## What this changes `assertListComparandShapes`' `$between` arm refuses `''` and `undefined` at either bound with `INVALID_FILTER` / 400, naming the blank side (MIN or MAX plus the index) and carrying the schema door's own two prescriptions — write the bound you meant, or drop `$between` for `{"$gte": min}` / `{"$lte": max}` if only one side was ever bounded. The longest assembled form measures 466 characters against the unrelaxed 500-character client bound, and the bound test grew the new cases. Order inside the arm is arity, then `null`, then blank, so a pair that is blank on one side and `null` on the other keeps the message it has had since 2026-08-31. ##⚠️ One ruled word could not be implemented as written — please read this cell The ruling says the runtime door refuses a blank endpoint "exactly as the schema door does (empty-after-trim string, `null`, `undefined`)". Those two halves disagree, and the parenthetical is the one that does not hold: **the schema door does not trim.** | endpoint pair | schema door on `2277d1fcd` | this PR's runtime door | |:--|:--|:--| | `[' ', 'M']` (whitespace-only) | `success: true` | passes | | `['\t\n', 'M']` | `success: true` | passes | | `['', 'M']` | refused, names MIN | refused, names MIN | `rangeEndpointSchema`'s own comment states the rule as `endpoint !== ''` and says in as many words: "⛔ Not a trim and not a whitespace rule: the ruling is the empty string, and widening it here would narrow a published face further than ruled." `filter.test.ts` then pins `RangeOperatorSchema.safeParse({ $between: [' ', 'M'] })` green on purpose, with the comment "this assertion is what keeps a later reader from widening it without a ruling of their own". So an empty-after-trim predicate here would have re-opened the very split this card exists to close — in the opposite direction, with whitespace passing the authoring door and being refused one step later — and narrowed a published face further than any ruling has. This PR implements the operative clause ("exactly as the schema door does", "the same guidance as the schema door") and leaves whitespace-only endpoints legal at both doors. ⛔ Nothing is re-adjudicated: if the intent really was to trim, that is a second narrowing of a published face and wants its own ruling, and it is one line here plus the `filter.test.ts` pin on the other side. ## Scope held, deliberately - ⛔ `RANGE_ENDPOINT_DESCRIPTION` and `packages/spec/src/data/filter.zod.ts` are not touched — B was refused, and that file is held by open PR objectstack-ai#19335. - ⛔ No ADR-0087 transition is registered — C was refused. The changeset's disposition is `already-registered` against objectstack-ai#18012's existing entry, which covers this same surface set. - ⭐ Only the `$between` row of the `filter-comparand-shape.test.ts` pin is inverted. The `$in` / `$nin` falsy-member rows stand, in place, with a note saying which row moved and why: falsy VALUES are values, and this ruling is about range ENDPOINTS. The replacement pin reads BOTH doors and asserts they agree, rather than restating either. - Pointer for objectstack-ai#13357: the null-shaped carve-out recorded there is preserved unchanged — `null` bounds keep their own message and their own prescription (the null predicate), and are checked first. This PR narrows nothing that ruling settled; it adds a second, differently-spelled blank alongside it. ## Verification Every reading below was taken in this worktree at HEAD `7682aebc` (after `git merge origin/main`), unless the line says otherwise. - `pnpm --filter @objectstack/spec build` — green. - `pnpm --filter @objectstack/spec test` — 502 files / 14696 tests passed. - `pnpm --filter @objectstack/spec typecheck` — green (`tsc --noEmit`, scripts project, and the test-layer debt ledger held at 54 files / 259 errors / 144 pinned signatures). - `pnpm --filter @objectstack/objectql exec vitest run src/engine-filter-array-lowering.test.ts src/engine-comparand-type-door.test.ts src/query-expression-conformance.test.ts src/protocol-explicit-filter-field-gate.test.ts` — 4 files / 289 tests passed. First attempt was a PREREQUISITE failure, not a red: the closure was unbuilt and the run died on "Failed to resolve entry for package @objectstack/core". Built `@objectstack/objectql^...` and re-ran. - Gate family re-derived in this worktree from the real changed paths: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — 81 families at this head (75 before the changeset existed). All 81 run with exit codes captured to disk before any pipe; `--ran` reconciles 81 derived / 80 run / 1 NOT MEASURED / 0 UNRUN. - The one NOT MEASURED is `node scripts/check-plugin-teardown-shape.mjs --self-test`, exit 3: its positive control is pinned to commit `621a487607881c66b2899b7e3477115229a156b4`, which this shallow checkout cannot reach; `git fetch --deepen 500` did not bring it. That command grades the checker's own fixtures, not this diff — the PR-relevant form, `node scripts/check-plugin-teardown-shape.mjs`, exits 0. - Three gates first answered exit 3 PREREQUISITE NOT MET on an unbuilt tree (`check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`) and one hit my runner's own 150-second cap (`check:query-options-erasure`). All four are green after `pnpm build`, and `check:type-check-debt` re-measures 4 ledger entries / 53 raw errors, none above its recorded number. - `pnpm lint` — the whole repo, `eslint . --no-inline-config`, exit 0 at `7682aebc`. No narrowing was claimed and none was needed. ## Acceptance notes Noted while measuring, ⛔ not filed by this dispatch and ⛔ not fixed here: - **A `{ $field }` reference as a `$between` endpoint is refused by the schema door and accepted by the runtime door.** The same two-door shape as this card, one endpoint spelling over, and already ruled on the schema side: `RANGE_ENDPOINT_DESCRIPTION` says "A { $field } reference is NOT an endpoint shape", ruled 2026-08-11 under objectstack-ai#7596. Reproduction on `2277d1fcd`: `RangeOperatorSchema.safeParse({ $between: [{ $field: 'a' }, 'M'] })` answers `success: false`, while `parseFilterAST({ f: { $between: [{ $field: 'a' }, 'M'] } })` returns the filter unchanged. It is left alone here because its refusal needs its own wording and, being a second narrowing of a published face, its own ruling — the same reason this card exists. Dedupe words: `field reference between endpoint` · `parseFilterAST runtime door` · `filter-comparand-shape` · `7596 endpoint shape` · `two doors disagree`. - **`@objectstack/hono` fails its dts build under `turbo run build --concurrency=2 --filter='./packages/*' --filter='./packages/*/*'`**, with `TS7016: Could not find a declaration file for module '@objectstack/plugin-hono-server'`, and builds clean under `pnpm build` on the same tree minutes later. Reads as a build-ordering race in a shared warm cache rather than a defect in the tree; recorded because the gate prescription for `check:type-check-debt` names that exact command. Carries no reproduction that does not depend on cache state, so it is an observation, not one of the three filing classes. --- _Generated by [Claude Code](https://claude.ai/code/session_01HnRAeVTLJevtQ5iCPX6JSm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 28, 2026
veigajoao
pushed a commit
to veigajoao/objectstack
that referenced
this pull request
Sep 29, 2026
…ction-*, data-* and element-* migration entries states each lesson in words, not tracker numbers (stage 4) (objectstack-ai#20509) Part of objectstack-ai#20233 Stage 4: the datasource-, filter-, action-, data- and element- families. Clause-②: no **Stage 4 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on objectstack-ai#19123 (`5749154545`) sets the shape: the lesson in words, and no number, dead or alive; a cross-repo number (`cloud#N`, `objectui#N`) is still a tracker number. This stage covers the next five families, `datasource-`, `filter-`, `action-`, `data-` and `element-`: **150 sites → 0** in the three prose fields, across 33 entry files. None of the 37 entries in these families carries a tracker id in `surface` (ruling A of the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do here). Each site now says what the cited ruling, measurement or fix decided. ADR ids stay. `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The pin now holds eleven families. ## Census — tracker ids in the author-shown fields **Instrument.** The stage-2 AST instrument, re-written to its published description (the stage-2 and stage-3 copies lived in removed scratchpads): a TypeScript-AST walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each entry object literal it evaluates the string value of `replacement`, `reason`, `acceptanceCriteria` and (counted separately) `surface`, joining string literals with `+`, then counts `#` followed by 4 or 5 digits at a word boundary. **Validated first** by reproducing earlier readings on their own trees (extracted with `git archive`): on `443b2f4fdc` (stage 1) `engine-` 67, `datasource-` 43, `filter-` 30, `action-` 26, `element-` 25, `data-` 24, whole tree 266 entries / 1,016 sites / 9 `surface`; on `0d7ed5a378` (stage 3 landed) whole tree **711** sites / 7 `surface` — every figure equal to the stage-1 census and to stage 3's after-count. Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), before and after. - **Dark (comment lines):** 823 `//` / docblock lines in entry files carry a tracker id, and none is counted; 823 before and after. Comment lines are the sibling card objectstack-ai#20234's surface, and this PR touches none (proved below). - **Dark (field boundary):** the 7 `surface` sites left in the tree (other families) count 0 in the three-field total. **Base `fc0db22b`:** `datasource-` 9 entries, **43** sites (3 / 38 / 2); `filter-` 11, **32** (1 / 30 / 1); `action-` 6, **26** (0 / 23 / 3); `element-` 5, **25** (2 / 19 / 4); `data-` 6, **24** (0 / 24 / 0). **150** sites (6 / 134 / 10) in 33 of the 37 entries; 88 distinct ids (80 bare, 8 `objectui#`, 2 `cloud#`). Whole tree: 310 entries, **721** sites (711 after stage 3, plus entries `main` added since in other families), 7 `surface`. **After this PR:** all five families **0**; the six earlier families still 0; whole tree **721 → 571**; `surface` 7 (other families). The PM's rough line count (156 sites, 33 files) is a wider instrument; the AST reading is 150 in the same 33 files. | entry | sites (replacement / reason / acceptanceCriteria) | short `#NN` | |---|---|---| | `17.action-descriptor-is-async-retired` | 2 (0 / 2 / 0) | | | `17.action-descriptor-resume-authority-default-flip` | 10 (0 / 7 / 3) | | | `17.action-session-roles-to-positions` | 12 (0 / 12 / 0) | | | `17.data-driver-find-stream-retired` | 1 (0 / 1 / 0) | | | `17.data-driver-query-omit-object` | 9 (0 / 9 / 0) | | | `17.data-engine-batch-retired` | 1 (0 / 1 / 0) | | | `17.data-field-changed-event-retired` | 4 (0 / 4 / 0) | | | `17.datasource-config-inline-credential-refused` | 1 (0 / 1 / 0) | | | `17.datasource-config-placeholder-refused` | 5 (0 / 5 / 0) | | | `17.datasource-config-url-userinfo-refused` | 4 (0 / 4 / 0) | | | `17.filter-regex-options-retired` | 8 (0 / 8 / 0) | | | `18.action-bulk-dispatch-contract-undeclared` | 1 (0 / 1 / 0) | | | `18.action-engine-facade-find-query-envelope` | 1 (0 / 1 / 0) | 1 | | `18.data-file-value-duration-unit-in-key` | 6 (0 / 6 / 0) | 1 | | `18.data-nosql-query-options-timeout-unit-in-key` | 3 (0 / 3 / 0) | 1 | | `18.datasource-config-mongo-options-credential-refused` | 5 (0 / 5 / 0) | | | `18.datasource-config-postgres-url-unparseable-refused` | 4 (0 / 4 / 0) | | | `18.datasource-config-url-query-credential-refused` | 4 (0 / 4 / 0) | | | `18.datasource-credentialsref-mongo-composed-no-username-refused` | 10 (2 / 7 / 1) | | | `18.datasource-credentialsref-mongo-url-no-user-refused` | 10 (1 / 8 / 1) | | | `18.element-data-source-and-object-block-filter-rule-array` | 11 (1 / 9 / 1) | 1 | | `18.element-number-filter-rule-array` | 7 (0 / 6 / 1) | | | `18.element-record-picker-filter-rule-array` | 7 (1 / 4 / 2) | | | `18.filter-between-blank-endpoint-refused` | 3 (0 / 3 / 0) | 1 | | `18.filter-between-field-reference-endpoint-refused` | 5 (1 / 4 / 0) | | | `18.filter-comparand-types-and-widget-nested-slots-refused-at-save` | 2 (0 / 2 / 0) | | | `18.filter-equality-array-comparand-refused` | 1 (0 / 1 / 0) | | | `18.filter-equality-array-comparand-refused-at-save` | 1 (0 / 1 / 0) | | | `18.filter-icontains-comparand-refused-at-parse` | 3 (0 / 3 / 0) | | | `18.filter-ne-array-comparand-refused` | 1 (0 / 1 / 0) | | | `18.filter-preset-ordering-comparand-refused` | 4 (0 / 4 / 0) | | | `18.filter-query-face-comparands-refused-at-save` | 1 (0 / 1 / 0) | | | `18.filter-text-operator-declared-type-refused` | 3 (0 / 2 / 1) | 1 | | **total, 33 entries** | **150 (6 / 134 / 10)** | **6** | ## Text only — proved by a base-vs-head AST comparison For every entry file this PR changes, both versions (`fc0db22b` and the head) are parsed and three things are compared token for token: every import declaration, every property other than the three prose fields (so `id`, `surface`, `from` / `to` and any matcher, by evaluated value), and every comment token in the file. **33 files compared, 0 with a non-prose change.** The instrument is shown able to fail first: on an in-memory copy it reports DETECTED for a mutated `id`, a mutated comment and a mutated `surface`. So none of objectstack-ai#20234's comment lines moved, and no entry's identity or matching moved. ## Every citation read, and what the text now says Each cited id was read with a single-card REST read (body plus the ruling, measurement or landing comments), resolved against the repository its sentence names. Ids are in code spans so this body posts no cross-references. **10 bare ids answer 404** on both the issues and the pulls endpoint (re-probed with a 200 control, `14478`), and **2 `cloud#` ids are unreadable from this session** (403); those sentences are rewritten from what `main` records, listed in Acceptance notes. **`datasource-` (14 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7990` | Maintainer, 2026-08-12, Option A: close inline credentials per artefact (each schema refuses at publish and diverts to its existing mechanism); the heuristic `sys_metadata` write guard parked. | "The maintainer ruled on 2026-08-12 to close that per artefact: each schema that admitted an inline credential refuses it at publish …"; "the inline-credential closure" | | `8078` (PR) | The spec half of that ruling; measured that `${…}` placeholders reach the client verbatim and that `config.url` still took the secret. | "measured by the credential census while the inline-credential refusal was being built"; "measured when the inline-credential refusal was built" | | `8082` | Maintainer, 2026-08-12, option A: refuse URL userinfo at publish through one shared value-level parse; runtime DSNs unaffected. | "The maintainer ruled on 2026-08-12 (Option A) to refuse the URL userinfo password at publish, through one value-level parse the driver schemas share" | | `8336` | Maintainer, 2026-08-13, direction 2 of two: refuse `${…}` at publish; implementing resolution rejected. | "The maintainer ruled on 2026-08-13 for the second of two directions: refuse the syntax loudly at publish" | | `8337` | The credential-bearing URL query parameter refusal, one syntax over from userinfo. | "the credential-bearing URL query parameters"; "the query string was the third spelling of the identical secret, one syntax over" | | `8876` | **404** — see Acceptance notes. | "the asymmetry the URL grammar keeps between its two userinfo halves — a username is not credential material" | | `8696` | **404** — see Acceptance notes. | "measured when the bound secret was made to reach the mongo client on its URL branch"; "`new URL()` rejects the multi-host form outright, and the mongo arm hands the authored URL to its client untouched"; "the defect class closed when each DSN branch was made to inject the bound secret its composed branch already used" | | `9041` | **404** — see Acceptance notes. | "the sibling URL-branch entry, `datasource-credentialsref-mongo-url-no-user-refused`"; "this URL-branch refusal" | | `9147` | The composed-branch twin: bound secret + no `url` + no `username` refused, inheriting the URL-branch ruling. | "this composed-branch refusal" | | `7314`, `7385`, `8152`, `8875` | The driver-factory arms that dropped something declared: the turso loader's install remedy and half its config, the other optional arms' missing-package remedy, turso's never-read bound secret, and (`8875`, **404**) the mysql DSN branch. | "the family of driver-factory arms, closed one driver at a time, that each dropped something declared without a word: the optional-driver arms that answered a missing package with no remedy, the turso arm that never read its bound secret, and the mysql and mongo DSN branches that discarded one" | | `8873` | **404** — see Acceptance notes. | "the postgres equivalent is judged on its own client's measurement, never inherited" | **`filter-` (24 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4706` | Maintainer, 2026-08-06, option B: retire `$regex` loudly and add `$icontains`; five-backend true regex (A) excluded (turso remote cannot, no business pull). | "the maintainer's 2026-08-06 ruling (option B: retire `$regex` loudly and add `$icontains`, rather than make five backends agree on one regex dialect)" | | `5701`, `5702` | The contract half and the driver half of that ruling. | "the retirement's own contract-half change"; "the contract half (the `$icontains` declaration, …)"; "the driver half" | | `6148` | **404** — see Acceptance notes. | "the gate that makes a breaking changeset state its ADR-0087 disposition" | | `18012` | **404** — see Acceptance notes. | "Maintainer ruling A of 2026-09-17: a blank `$between` endpoint is refused at the authoring door, and the refusal names the blank side." | | `13495` | driver-memory's reference matcher answered a null-bounded range with every valued row. | "The reference matcher had already been taught to survive the null-bound form of exactly this" | | `objectui#9695` | The console filter builder stops padding a half-typed pair with an empty string. | "is a change to the console's own filter builder and lands on its own schedule" | | `7596` | Maintainer, 2026-08-11, ADR-0049 REMOVE: no `{ $field }` endpoint in either `$between` union. | "Maintainer ruling of 2026-08-11 on column-reference range endpoints, ADR-0049 enforce-or-remove: REMOVE"; "reviewed and accepted with that ruling" | | `7713` (PR) | Shipped that removal with a not-required disposition. | "the 2026-08-11 changeset carried the disposition not-required" | | `5222` | `$field` compiled to a column-to-column comparison on the SQL faces. | "the position the column-to-column comparison compiles on every face" | | `19377` | **404** — see Acceptance notes. | "filed as an entry of its own rather than as an already-registered rider" | | `20116` | The collector for the family "the save door accepts comparand shapes the query faces refuse", closed in stages. | "the second stage of closing the family of comparand shapes the save door accepted and the query faces refused"; "the family of comparand shapes …" | | `7872` | Maintainer, 2026-08-12: the shared face accepts `string` / `number` / `bigint` / `boolean` / `null` / `Date` and refuses everything else. | "the accepted set the maintainer ruled on 2026-08-12: string, number, bigint, boolean, null and Date" | | `19889` | Director seat, 2026-09-24, option A: the schema door refuses what the compile face refuses (the standing array-equality refusal applied to it). | "Ruled on 2026-09-24 (option A), applying the standing refusal of an array in the equality slot to the schema door" | | `19757` | Maintainer, 2026-09-23, option 乙: refuse an equality-slot array at the shared face; 甲 (declare array equality) and 丙 (document the divergence) rejected. | "Maintainer ruling of 2026-09-23 (option 乙 — rather than declaring an array equality the SQL-family backends would have to invent, or documenting a divergence that stays silent on one backend)" | | `19514`, `objectui#9050` | The protocol half of the maintainer's 2026-09-20 ruling C′ on the console's filter converter, rule 1 「the differences are the protocol's to close」. | "The protocol half of the maintainer's 2026-09-20 ruling (option C-prime) on the console's filter converter, whose first rule reads, verbatim and untranslated: 「the differences are the protocol's to close」" | | `18113` | Published the text-comparand refusal predicate and reason beside `FILTER_TEXT_CASES`. | "the pair published in this package beside FILTER_TEXT_CASES for exactly this reason" | | `19886` | Director seat, 2026-09-24, option A on standing text: `$ne` with an array refused at the shared face and the schema door. | "Ruled on 2026-09-24 by the director seat, on the standing contract text (option A)" | | `8690` | Ruled 2026-08-15: option B (engine door refuses an uninterpretable temporal comparand) with option C (the preset refusal at authoring) alongside. | "The authoring half (option C) of the maintainer's 2026-08-15 ruling on uninterpretable temporal comparands, ruled alongside the engine door (option B)"; "measured on the defect report" | | `8808` (PR) | The engine door. | "before the engine door and a 400 after" | | `15661`, `15773` | Maintainer, 2026-09-05, C-deny: refuse a text operator over a never-string declared type, over the existing type sets; landed at the engine seam. | "Maintainer ruling of 2026-09-05 (option C-deny: refuse now, over the type sets the contract already declares, minting no new vocabulary), landed at the engine seam" | | `14079` | Ruling A: a stored non-string value never satisfies a positive text operator and satisfies `$notContains`. | "(a stored value that is not a string never satisfies a positive text operator and satisfies `$notContains`)" | **`action-` (19 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `6748` | Retire `isAsync`: zero readers on a fresh three-repo measurement. | "a fresh three-repo measurement (taken when the key was filed for retirement, and re-run at pickup)" | | `6667` | Enforce `supportsPause` at runtime. | "The sibling took the ENFORCE leg of the same ruling" | | `3801` | The generic resume route needs an authorization gate keyed on the suspended node. | "The generic resume route's authorization gate keys on the SUSPENDED NODE" | | `3823` | The revise-window `wait` pause is service-owned but inherited `'any'`. | "The revise-window incident decided the direction" | | `4484`, `5540`, `6011` | The three retirements whose disposition this one shares. | named by their entry ids, which the sentence already carried | | `5561` | `resumeAuthority` defaulted to `'any'` — fail-open by omission. | the sentence already states it; ADR-0019's resume-seam addendum named by date and title | | `5703` | `supportsPause` / `isAsync` were zero-reader declarations. | "no longer the declaration nothing enforced" | | `5613` | Maintainer, 2026-08-06, contract-first ("C skeleton + A semantics"): declare the shape as it stands, then rename on the typed face. | "The maintainer ruled contract-first on 2026-08-06 (…: declare the shape as it stands first, then rename on the typed face)"; "the runtime half of the same ruling" | | `5697`, `5779` | Phase 1 (the schema) and phase 2's spec half (the canonical key and the alias). | "phase 1 declared …"; "`positions` is now the canonical key …" | | `5050` | The hook-side `roles` removed outright. | "(removed outright)" | | `3280`, `3290` | The hook `ctx.session.tenantId` alias: deprecated, then removed in the next major. | "the hook `ctx.session.tenantId` alias: deprecated first, removed in the next major" | | `4579`, `4657` | The `openApi31` and `activationEvents` retirements. | named by their surfaces, which the sentence already carried | | `17319` | Ruled 2026-09-12, A: an action declares its dispatch contract; B (unify the two wirings) refused. | "the 2026-09-12 ruling that made an action declare its dispatch contract refused exactly that option" | | `14175` | The earlier typing fix that gave `find` the filter alone. | "the parameter shape an earlier typing fix chose (the filter alone), ruled by the director seat on 2026-09-12, with the maintainer's agreement" | **`data-` (15 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4484`, `4618`, `4673` | The three retirements, each already stated by its own entry. | trailing ids dropped (ADR ids kept) | | `4639` | Multi-record predicate writes got `data.records.*` events. | "since multi-record predicate writes were given events of their own"; "the way bulk writes were given theirs" | | `3196` | Webhook triggers trimmed to producers that exist. | the sentence already states it | | `cloud#1053`, `cloud#1030` | **403 here** — see Acceptance notes. | "20 such sites were measured in the downstream cloud codebase, and a `$like` the type layer would have caught reached runtime there through exactly that hole" | | `6350` | The stock reconciliation of the v17 train's breaking changesets against the ledger; this change was its control sample and was itself an omission. | "Registered by the stock reconciliation that compared the breaking changesets already on the v17 release train against this ledger. This change was that audit's CONTROL sample …"; "(backfilled by that reconciliation)" | | `5181` | The `DriverQuery` narrowing itself. | "this change"; "this narrowing" | | `6321`, `6083` | Two later call-parameter changes, both registered (`6083` **404**; `main` records it as ADR-0122 phase 2). | "Two later, smaller driver call-parameter changes both registered" | | `18669` | Maintainer, 2026-09-17, ruling A: rename the last two duration keys no closed duration type could express, no new closed type, no narrowing. | "Maintainer ruling A of 2026-09-17 on the last two duration keys no closed duration type could express" | | `18122` | Published `DurationMs` / `DurationSeconds` beside `EpochMs`, the unit set derived from six genuine duration rows. | "published beside `EpochMs` as a closed duration type"; "one of the six genuine durations the closed types' unit set was derived from" | | `14478`, `15680` | Ruling B of 2026-09-02 on duration units; the data-directory rename round. | "Maintainer ruling B on duration units (2026-09-02)"; trailing ids dropped | **`element-` (17 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `objectui#6206` | Maintainer, 2026-08-25, option B: one filter orthography platform-wide — the `ViewFilterRule` array, not a record-shaped exception; measurement-first binding. | "the maintainer's 2026-08-25 ruling, option B: …" (three entries); "the console-side half of this convergence" | | `15442`, `15449` | Ruled 2026-09-06, option A: converge the binding and the four `object-*` doors family-wide, one entry; exception and mixed rejected. | "ruled 2026-09-06, option A: converge the binding and the four block doors family-wide, under one entry, rather than record an exception"; "a gap measured on its own" | | `7751` | Maintainer, 2026-08-12: the `object-*` block props enter `ComponentPropsMap`. | "a read-point record derived from the renderers on 2026-08-13, when the `object-*` blocks first got props schemas in the map" | | `objectui#6948` | The console's filter converter had no branch for `$and` / `$or` / `$not`. | "(the console's filter converter had no branch for them)" | | `15828` | `POST /analytics/query` refused the array `where` the adapter sent. | the sentence already states it; "the runtime route's refusal of the array corrects it here" | | `16626` | **404** — see Acceptance notes. | "parked behind a bump of that pin" | | `objectui#7754`, `objectui#7752`, `objectui#6828` | The console adapter lowers an array analytics filter before the wire; `aggregate()` runs `translateFilterArray`. | "the console adapter was changed so `ObjectStackAdapter.aggregate()` runs the same `translateFilterArray`"; "The adapter-side lowering lands in the console's own repository." | | `5158` | Maintainer, 2026-08-04, ruling C: `FilterArray` is input-only sugar with one lowering seam (`parseFilterAST`). | "since the maintainer's 2026-08-04 ruling C declared the array input-only sugar with one lowering seam" | | `5334` | The in-process analytics door, added when an array `where` was silently dropped. | "(added when an array `where` was found silently dropped on the analytics path)" | | `17321` | Ruled 2026-09-12, B: a partial D2 conversion for the losslessly mappable record forms; combinators pass through, named. | "(ruled 2026-09-12, option B: convert what maps losslessly and name what does not, rather than leave every stored row to its next save or flatten combinators)" | | `14406` | Converged `element:record_picker`; its census pin asks whether any `filter` door refuses the array. | "the twin of the census pin that asks whether any `filter` door still refuses the array"; the 2026-08-25 Option-A ordering ruling stated as "measure the consumer's read path before the contract moves" | | `12039` | `element:number` converged. | "after `element:number` converged" | | `objectui#7663` | The console registry's `inputs.filter` flip for the picker. | "a console-side change filed in the objectui repository, blocked on that release" | | `15829` | The dashboard widget `filter` location, a finding of its own. | "a different family, judged on its own" | ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`, `kernel-`, `system-`, `datasource-`, `filter-`, `action-`, `data-`, `element-` (`data-` does not select `datasource-` or `dataset-`: the match is `startsWith('data-')`). The `REWRITTEN` floor rises from **55 to 88** ids: the 33 entries of this stage that carried a tracker id. The three `it` blocks are textually unchanged: the detector is exercised on both sides, every covered prefix must select an entry, and each covered block is found verbatim in the real CLI's stdout before it is asserted clean. The file keeps its stage-1 name; the header lists the eleven covered families. ## Ablation — the widened pin can fail on a new-family block From committed state, HEAD `d8bcc46a`, in one lock turn, with `scripts/ablation-replace.mjs` in wrap mode (it owns the mutation's restore trap; the leg script adds its own `trap … EXIT INT TERM` that restores `registry.ts` from `HEAD` by absolute path and checks the blob) and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is built from the generated `registry.ts`, so that is the file mutated. - **Mutation.** In `registry.ts`, the `acceptanceCriteria` of `datasource-credentialsref-mongo-url-no-user-refused`: anchor `parse reports this URL-branch refusal.` → `parse reports the objectstack-ai#9041 refusal.` The tool read anchor 1 → 0 and replacement 0 → 1, blob `7001c102` → `6f3f81ff`. - **Mutate leg.** Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `datasource-credentialsref-mongo-url-no-user-refused: the printed guidance cites a tracker id: expected 'objectstack-ai#9041' to be undefined`. - **Restore.** Tool-proven: blob `7001c102` == HEAD, `git diff HEAD` empty. - **Restore leg.** Spec build exit 0. The `--absent` preflight found the marker in none of 224 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. Whole tree afterwards: 0 dirty paths. ## Verification Final head **`d8bcc46a`**. Every heavy run went through `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0` on each turn; per-step exit codes recorded separately). Where a line was taken at `95fb97ea` it says so: the one commit after it (`d8bcc46a`) changes one entry sentence and the three generated projections, and every reader of that text was re-run at `d8bcc46a`. - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 58 successful, 58 total` (at `95fb97ea`); the spec package was rebuilt at `d8bcc46a` in both ablation legs (exit 0). - **Pin:** at `d8bcc46a`, the ablation's restore leg: `test/migrate-meta-engine-guidance.test.ts` `3 passed`. With its neighbour at `95fb97ea`: `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`, `Tests 10 passed | 1 skipped` (the skip is the default-range file's own `skipIf`). Re-run with its neighbour at `d8bcc46a`: `Test Files 2 passed`, `Tests 10 passed | 1 skipped`. One earlier run at this head is void and is not counted: it spawned the CLI while a concurrent lock-free gate was rebuilding the package `dist/` trees (`MODULE_NOT_FOUND`, both files); the re-run is the reading. - **Spec, the whole `local` project** (at `95fb97ea`): `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gives `Test Files 572 passed (572)`, `Tests 16789 passed | 1 todo`. **The `repo` project** (at `d8bcc46a`): `Test Files 38 passed`, `Tests 690 passed`. **The readers of the changed sentence** (at `d8bcc46a`): `src/migrations`, `src/ui/action-params.test.ts`, `src/ui/filter-rule-array-guidance.test.ts` give `Test Files 5 passed`, `Tests 229 passed`. - **CLI unit** (at `95fb97ea`, no CLI file changed after): `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts` give `Test Files 2 passed`, `Tests 28 passed`. - **The call-spelling census that reads `registry.ts`:** `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/sql-driver-query-signature.test.ts` gives 15 passed. - **Typecheck** (at `95fb97ea`; the last commit edits string literals only): `pnpm --filter @objectstack/spec typecheck` exits 0 (test layer: 53 files / 251 errors held in its ledger); `pnpm --filter @objectstack/cli typecheck` exits 0 (3 files / 28 errors held). - **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives **90** families at `d8bcc46a` (the same set it derived at `95fb97ea`). `--ran` over the recorded exit codes reads **90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16735 customer-facing string(s) across 1167 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (310 semantic, 230 retired-key, 206 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:org-identifier`, `check:nul-bytes`, `check:dual-build-cjs-loads` (104 require entry points across 66 packages load), `check:type-check-debt`, `check:adr-0087-registration` and `check:changeset-no-major`. - How the reading was assembled: a container restart cut the pass at `d8bcc46a` after 30 families; the other 60 were run afterwards on the same head, and `check:type-check-debt`, whose first resumed run lost a package build to an OOM kill (exit 3, `PREREQUISITE NOT MET`), was re-run alone and exited 0 ("4 ledger entr(ies) re-measured … none above its recorded number"). - The earlier full pass at `95fb97ea` read 88 exit 0, `check:dual-build-cjs-loads` exit 3 (dists not yet built) and **`check:org-identifier` exit 1**: my rewrite of the action-session entry had spelled the removed hook read literally. `d8bcc46a` names that alias in words instead, and the gate reads OK there. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 36 changed `.ts` files at `d8bcc46a` reports 36 files, 0 errors, 0 warnings. - The population is read from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 36 are in it (no file-ignored warning). - Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so a text edit cannot move the verdict on a file it does not touch. - **Mergeability:** `main` moved 10 commits past the base, to `4a1df196`; the only ones under `packages/spec/src/migrations/` are the landed `20488` (two entries in the `field-` and `ui-` families, plus `registry.ts`). A driver-free bare-clone `merge-tree --write-tree` of `d8bcc46a` against `4a1df196` exits 0 with no conflicted path, and the census over that merged tree reads 0 sites in all eleven covered families, so `main` was not merged in; CI's merge ref runs the registry gates on the merged tree. ## Acceptance notes - **Ten dead ids, rewritten from the code on `main`.** Each answers 404 on both the issues and the pulls endpoint, with a 200 control (`14478`). - `8696` / `8875` / `8873` (the bound secret reaching the mongo, mysql and postgres clients): `service-datasource`'s `default-datasource-driver-factory.ts`, its CHANGELOG (the mongo URL-branch entry says it "closes the last arm of the family" the other cards closed one driver at a time; the mysql DSN fix is `{ uri, password }`) and `bound-secret-dsn-branches.test.ts`, which pins the bound secret outranking `options.auth`. `common.zod.ts` and `pg-url-grammar.server.ts` record why the shared helpers must not parse (mongo's multi-host form, which `new URL()` rejects). - `8876`: `common.zod.ts` (`urlUserinfoUsername`, the username half of the same grammar; a username is not credential material). - `9041`: `datasource.zod.ts` (`CREDENTIALS_REF_MONGO_URL_NO_USER_REFUSED` and its docblock); the text now names the sibling entry by id. - `6148`: `scripts/check-adr-0087-registration.mjs`'s header (a declared-breaking changeset must state its ADR-0087 disposition in writing). - `18012`: `.changeset/18012-between-blank-endpoint-refused.md` and `filter.zod.ts` (ruling A of 2026-09-17: a blank bound refused at the authoring door, the blank side named). - `19377`: `.changeset/19377-between-field-endpoint-runtime-door.md` and `filter-comparand-shape.ts` (the runtime door brought under the 2026-08-11 removal). - `16626`: `.changeset/filter-orthography-binding-and-object-blocks.md` on `main` records it as the objectui pin bump the element convergence was parked behind; the text now says "a bump of that pin", and the pin sha it names is unchanged. - `6083`: `main` records it as ADR-0122 phase 2 (the spec CHANGELOG); the sentence keeps its claim ("two later, smaller driver call-parameter changes both registered") without the numbers. - **Two cross-repo ids this session cannot read.** `cloud#1053` and `cloud#1030` answer 403 (`objectstack-ai/cloud` is not reachable here, and attaching it was refused). The sentence was rewritten from what this repository records: `packages/spec/CHANGELOG.md` (the published `DriverQuery` entry: 20 measured `as any` sites downstream, and a `$like` that reached runtime through that hole) and the `5181` card body ("cloud 实测 20 处 cast … cloud#1030 的 `$like` 本可在类型层拦住"). Nothing the cloud cards add beyond that is claimed. - **Short numbers and ruling-record ids went too (invisible to the regex).** Six decision-batch numbers (`objectstack-ai#43` ×2, `objectstack-ai#55`, `objectstack-ai#123`, `objectstack-ai#146`, `objectstack-ai#151`), three ruling-record comment ids and one `batch 217 item 3` spelling are numbers an author is shown and cannot follow, so each is dropped. So are the maintainer's batch acknowledgements 「146 同意」 and 「217 同意」 and the bare 「同意」 beside `objectui#6206` (×3), `15442` and `14175`: they record only that a batch was approved, and each sentence now states the ruling's date and content instead. The one verbatim quote that carries a lesson, 「the differences are the protocol's to close」, is kept untranslated. - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** A scan of the five families' evaluated prose for any run of three or more digits and for `issue` / `card` / `PR` / `batch` / `record` / `summon` plus a number now finds only HTTP statuses, ports and example values. The two `PR #…` citations were `#`-spelled, so the instrument saw them. - **One test pinned a removed tracker number.** `packages/spec/src/ui/action-params.test.ts` asserted `reason` matches the `5613` number; it is re-pinned on the sentence that carries the ruling (`/ruled contract-first/`). No other test reads these entries' prose for a tracker id (a grep of test files for the 88 cited numbers finds only comment lines and assertions on other runtime strings). - **No open PR touches these five families.** Read twice. At 2026-09-28T19:04Z: 11 open PRs' file lists (227 rows). Again at 20:28Z, just before opening this one: 8 open PRs (195 rows; the Version Packages PR `17076` skipped both times), none carrying a `migrations/entries/semantic/NN.(datasource|filter|action|data|element)-*` file. PRs `20504`, `20503`, `20460` and `20458` add or edit entries in other families (`turso-`, `view-`, `stack-`, `cube-`) and regenerate `registry.ts`: ordinary concurrency, regenerate on merge. Widening the pin reds no sibling. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1–3; only the protocol-17 entries appear in them. - **What later stages pick up** (whole tree at this head, same instrument): **571** prose-field sites in the other families, **61** short `#NN` sites, **7** `surface` sites, and three `cloud#` sites (`storage-service-list-retired`, `cloud-subpath-retired`, `cluster-driver-dangling-values-removed`; the fourth, `data-driver-query-omit-object`, is done here). - **Noted, not filed — the same rule outside this card's fields.** Runtime strings outside the migration entries still carry tracker numbers. One read in passing: `AutomationEngine`'s boot warning for a pausing node type that never declares `resumeAuthority` (`packages/services/service-automation/src/engine.ts`, the line that reads `so the objectstack-ai#3801 resume gate REFUSES every pause it creates`) is printed to a plugin author, and `resume-authority-declaration.test.ts` asserts the `3801` and `3823` numbers are in it. That is the runtime-string rule this card applies, on a producer outside its surface; it is reported to the seat, not fixed here. ## Line budget Entry files: **367 changed lines** (+214 / −153) across 33 files, against the stage-1 ≈400 budget. The whole diff is **871 lines** (+527 / −344) in 39 files. Of the rest, `registry.ts` is 367, the two projections are 68 (`spec-changes.json` 44, the upgrade guide 24), the widened pin is 42, the re-pinned test 2 and the changeset 25. --- _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>
veigajoao
pushed a commit
to veigajoao/objectstack
that referenced
this pull request
Sep 29, 2026
… sites to the commits that decided them (stage 5) (objectstack-ai#20576) Part of objectstack-ai#20234 Clause-②: no ## What changed This is stage 5 of the staged sweep: the `ui/` area (`packages/spec/src/ui/**`, 133 files), plus two sites freed since stage 4: `data/filter-subtree-provenance.ts` (PR objectstack-ai#20460 landed without touching it) and `meta-spelling/manifest-collection-spelling.ts` (the census hand-over, comment 5882628946 on objectstack-ai#20234). `ui/view-grouping-query.ts` is excluded because objectstack-ai#20446's claim holds it; it carries 4 citations and none of them is dead, so the exclusion removes nothing. Later stages cover the other areas, so this PR says `Part of`. Every comment and docblock site in that population that cites a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123). That is **87 comment sites**: 84 re-anchored and 3 respelled. - **84 re-anchored, over 25 numbers.** Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and says in its own words what that commit decided. One line (`ui/dashboard.zod.ts:686`) quotes ADR-0087 itself; it keeps ADR-0087 as its citation and paraphrases the amendment heading instead of quoting its number. - **3 respelled**, so each number of a sibling pair carries its own qualifier: `ui/view.zod.ts:3634` now reads `objectui#6110 + objectui#6111`, and `ui/component.zod.ts:3479` and `:4140` now read `objectui#8221's PR objectui#8758`. Each second number answers 404 here, and the sentence attributes it to objectui (objectui REST: `issues/6111` 200, `pulls/8758` 200, merged 2026-09-09). Only comments changed, plus the one generated reference page they project into and a patch changeset. Every source file keeps its line count (90 lines out, 90 in, over 25 files). Three of the 90 lines held no dead number; each is the other half of a rewritten sentence: `ui/action.zod.ts:400`, `ui/action-param-carryover.test.ts:13` and `ui/expression-bindable-text-keys.test.ts:119`. No code token moves (see the guard below). **No tracker number is added.** Every tracker number on an added line was already on the lines it replaces, and no `PR #N` is added. ## Census: before and after **Instrument.** This is the instrument of stages 1 to 4, rebuilt for this stage. It sends REST `GET /repos/objectstack-ai/objectstack/issues/N` without following redirects, for every distinct number cited in the population. The population is: - the citation gate's own exported `CITATION_RE` and `NON_CITATION_HEADS` at the base, kept when the qualifier is none, `objectstack`, `objectstack-ai/objectstack`, `framework`, `pre-` or `post-`; - matched case-insensitively (`Pre-`, `POST-`, `Framework`); - N of 100 or more, excluding `summon` heads. A qualifier covers only the number it is joined to. 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: 18 of 18 lit (200) and 18 of 18 dead (404) over 6 checkpoints in the base run, and 15 of 15 and 15 of 15 over 5 checkpoints in the head run. | reading | tree | numbers probed | 200 | 404 | 301 or other | dead sites | lines | files | of which comments | of which strings | |---|---|---|---|---|---|---|---|---|---|---| | before | base `487a7846df`, probed 2026-09-29T02:57:01Z to 02:59:36Z | 400 | 372 | 28 | 0 | **111** | 110 | 26 | 90 | 21 | | after | head `83e39641d0`, probed 2026-09-29T03:15:00Z to 03:18:06Z | 383 | 372 | 11 | 0 | **24** | 23 | 8 | 3 | 21 | The head probe found no number newly dead since the base probe: the same 372 numbers answer 200. The head was probed at `83e39641d0`; every census file is byte-identical at the final head. **Cross-check under the grammar that landed during this stage.** PR objectstack-ai#20554 (`199002b3e4`) landed the closed qualifier set while this stage ran, and it reads `objectui PR objectstack-ai#8758` as objectui's number. Re-run with that gate's own `extractCitations` and `namesThisRepository`, the same population reads **108** dead sites before and **21** after, all 21 test strings. The difference is exactly the three `objectui PR objectstack-ai#8758` prose sites below, which that PR's own header measured as "census deaths here that are not deaths at all". **Per file.** Cited sites are every in-repo citation the population reads, live or dead. | file | cited sites (base) | dead before | by class | dead after | |---|---|---|---|---| | `data/filter-subtree-provenance.ts` | 9 | 3 | 3 docblock | 0 | | `meta-spelling/manifest-collection-spelling.ts` | 8 | 2 | 2 line comment | 0 | | `ui/action-param-carryover.test.ts` | 5 | 3 | 2 line comment, 1 string | 1 | | `ui/action.test.ts` | 39 | 3 | 3 line comment | 0 | | `ui/action.zod.ts` | 90 | 7 | 5 docblock, 2 line comment | 0 | | `ui/bulk-action.test.ts` | 9 | 4 | 2 line comment, 2 string | 2 | | `ui/bulk-action.zod.ts` | 9 | 3 | 2 docblock, 1 line comment | 0 | | `ui/component-element-navigation-17987.test.ts` | 8 | 5 | 1 docblock, 4 string | 4 | | `ui/component-type-vocabulary.test.ts` | 8 | 2 | 2 docblock | 0 | | `ui/component-type-vocabulary.ts` | 2 | 1 | 1 docblock | 0 | | `ui/component.test.ts` | 190 | 22 | 11 line comment, 11 string | 11 | | `ui/component.zod.ts` | 265 | 20 | 16 docblock, 4 line comment | 0 | | `ui/dashboard.zod.ts` | 52 | 1 | 1 docblock | 0 | | `ui/expression-bindable-text-keys.test.ts` | 3 | 2 | 2 line comment | 0 | | `ui/expression-bindable-text-keys.zod.ts` | 4 | 2 | 2 docblock | 0 | | `ui/form-select-option.test.ts` | 3 | 1 | 1 docblock | 0 | | `ui/index.ts` | 15 | 2 | 2 line comment | 0 | | `ui/interaction-config-retirement.test.ts` | 19 | 1 | 1 docblock | 0 | | `ui/react-blocks.test.ts` | 9 | 2 | 1 docblock, 1 string | 1 | | `ui/react-blocks.ts` | 17 | 4 | 2 docblock, 2 line comment | 0 | | `ui/view-form-features-root.test.ts` | 4 | 1 | 1 line comment | 0 | | `ui/view-metadata-schema.test.ts` | 31 | 3 | 2 line comment, 1 string | 1 | | `ui/view-submit-redirect-url.test.ts` | 8 | 1 | 1 line comment | 0 | | `ui/view.test.ts` | 136 | 2 | 1 docblock, 1 string | 2 | | `ui/view.zod.ts` | 331 | 13 | 10 docblock, 3 line comment | 2 | | `ui/widget-i18n-retirement.test.ts` | 17 | 1 | 1 line comment | 0 | `ui/` alone went from 106 dead sites in 24 files to 24. The other 107 `ui/` files carry no dead site. ## Per-number table Anchors are 9-hex commit abbreviations. "Wrote" means the commit's own diff added the line being rewritten. | number | comment sites / files | anchor: what it decided | |---|---|---| | `objectstack-ai#5970` | 4 / 2, `action.zod.ts:819`, `action.test.ts:268`, `:304`, `:354` | `97e7e3caa`: `ActionSchema.visible` / `disabled` speak one condition shape; `visible` gains its boolean arm. Stage 1's anchor for the same number | | `objectstack-ai#6276` | 7 / 1, `component.zod.ts:34`, `:165`, `:568`, `:2455`, `:2546`, `:2554`, `component.test.ts:2126` | `78f0be872`: declares `element:record_picker`'s flat `sort` / `limit` on the objectstack-ai#5611 rule (maintainer ruling 2026-08-08, direction A). It wrote `:34`, `:2455` and the "enumerate by the renderer's read pattern" lesson | | `objectstack-ai#8794`, `objectstack-ai#8836` | 3 / 1, `filter-subtree-provenance.ts:130`, `:131`, `:156` | `1850ebbb0`: corrects the reuse-safety claim from the survey and pins the invariant. It wrote `:131` itself. Stages 1 and 3 gave both numbers this anchor | | `objectstack-ai#9933` | 7 / 2, `view.zod.ts:2517`, `:5060`, `:5086`, `:5118`, `:5513`, `view-metadata-schema.test.ts:398`, `:410` | `d5552ca13`: admits `columnState` as an explicitly runtime-only view-overlay key, rejected by name at every authoring door. It wrote "explicitly out of objectstack-ai#9933's scope" | | `objectstack-ai#9972` | 3 / 2 (+1 unread), `component.zod.ts:2213`, `component.test.ts:382`, `:3565`; also `:3612`'s `objectstack-ai#9881/objectstack-ai#9972`, a slash-joined spelling the grammar does not read | `60e0f900a`: records the live read point of `page:tabs` `items[].icon` and its accept-pin. It wrote the `:382` header | | `objectstack-ai#10194` | 1 / 1, `manifest-collection-spelling.ts:71` (`pre-objectstack-ai#10194`) | `2306a765c`: `/meta/theme` and `/meta/analytics_cube` stop storing any JSON as success and validate at the write door. The line now says "the store-anything branch from before commit 2306a76". Stage 1's anchor | | `objectstack-ai#10274` | 6 / 2, `component.zod.ts:2304`, `component.test.ts:308`, `:405`, `:3591`, `:3603`, `:3612` | `d1ba685ec`: re-measures the four pin citations and gates the class. Its gate header records that the re-measure found two anchors wrong since they were written, which is why a refresh re-reads. Stage 3's anchor | | `objectstack-ai#10485` | 4 / 4, `index.ts:51`, `interaction-config-retirement.test.ts:116`, `widget-i18n-retirement.test.ts:112`, `manifest-collection-spelling.ts:67` | `35ad101bc`: retires the `themes` carrier and `ThemeSchema` whole (ruled B, 2026-08-21; ADR-0049 stays cited). Stage 1's anchor | | `objectstack-ai#11284` | 5 / 2, `react-blocks.ts:39`, `:94`, `:109`, `:295`, `react-blocks.test.ts:139` | `5383fa670`: the react tier converges on the metadata-tier vocabulary, deprecate-first. Its changeset heads "(objectstack-ai#11284, maintainer ruling 2026-08-23)" | | `objectstack-ai#11350` | 1 / 1, `index.ts:105` | `ece4dad31`: records the maintainer ruling of 2026-08-23 that a type in an entry's public declarations must be nameable from that entry. Same wording as stage 1's `kernel/index.ts:53` | | `objectstack-ai#11507` | 2 / 2, `component.zod.ts:1465`, `component.test.ts:2726` | `88b9d749a`: declares `sys_activity.type` an open, author-extensible vocabulary (maintainer ruling 2026-08-24, direction 4). Stage 3's anchor | | `objectstack-ai#11658` | 2 / 2, `component.zod.ts:1464`, `component.test.ts:2725` | `1a6a19c31`: opens `RecordActivityProps.types` to author-contributed kinds, executing that ruling. Stage 3's anchor | | `objectstack-ai#11703` | 3 / 2, `action.zod.ts:399` to `:400`, `:460`, `action-param-carryover.test.ts:12` to `:13` | `5cb62d88b`: `clone_permission_set` carries all five copied facets; its params list had silently dropped three. The lines now name "the silent-drop shape commit 5cb62d8 fixed" | | `objectstack-ai#11753` | 5 / 2, `action.zod.ts:66`, `:390`, `:398`, `:409`, `action-param-carryover.test.ts:1` | `0e4e51b0a`: `ActionParamSchema.carryOver`, the spec half of the 2026-08-25 maintainer ruling (recommendation A). It wrote every one of these lines, and its changeset records the `visible: false` measurement `:398` names | | `objectstack-ai#12194` | 1 / 1, `view.zod.ts:4838` | `311433f6b`: declares the metadata item-name grammar (`QUALIFIED_ITEM_NAME_PATTERN` among it) and refuses it at the publish door. Stage 2's anchor | | `objectstack-ai#12868` | 5 / 2, `view.zod.ts:2938`, `:2966`, `:3179`, `:7087`, `form-select-option.test.ts:4` | `c459da6bc`: narrows the per-option `default` key out of the form-view options vocabulary. Its changeset records the ruled census `:2966` cites ("measured ZERO occurrences"). Stages 3 and 4's anchor | | `objectstack-ai#12950` | 3 / 2, `component-type-vocabulary.ts:4`, `component-type-vocabulary.test.ts:4`, `:101` | `225e7690f`: created `component-type-vocabulary.ts`; its message records the readiness read `:101` pins (`global:search` and `global:notifications` stay declared) | | `objectstack-ai#13156` | 2 / 2, `view-form-features-root.test.ts:70`, `view-submit-redirect-url.test.ts:110` | `fd289be45`: strips tracker ids from function-declaration-built refusal prose. It wrote both lines. Stage 3's wording ("commit fd289be's strip") | | `objectstack-ai#13670` | 1 / 1, `expression-bindable-text-keys.zod.ts:72` | `8c6a7fc0b`: records `text.value` as deliberately omitted; its message states the ruling that `text`'s evaluation channel is `content` alone | | `objectstack-ai#13672` | 3 / 2, `expression-bindable-text-keys.zod.ts:89`, `.test.ts:65`, `:118` | `e854a531a`: narrows the `button` row to the spelling its key reaches, and records `action:button` and `ui:button` as deliberately out | | `objectstack-ai#16626` | 1 / 1, `component.test.ts:2336` | `30b099078`: the objectui pin bump to `53ded82bf7a4` that ships objectui#7754's array-analytics lowering, the door the family waited on. The association is PR objectstack-ai#16788's body (it names objectstack-ai#16626 as the card it lands), and `30b099078` is that PR's merge commit; neither its message nor its diff names objectstack-ai#16626 (the stage-3 objectstack-ai#11065 precedent) | | `objectstack-ai#17987` | 9 / 2, `component.zod.ts:10`, `:4014`, `:4062`, `:4153`, `:4193`, `:5068`, `:5226`, `:5457`, `component-element-navigation-17987.test.ts:4` | `e233db9db`: declares element-level `navigation` on `object-kanban` / `object-calendar` and gives `object-timeline` its `ComponentPropsMap` row, executing the objectui#8652 ruling (verbatim `B`) | | `objectstack-ai#18003` | 1 / 1, `dashboard.zod.ts:686` | **ADR-0087**, the rung above a commit. The line quoted the ADR's own amendment heading, number included. It now reads "(ADR-0087, its 2026-09-13 amendment, 「the level half」)": the ADR stays the citation and the fragment it quotes is verbatim | | `objectstack-ai#18177` | 5 / 2, `bulk-action.zod.ts:51`, `:169`, `:262`, `bulk-action.test.ts:61`, `:307` | `adabccf5f`: `BulkActionParamSchema` is strict and declares `dependsOn`, executing decision batch objectstack-ai#146 item 4, letter A | | `objectstack-ai#6111` | 1 / 1, `view.zod.ts:3634` | respelled `objectui#6111` (not re-anchored): it is objectui's number | | `objectstack-ai#8758` | 2 / 1, `component.zod.ts:3479`, `:4140` | respelled `PR objectui#8758` (not re-anchored): objectui's PR objectstack-ai#8758, merged 2026-09-09 | No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of the 25 re-anchored numbers except objectstack-ai#18003. ADR-0126 mentions objectstack-ai#11703 and objectstack-ai#11753 only as references ("permission-set precedent"), not as the record of either decision. **Anchor checks.** Every sha on an added line is one of 23, and none is on a removed line. At the base `487a7846df`: - each matches exactly one object (`git rev-parse --disambiguate`, count 1, 23 of 23); - each is an ancestor (`git merge-base --is-ancestor`, exit 0, 23 of 23); the control leg `e9584681a4` also exits 0, and the repository is not shallow; - for 22 of the 23, a grep of the commit's own message or diff finds the number it replaces (the message for 16; the diff for `0e4e51b0a`, `5383fa670`, `c459da6bc`, `225e7690f`, `e854a531a`, and for objectstack-ai#8836 in `1850ebbb0`). `30b099078` is the exception explained in the table. - Each commit was read for the rule its line states, not only for the number. In most cases the commit wrote the very line it now anchors. Wordings to check, each true of its commit: - `filter-subtree-provenance.ts:130` and `:156` read 「survey commit 1850ebb records」: the survey was the card's, and the commit's message records its measurement. It is stage 3's wording for the same relation (「from the survey it records」). - `component.zod.ts:1465` and `component.test.ts:2726` read 「maintainer ruling commit 88b9d74 declared」: that commit landed the ruling (direction 4) as the `sys_activity.type` declaration. - `manifest-collection-spelling.ts:71` reads 「the store-anything branch from before commit 2306a76」: before that commit, `PUT /meta/theme/:name` stored any JSON as success. ## Mechanical guard: no code token moves The check compares leaf tokens with comments stripped, base `487a7846df` against the head. It uses the TypeScript parser's leaf tokens from the head's lockfile, so template literals are scanned in context, and it excludes JSDoc nodes. It ran over all 25 touched `.ts` files, and every control mutates the head text in memory only. - Real run: 101,836 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control (`ui/index.ts`): 0 files changed (exit 0). - Positive control (a declaration inserted into `ui/view.zod.ts`): 1 file reads DIFFER at token 19222 (exit 1). - Positive control (one digit changed in a `component.test.ts` test title): 1 file reads DIFFER at token 14972 (exit 1). Line balance holds in every file, 90 out and 90 in over the 25, and every line count is equal at base and head. Tracker numbers: added-not-removed is empty in every file. The net-removed numbers are the 25 in the table, 85 sites: the census's 84 comment sites, plus the slash-joined `objectstack-ai#9972` at `component.test.ts:3612`. ## Generated page `check:generated` proved one artifact stale: `content/docs/references/ui/expression-bindable-text-keys.mdx`, the projection of `expression-bindable-text-keys.zod.ts`'s module docblock. `check:generated --fix` regenerated only that page, and a re-run read `All 15 generated artifacts are up to date`. Its two changed lines are the `:72` and `:89` substitutions verbatim. No other docblock here projects into a reference page, and nothing under `skills/**` moved. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/spec` is included. It says only that the provenance comments were re-anchored. `Clause-②: no`: no export, key, value or type moves (the guard above). Measured on the head's built package: 6 touched sources are `src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten comments also reach `dist`: - `c459da6bc` appears in 32 bundled `.js` files and 2 `.d.ts`; - `adabccf5f` in 24 `.js` and 2 `.d.ts`; `d5552ca13` and `0e4e51b0a` in 24 `.js` each; `e233db9db` and `78f0be872` in 2 `.js` and 2 `.d.ts` each; - the positive control, the pre-existing sentence 「the object-field face enforces」, appears in 32 files. ## Gates (head `1b885d3c27`) - **Citation judging pass, run as CI runs it:** `pnpm check:issue-citations && node scripts/check-issue-citations.mjs`, both under the grammar PR objectstack-ai#20554 landed, exit 0. The self-test passes 114 cases in 8 batteries. The live, diff-scoped run judged 13 citations across 11 files: 3 resolve and 10 are declared cross-repo references. It reads "every citation this change adds resolves". - **Doc authoring:** `pnpm check:doc-authoring` exits 0. Its 16,759 customer-facing strings across 1,174 spec sources carry no internal issue id, and the sibling-package prose-id baseline holds with no growth. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at this head derived 112 families, and all 112 exit 0. `--ran` reports 112 run, 0 NOT MEASURED, 0 unrun. A full `turbo run build` of `./packages/*` at this head ran first, under the shared verify lock: 71 of 71 tasks, VERDICT command-exit 0. So no gate met an unbuilt prerequisite. - **Five roster gates the derivation flags for this diff** (their rosters sit in `.changeset/` or `packages/`, so their silence proves nothing): `node scripts/check-changeset-fixed.mjs`, `pnpm --filter @objectstack/spec run check:spec-changes`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`. All exit 0. - `pnpm --filter @objectstack/spec run check:generated` (derived) reads `All 15 generated artifacts are up to date`, and `check:docs` reads `226 generated files in sync`. - **Tests and typecheck, under the lock:** - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui src/meta-spelling`: Test Files 98 passed (98), Tests 3452 passed (3452), VERDICT command-exit 0 (the chain held the lock 142s on a shared box). - The 16 spec suites outside `src/ui` that read the touched files' source text or pin their lines: Test Files 16 passed (16), Tests 489 passed (489). They are `scripts/{export-origins,file-description,root-index,schema-closure,skill-map-guards,strictness-ledger}.test.ts`, `src/ai/tool-confirmation-prescription-tense.pin.test.ts`, `src/api/api-entry-graph.pin.test.ts`, `src/contracts/scoped-context.test.ts`, `src/data/filter-subtree-provenance.test.ts`, `src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence,union-author-message-pins}.test.ts`, `src/system/constants/platform-object-names.test.ts` and `src/type-alias-convention.pin.test.ts`. Three more suites matched the reader scan and are not run here: `scripts/build-schemas-check-mode.test.ts` only imports `ViewItemSchema` (code the guard proves unchanged) and rebuilds schemas in a temp tree; `scripts/def-key-collisions.test.ts` names `ui/view.zod.ts` only in a comment; `scripts/published-projection-choke-point.test.ts` matched on `build-react-blocks-contract.ts`, not a touched file. They are left to CI. - `pnpm --filter @objectstack/spec typecheck`: exit 0, including `check:test-typecheck` (53 files, 251 errors, 138 pinned signatures held). The same three runs also passed, with the same counts, on the pre-merge tree. - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 25 touched `.ts` files gives 25 files, 0 errors and 0 warnings. All 25 are in eslint's own population (`isPathIgnored` is false for each, read through eslint's API). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, which its own lines 327 to 328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Merge probe:** a `merge-tree` of the head onto `origin/main` `f572a7eb3c`, from a bare shared clone with no merge driver registered, exits 0 (2026-09-29T04:15Z). ## Acceptance notes - **Base and merge.** The branch forked from `487a7846df`, one commit past the claim's stamp `6154165484` (PR objectstack-ai#20551, outside the surface). `origin/main` then moved three commits, and `199002b3e4` (PR objectstack-ai#20554) changed `scripts/check-issue-citations.mjs`, so the gate derivation read STALE TREE. `origin/main` `dee9b26f6c` was merged in (`1b885d3c27`): a clean merge with no driver-routed path and no lockfile change, touching none of this diff's files. The PR's delta against `dee9b26f6c` is exactly its 27 files. `origin/main` has since moved one more commit, `1c761c0d71` (PR objectstack-ai#20565, tests in two other packages), which touches none of them. - **One site beyond the hand-over's list.** The hand-over named `manifest-collection-spelling.ts:71` (`pre-objectstack-ai#10194`). The same comment's first line, `:67`, cites `objectstack-ai#10485`, which also answers 404, and it is rewritten too. The claim names this file; the fix is the same defect class, mechanical in the form stages 1 to 4 fixed, in a file no other claim holds, under the same gates. Reverting it would be one line. - **Open PRs, re-read at 2026-09-29T04:22Z:** 8 open PRs, and none touches any file in this diff. The one that touches `ui/` is objectstack-ai#20570 (objectstack-ai#20446's), on the excluded `view-grouping-query.ts`. The in-flight `Claim:` comments on the 12 `pm:dispatched` cards were read too: only objectstack-ai#20446's names a `ui/` file (`view-grouping-query.ts`, excluded above). - **Hypothesis 2, measured.** In `ui/`, 14 sibling-qualified pairs leave the second number bare: 13 on one line (`+`, `and`, `,`, `/` or `'s PR` between them) and `component.zod.ts:3478` to `:3479`, split across a line break. In 3 of them the second number answers 404 here and the sentence attributes it to objectui (`objectstack-ai#6111` once, `objectstack-ai#8758` twice); they are respelled above. In the other 11 the second number answers 200 here, so it is judged as this repository's and left: `action.zod.ts:1603`, `component.test.ts:2052`, `:2199`, `:2315`, `component.zod.ts:3276`, `:3793`, `:4812`, `expression-bindable-text-keys.zod.ts:33`, `page.test.ts:696`, `react-blocks.ts:256` and `widget.zod.ts:34`. The PR objectstack-ai#20554 header measured `,` and `and` pairs as naming this repository's number and `/` pairs as mostly, but not always, the qualifier's. Whether any `/` pair here names objectui's number is not measured; a 200 here cannot tell. - **What stays in this population: 24 dead sites** (21 under the landed grammar). - **21 test strings**, left as tokens (vitest `it` / `describe` titles in 7 test files): `objectstack-ai#6276` ×6, `objectstack-ai#11658` ×3 and `objectstack-ai#11507` ×1 in `component.test.ts`; `objectstack-ai#9972` in `component.test.ts:411`; `objectstack-ai#17987` ×4 in `component-element-navigation-17987.test.ts`; `objectstack-ai#18177` ×2 in `bulk-action.test.ts`; `objectstack-ai#9933` in `view-metadata-schema.test.ts:406`; `objectstack-ai#11284` in `react-blocks.test.ts:147`; `objectstack-ai#11753` in `action-param-carryover.test.ts:17`; `objectstack#11195` in `view.test.ts:3396`. None is a Zod `.describe()` text, an exported string or a migration-entry field, so no form D site arises here. - **3 comments that name objectui's live PR objectstack-ai#8758 in prose** (`objectui PR objectstack-ai#8758`): `view.zod.ts:2354`, `:2579` and `view.test.ts:426`. They are not pairs, and the landed grammar reads them as objectui's. - **Left for later stages of objectstack-ai#20234** (the stage-4 landing comment 5882686893's list, unchanged): the migrations area, the `liveness/**` notes, the `why` strings and the `PROVENANCE_WAIVERS` reason, `rest-server.zod.ts`, the held `analytics*` and `driver/turso.*` files, the 2 `AGGREGATION_CASES` note strings, and `data/`'s test strings and deliberate markers. - **The same rot outside `packages/spec/src`** is objectstack-ai#20556's, not this card's. Examples met here: ADR-0087's own amendment heading (`docs/adr/0087-metadata-protocol-upgrade-contract.md:390`, `:397`) cites the dead `objectstack-ai#18003`, and ADR-0126 cites `objectstack-ai#11703` and `objectstack-ai#11753`; both are governed. `packages/spec/scripts/strictness-ledger.test.ts:375`, `:380` cite `objectstack-ai#9933`, and `check-objectui-pin-citations.ts` cites `objectstack-ai#10274` and `objectstack-ai#9972`. - **Unchanged wording.** `react-blocks.test.ts:140` says the 2026-08-23 ruling was "recorded on-card". The card is gone, and the changeset of `5383fa670` (now cited on `:139`) records the ruling. The line holds no number, so it is left. - **The citation gate's reach.** It defers `packages/**/*.test.ts`. The 11 touched non-test files are in its judging population. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…ecision in words instead of a tracker number (stage 20) (objectstack-ai#21895) Part of objectstack-ai#20749 Clause-②: no Stage 20 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the first name-ordered `ui/` group: the 32 test files directly under `packages/spec/src/ui/` from `action-confirm-params-guard.test.ts` to `component-record-block-field-security.test.ts`. Those files carried 97 messages and 102 tracker ids, citing 65 records. 101 of those ids now either state what their record decided, in words (form D), or are dropped where the title already says it. One id stays, because an assertion in the same file reads it (below). Text only: no assertion, identifier, test count or code comment changes, and no file is renamed. ## Census at the base (`e085a8c3be`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages 10 to 19 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. The base is `e085a8c3be`, stage 19's landing and the claim's base. Both instruments read **860 messages / 908 ids in 190 files**, the seat's reading and stage 19's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `ui/` (this PR: 32 of the 84 files) | 84 | 396 / 419 | 378 / 401 | 18 / 18 | | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **190** | **860 / 908** | **804 / 851** | **56 / 57** | The group reads **97 messages / 102 ids in 32 files**, the seat's figures file for file: | file (under `ui/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `action-confirm-params-guard.test.ts` | 4 / 4 | 4 / 4 | 0 | | `action-description.test.ts` | 3 / 3 | 3 / 3 | 0 | | `action-dispatch-contract.test.ts` | 4 / 4 | 4 / 4 | 0 | | `action-doubled-redirect.test.ts` | 1 / 1 | 1 / 1 | 0 | | `action-newtaburl-pair.test.ts` | 1 / 1 | 1 / 1 | 0 | | `action-on-success.test.ts` | 3 / 4 | 3 / 4 | 0 | | `action-param-carryover.test.ts` | 1 / 2 | 1 / 2 | 0 | | `action-param-default-value.test.ts` | 3 / 3 | 3 / 3 | 0 | | `action-params.test.ts` | 9 / 9 | 9 / 9 | 0 | | `action-requires-confirmation-docblock.pin.test.ts` | 5 / 6 | 3 / 4 | 2 / 2 | | `action-row-update.test.ts` | 5 / 5 | 5 / 5 | 0 | | `action.test.ts` | 12 / 12 | 12 / 12 | 0 | | `app-nav-expanded-alias.test.ts` | 1 / 1 | 1 / 1 | 0 | | `app-nav-target-exclusivity-export.test.ts` | 1 / 1 | 1 / 1 | 0 | | `app-strictness-batch19.test.ts` | 1 / 1 | 1 / 1 | 0 | | `app.test.ts` | 9 / 9 | 9 / 9 | 0 | | `aria-carrier-tombstones.test.ts` | 1 / 1 | 1 / 1 | 0 | | `assembled-view-artifact-type.test.ts` | 1 / 1 | 1 / 1 | 0 | | `assembled-views.test.ts` | 1 / 1 | 1 / 1 | 0 | | `bulk-action.test.ts` | 4 / 4 | 4 / 4 | 0 | | `calendar-config-allday-prescription-17054.test.ts` | 2 / 2 | 2 / 2 | 0 | | `chart-aggregate.test.ts` | 1 / 1 | 1 / 1 | 0 | | `chart.test.ts` | 7 / 9 | 7 / 9 | 0 | | `component-action-element-rows-20371.test.ts` | 1 / 1 | 1 / 1 | 0 | | `component-action-row-endpoint-21005.test.ts` | 1 / 1 | 1 / 1 | 0 | | `component-element-navigation-17987.test.ts` | 4 / 4 | 4 / 4 | 0 | | `component-form-custom-fields-sections-typed.pin.test.ts` | 1 / 1 | 1 / 1 | 0 | | `component-object-grid-default-filters.pin.test.ts` | 1 / 1 | 1 / 1 | 0 | | `component-object-grid-export-options-members.pin.test.ts` | 1 / 1 | 1 / 1 | 0 | | `component-object-grid-pagination-accept-set.pin.test.ts` | 1 / 1 | 1 / 1 | 0 | | `component-props-unknown-members.pin.test.ts` | 2 / 2 | 1 / 1 | 1 / 1 | | `component-record-block-field-security.test.ts` | 5 / 5 | 5 / 5 | 0 | | **32 files** | **97 / 102** | **94 / 99** | **3 / 3** | The three "other" strings are `action-requires-confirmation-docblock.pin.test.ts:168` and `:175` (two `expect` messages) and `component-props-unknown-members.pin.test.ts:322`. The claim calls the third one an `expect` message too; it is the `ruling` value of a ledger entry. 12 more test files in the same name range carry no id and are not touched. - **Controls.** Lit: `ui/component.test.ts`, outside the group, reads 70 ids at the base and at the head. Dark: `action-requires-confirmation-docblock.pin.test.ts` reads 0 at the head while 4 of its comment lines still carry a number. Planted in scratch copies of head files: an id put into an `app-nav-expanded-alias.test.ts` title reads 1 / 1, and an id put into a `bulk-action.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same as the gate pattern in 29 of the 32 files at the base. The other three differ only by 16 hex colour literals (`'#0070F3'` in `app.test.ts`, `'#8B5CF6'` in `bulk-action.test.ts`, the `colors` arrays in `chart.test.ts`), which this PR does not touch. - **At the head:** 764 messages / 807 ids in 159 files. The 32 files read 1 / 1 (the kept `:322`), `ui/` reads 300 / 318, and no other file moved. ## How the area was chosen `ui/` has no subdirectory, so it is taken like `data/`, in name-ordered file groups near the ~100-id bound. Stage 19's re-cut named this group at 102 ids, and this census reads 102, so no re-cut was needed. **Named for the next stages** (cut from the head census, 764 / 807): - `ui/` 318 ids in 53 files, about three stages. The next group nearest 100 runs from `component-record-blocks.test.ts` to `dashboard.test.ts`: 7 files, 103 messages / 109 ids (98 titles / 103 ids, 5 other). `component.test.ts` alone carries 70. Cutting one file earlier gives 88. That group holds five "other" strings: `dashboard-chart-structure-refusal.test.ts:94` (two ids) and `dashboard.test.ts:124` (two ids), which read like placeholders (`objectstack-ai#111`, `objectstack-ai#222`), and `dashboard.test.ts:205` (objectstack-ai#5022). - `api/` 201, two stages. `system/` 165, two. The files directly in `src/`, 120, one. - The three docblock needles (`ai/build-progress.test.ts` ×2, `contracts/approval-service.test.ts` ×1), one stage, with an at-tier review. The id this PR keeps (`component-props-unknown-members.pin.test.ts:322`) fits there too. ## The three "other" strings: two rewritten, one kept - **`action-requires-confirmation-docblock.pin.test.ts:168` and `:175` are not needles.** Each one is the failure message (the second argument of `expect(value, message)`) of an assertion whose expected value carries no id: `.toEqual([])` over the docblock's positive `confirmText` claims, and `.not.toMatch(/confirmText/)` over the classifier's body. Nothing compares the message text. Both are rewritten and declared to the text-only tool. - **`component-props-unknown-members.pin.test.ts:322` is kept.** Its `ruling: 'decision card objectstack-ai#21704, fork 4, letter B (record 5979239990)'` is the expected value of the assertion at `:417` in the same file, `expect(reason.kind === 'opaque' && reason.ruling, key).toMatch(/objectstack-ai#21704/)`, and the `ruling` slot is typed as naming the record that holds the member (`:188`). Removing the id turns `:417` red, and moving that regex would change assertion text, which this stage does not do. It is not a docblock needle, since it reads a value in its own file, but it is held the same way. It is reported for the needle stage. ## What each id became - **36 literals (41 ids)** now state a decision in words. - **13 literals (13 ids)** get their subject back in words, where the number stood for a thing, such as "the objectstack-ai#7428 pair rule". - **47 literals (47 ids)** drop a number the title already explains. Every cited record was read with its comments through REST: 62 answer 200 and 3 answer 404. Three citations are cross-repo (`objectui#3139`, `objectui#3382`, `objectstack-ai/objectui#11610`); all three were read from objectui and answer 200. Where a record answers 404, or answers 200 without the decision, the decision was read from what landed: - **objectstack-ai#11753** (404): its ruling, recommendation A, is quoted in its spec-half card objectstack-ai#11992. - **objectstack-ai#17987** (404): the landing commit `e233db9dbb` ("declare element-level `navigation` on object-kanban / object-calendar and give object-timeline its ComponentPropsMap row"), executing objectui#8652's ruling B. - **objectstack-ai#18177** (404): the landing commit `adabccf5fb` ("BulkActionParamSchema is strict and declares dependsOn"), decision batch objectstack-ai#146 item 4, letter A. - **objectstack-ai#3896** (200): the record is a sharing-rule REST defect, closed with no comment. The "close-out" the title names is what landed under that citation: the `action-inert-keys-removed` conversion in `conversions/registry.ts` ("capability claims nothing enforced") and the `shortcut` / `bulkEnabled` tombstones in `ui/action.zod.ts`. - **objectstack-ai#3701** (200, closed with no comment): the convention as `ui/chart-aggregate.ts` writes it down. **Stated in words:** | record(s) | literal (under `ui/`) | now reads | the decision | |:--|:--|:--|:--| | objectstack-ai#7278 | `action-confirm-params-guard.test.ts:148` | "… — the one-dialog shape the confirm question migrated TO" | Maintainer option 1: drop `confirmText` and carry the question as the param dialog's description, one decision in one dialog. | | objectstack-ai#7367 | `action-description.test.ts:35` | "ActionSchema.description — the line the param dialog shows" | `description` joins the action contract, shaped like `label`, as the param dialog's description line. | | objectstack-ai#17319 | `action-dispatch-contract.test.ts:43`, `:77` | "… (and still true: the declared dispatch contract is an authoring key)"; "an action's dispatch contract speaks `bulkActionDefs`' own vocabulary" | Ruling A: an action declares its bulk dispatch contract in the bulk def's own `execution` vocabulary, and a view that wires it the other way is refused at validate. | | objectstack-ai#11519 | `action-doubled-redirect.test.ts:24` | "ActionSchema — doubled post-success navigation is refused, with no precedence field" | Refuse the doubled channel; no `precedence` field. | | objectstack-ai#9566, objectstack-ai#9474 | `action-on-success.test.ts:13` | "ActionSchema.onSuccess — one closed post-success navigation key for api and script actions" | Ruled together: one closed `onSuccess` key (`navigate` + `openIn`) for both types. | | objectstack-ai#4352 | `action-on-success.test.ts:141`; `action.test.ts:1365` | "type scope — api and script only, refused on any other type, never silently ignored"; "… the publish gate resolves to it, so a `body` off `script` is refused at publish" | Outlet 1: the runtime follows the spec, and contradictory type-scoped metadata errors at publish instead of being ignored. | | objectstack-ai#11992, objectstack-ai#11753 | `action-param-carryover.test.ts:17` | "ActionParamSchema.carryOver — seeded from the row, shown read-only, submitted verbatim" | Recommendation A on objectstack-ai#11753, executed by objectstack-ai#11992. | | objectstack-ai#20740 | `action-param-default-value.test.ts:250` | "… refuses a zone-suffixed `time` value — a time of day is a zone-less wall clock" | `ClockTimeValueSchema` narrowed to the zone-less wall clock. | | objectstack-ai#5568 | `action-params.test.ts:139` | "names `_selectedIds` when the caller sent `selectedIds` — the declared channel for a selection" | Verified and closed: `params._selectedIds` is the working declared channel; the reported gap was not one. | | objectstack-ai#5613 | `action-params.test.ts:294` | "accepts the DUAL-EMIT shape the runtime emits through the rename window — …" | Contract first, then `roles` → `positions` with a deprecation window in which the runtime emits both keys. | | objectstack-ai#7828, objectstack-ai#13865 | `action-requires-confirmation-docblock.pin.test.ts:128`, `:150`, `:168`, `:172`, `:175` | "… docblock names only the declared signals the classifier reads"; "would flag the retired sentence …"; "… re-seeds the retired leg — only declared semantics classify" (message); "… still does not read `confirmText` — only declared semantics classify"; "… reopen the declared-semantics ruling before the docblock …" (message) | objectstack-ai#7828 option A: `actionLooksDestructive` classifies on declared semantics only (`mode`, `variant`), never on UI copy. objectstack-ai#13865 retired the docblock sentence that still named `confirmText`. | | objectstack-ai#3405 | `action.test.ts:85`, `:141` | "inline lookup reference target — `reference`, the FieldSchema spelling"; "… at `reference`, the one key an author writes" | `ActionParamSchema` gains `reference`, named as `FieldSchema.reference` so the spelling authors already write is the legal one. | | objectstack-ai#15811 | `action.test.ts:217` | "… at the SLOT, which needs a `source` to evaluate, not at the lowering" | Ruling A: every engine-evaluated expression slot requires a non-blank `source`. | | objectstack-ai#20323 | `action.test.ts:1116` | "Action ARIA Integration (retired — no action surface ever read it)" | Retire `action.aria`: no action surface reads it. | | objectstack-ai#6888 | `action.test.ts:1513` | "`global_nav` is retired — it rendered nowhere" | Direction 2: retire `global_nav`, which rendered nowhere in the running app. | | objectstack-ai#3896 | `action.test.ts:1612` | "audit close-out — retired shortcut/bulkEnabled, capability claims nothing enforced" | Read from what landed (above). | | objectstack-ai#5016 | `action.test.ts:1650` | "action param option vocabulary — declared only where a renderer delivers it" | Ruling B, on condition that it lands with the renderer and after a per-key liveness audit; `visibleWhen` is declared, `color` / `default` / `icon` / `disabled` stay refused. | | objectstack-ai#4001 | `app-strictness-batch19.test.ts:99` | "批 19, unknown keys refused — the `verify` check …" | Every authorable surface goes strict; spelled as stage 18 spelled the 批 20 titles. | | objectstack-ai#5320 | `assembled-views.test.ts:51` | "AssembledViewArtifactSchema — the declared home for non-container view artifacts" | Fork ruling B: a declared, portable home for non-container view artifacts. | | objectstack-ai#4457 | `bulk-action.test.ts:20` | "BulkActionDefSchema — the def shape is typed, not `z.any()`" | Type the def that was `z.record(z.any())`. | | objectstack-ai#17054 | `calendar-config-allday-prescription-17054.test.ts:106` | "what declaring `allDayField` did NOT open" | Ruling A: `CalendarConfigSchema` declares `allDayField`. | | objectstack-ai#3701 | `chart-aggregate.test.ts:45` | "result-column naming convention — rows keyed by the raw field names" | Read from what landed (above). | | objectstack-ai#17751 | `chart.test.ts:271` | "Chart ARIA Integration — retired, no renderer ever applied it" | Retire `ChartConfigSchema.aria`. | | objectstack-ai#4001, objectstack-ai#5583 | `chart.test.ts:476` | "批 15 — the two chart sites left open on a measurement, since CLOSED as strict objects" | objectstack-ai#5583: both schemas became strict objects. | | objectstack-ai#5022 | `chart.test.ts:631` | "ChartDrillDownSchema — the honest subset, every key one ObjectChart reads" | Declare `drillDown` with only the keys `ObjectChart` reads. | | `objectui#3382`, objectstack-ai#5435 | `chart.test.ts:721` | "target: 'navigate' is ACCEPTED — the chart renderer delivers it now" | `ObjectChart` gained `'navigate'`, so the union gained the member. | | `objectui#11610` | `component-form-custom-fields-sections-typed.pin.test.ts:496` | "§5 the grid widget's eight field-level keys, camelCase since objectui renamed them" | objectui renamed the eight keys to camelCase with no dual read. | | objectstack-ai#19046 | `component-object-grid-pagination-accept-set.pin.test.ts:172` | "both arms refuse a … pageSize — the view and component arms no longer disagree" | The grid component arm refuses the page sizes the view arm refuses. | | objectstack-ai#18159 | `component-record-block-field-security.test.ts:174` | "`requiredPermissions` is declared on the three blocks as a capability set (instruments A and B)" | Ruling A: the key follows the objectstack-ai#19186 ruling, an ADR-0066 capability set. | **Subject back in words** (13 literals): "objectstack-ai#7428 —" becomes "the `confirmText` + `params` guard" or "the pair guard" (`action-confirm-params-guard.test.ts:130`, `:171`), and "the objectstack-ai#7428 pair rule" becomes "the `confirmText` + `params` pair rule" (`action-row-update.test.ts:280`); "the surfaces objectstack-ai#7367 …" becomes "the surfaces the action `description` key …"; two `objectstack-ai#17319 —` prefixes become "`execution` on an action" and "dispatch-contract default"; "the pre-existing probes (objectstack-ai#9474)" becomes "the spellings tried before `onSuccess` existed"; "the bare filter objectstack-ai#14175 declared" becomes "the bare filter the facade was first typed with"; "objectstack-ai#14092 — boundaries" becomes "the declarative row update — boundaries"; "objectstack-ai#17631's shape" becomes "the never-parsing gate it once built"; "§6 objectstack-ai#21464 is closed out" becomes "§6 the `z.unknown()` member sweep is closed out"; and two `objectstack-ai#18159 —` prefixes become "the three record blocks" and "the field-security pair". **Dropped where already stated** (47 literals, 47 ids). A number goes only where the title already says its decision. Examples: "ActionSchema — newTabUrl requires opensInNewTab: true (objectstack-ai#11842)"; "ActionSchema — the `execute` alias is REMOVED (objectstack-ai#3855)"; the `objectstack-ai#15124 —`, `objectstack-ai#15117 —` and `objectstack-ai#5779 —` prefixes, each in front of the rule it names; the four `[objectstack-ai#17987]` prefixes on `component-element-navigation-17987.test.ts`; "— the shape is closed, so its accept means something (objectstack-ai#18177)"; "retired fail-open area gates (objectstack-ai#4651)". `(ADR-0049)`, `(ADR-0066 D4)` and `(ADR-0078)` stay: they cite decision records by number, not tracker ids. **No file is renamed.** Four file names carry a number (`calendar-config-allday-prescription-17054`, `component-action-element-rows-20371`, `component-action-row-endpoint-21005`, `component-element-navigation-17987`); they are not this card's. ## Readers - **Test-name filters:** none. No tracked script, workflow or package config passes `-t` / `--testNamePattern`. - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`, and none of the 32 files calls a snapshot matcher. - **Projects:** one of the 32 files runs in the `repo` project: `action-requires-confirmation-docblock.pin.test.ts` is listed in `packages/spec/vitest.repo-tests.json`, because it reads `packages/runtime/src/action-execution.ts`. The other 31 run in `local`. - **By substring:** every old literal, plus a window around each id (290 needles), was searched with `git grep` at the base, across the tracked tree outside its own file. No gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 11 hits: - 7 code comments in `ui/component.zod.ts` citing `objectstack-ai/objectui#11610`, and one in `ai/agent.test.ts:193` ("objectstack-ai#3896 close-out"); - one release-owned line, `content/docs/releases/v17/17-0.mdx:326`; - one sibling title in `runtime` (`action-engine-facade-find-envelope.test.ts:80`, "objectstack-ai#15124 — …"). None reads a spec test title. - **The files by name:** a few gates and ledgers name group files by path, never by title: `scripts/check-parse-guard.mjs` reads a code line of `app.test.ts`, `packages/spec/test-typecheck-debt.json` keys `app.test.ts` and `chart.test.ts` on error signatures, and `vitest.repo-tests.json` lists the docblock pin. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. This stage declares two lines, `action-requires-confirmation-docblock.pin.test.ts:168` and `:175`. - **Result:** 32 of 32 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 96 changed string leaves in 96 literals: 94 titles and 2 declared. The diff's `+` and `-` lines are exactly the 96 planned lines, and every file keeps its line count. - **Controls (12 of 12 as predicted, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; an `it.each` row given an id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF; a declared message given an id back VIOLATION; the kept `:322` ruling value edited VIOLATION. - **Templates and tables:** the one template title (`component-object-grid-pagination-accept-set.pin.test.ts:172`) changes only after its `${label}` span. No `.each` title, `%s` / `$name` placeholder or table row changes. **Test counts:** the 32 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 959 tests in 32 files, all passed, with the same count and status sequence per file in 32 of 32. 513 full test names change, and no full name repeats on either side. Each changed name equals the base name with the planned replacements applied. The comparison script flags one name: its plan entry spells `’` as the source does, and the printed name carries the decoded character. With the escape decoded, that name matches too. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 32 touched files are in it, and no `*.test.ts` at all. The controls `src/ui/action.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old literal each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `fa39bcf62e`) - `pnpm turbo run build` over all packages: 71 / 71, through the shared verify lock (`VERDICT command-exit 0`). - `@objectstack/spec`: - `vitest run --project local`: 616 files, 18426 passed, 1 todo. - `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 32 touched files, counted with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date, against the `dist/` the build above wrote. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stage 19, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 32 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 32 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 192 changed lines (+96 / -96). - A control-byte scan over the 32 files finds none. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was two commits past the base (`67c544ccca`: objectstack-ai#21887, objectstack-ai#21891). Neither touches any of the 32 files, and neither touches `packages/spec`: their 9 files are in `service-datasource`, `qa/dogfood`, the QA checklist and one changeset. So `main` was not merged. `git merge-tree` onto `67c544ccca` is clean. None of the 13 open PRs touches the 32 files. ## Acceptance notes - **The kept id** (`component-props-unknown-members.pin.test.ts:322`, read by `:417`) is held for the needle stage, as above. - **Same-id test titles in this card's later stages** go with those stages. 30 lines in `packages/spec/src` cite ids this PR handled, for example `api/api-error-code-type.test.ts:71` ("[objectstack-ai#19920] …"), `system/job.test.ts:836` ("retired job.id (objectstack-ai#4667)"), `ui/view-strictness-batch18.test.ts:91` ("objectstack-ai#4001 批 18 — …"), `ui/view.test.ts:3144` ("(objectstack-ai#3896 close-out)") and `ui/component.test.ts:3518` ("(objectstack-ai#19514)"). `ui/view.test.ts:3386` cites `objectui#5435`, a different record. - **Same-id test titles in other packages** are their lanes' test-string shares. A search of `describe` / `it` / `test` lines outside `packages/spec/src` finds 44 lines citing ids this PR handled, in 11 packages: `lint` 16 (6 files), `runtime` 9 (6), `cli` 5 (3), `plugin-approvals` 3 (2), `spec/scripts` 3 (2), `objectql` 2 (2), `plugin-security` 2 (2), and one each in `plugin-sharing`, `qa/dogfood`, `rest` and `service-automation`. Examples: `runtime/src/action-execution-destructive.test.ts:86` ("(objectstack-ai#7828 Option A)"), `lint/src/validate-react-page-props.test.ts:1008` ("since objectui#3382 (objectstack-ai#5435)"), `cli/test/i18n-extract-action-description.test.ts:26` ("(objectstack-ai#7367)"). Some of the `objectstack-ai#3896` hits there (`plugin-sharing`, `qa/dogfood`) cite the sharing-rule record itself. - **Code comments still carry ids** in these files and their sources, for example the `[objectstack-ai#13865]` header of the docblock pin, the `objectstack-ai#20323` comment above `action.test.ts:1116` and the `objectstack-ai/objectui#11610` comments in `ui/component.zod.ts`. Comments are not this card's share, and none is touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Patch release bumping all packages from 0.3.2 to 0.3.3 using Changesets.
Changes in This Release
Technical Details
changeset versionv0.3.3created for this releaseOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.