Repository navigation
Validation Protocol: Cross-Field, Async, and Conditional validation - #59
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…umentation Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
huangyiirene
approved these changes
Jan 21, 2026
huangyiirene
marked this pull request as ready for review
January 21, 2026 13:55
Copilot
AI
changed the title
[WIP] Add validation protocol enhancements for cross-field and async checks
Validation Protocol: Cross-Field, Async, and Conditional validation
Jan 21, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 17, 2026
…tack-ai#7768) (objectstack-ai#7813) * feat(spec): Field.number gains useGrouping presentation hint (objectstack-ai#7768) FieldSchema gains an optional `useGrouping: boolean` (Option A, ruled 2026-08-11 on objectstack-ai#7768, maintainer veto window open) so an authored number field can opt out of Intl.NumberFormat's digit grouping without losing numeric semantics -- the fix for years (Field.number({ scale: 0, min: 1900 })) rendering as "2,026" that downstream apps have worked around three times by converting to Field.text (hotcrm-heimao#35/objectstack-ai#40/objectstack-ai#59). No default is declared: absent defers to the renderer (interim heuristic today, locale default eventually -- objectui#4033's contract, not this package's). Threads through Field.number(...) automatically via the existing FieldInput shape, same as scale/min. Also: liveness ledger classifies the key `planned` (objectui#4033 is the pending consumer); authorable-surface/data.json, field.mdx and state-counts.md regenerated to match. * chore(spec): regenerate field docs/authorable-surface/liveness after main merge Wholesale regen (gen:docs, gen:schema's authorable-surface projection, gen:liveness-counts) to re-materialize artifacts that drifted from commits main picked up since this branch's last merge — the `internal` field key (objectstack-ai#7728) and the `flows` translation surface's planned entries (objectstack-ai#7763). check:generated: 13/13 green; check:liveness: green. --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 6, 2026
This was referenced Sep 6, 2026
zhuangjianguo
pushed a commit
that referenced
this pull request
Sep 8, 2026
…aggregate × field-type table (#16685) Decision batch #80 (2026-09-08) holds ruling #11152 - booleans aggregate as numbers on every backend, no per-aggregate exception - over batch #59's blanket "every other pair refused", which never named booleans. The four arithmetic / order rows gain the BOOLEAN_VALUE_TYPES members; the module TSDoc and the table's pending changeset no longer publish the refusal; the pins hold the boolean rows both literally and against AGGREGATION_CASES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…AggregationFunction × FieldType) dataset measures are refused against (objectstack-ai#16353) (objectstack-ai#16684) * feat(spec): declare the aggregate × field-type compatibility matrix (objectstack-ai#16353) Export AGGREGATE_FIELD_TYPE_COMPATIBILITY and isAggregateCompatibleWithFieldType from @objectstack/spec/data: the one table the dataset compiler and the lint rule refuse dataset measures against. Rows follow the director ruling (decision batch objectstack-ai#59), resolved against the full FieldType membership through the field-value semantic classes; pinned literally in the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf * chore(spec): pin the aggregate × field-type table in the api-surface and export-origins baselines (objectstack-ai#16353) Regenerated by `check:generated --fix` after a full spec build: the two stale shards (api-surface/data.json, export-origins/data.json) each gain the two new exports and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf * fix(spec): fail-closed shape guard on isAggregateCompatibleWithFieldType; correct the published grounds for the boolean and time rows (objectstack-ai#16353) Contract-review patch round. The predicate now refuses any non-string input (a property-key lookup alone coerced ['count'] / { toString } to a member spelling); pinned. The TSDoc and changeset no longer claim booleans are the divergence class - objectstack-ai#11152 has every backend answer them as numbers - and record that row, plus the min/max refusal over the string classes (objectstack-ai#15768 types them as a supported 'string' result), as overrides of existing opinions referred to the maintainer. The time justification names SQLite's canonical TEXT form (objectstack-ai#3994). No row changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 9, 2026
…aggregate × field-type table (objectstack-ai#16685) (objectstack-ai#16750) * feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (objectstack-ai#16685) Decision batch objectstack-ai#80 (2026-09-08) holds ruling objectstack-ai#11152 - booleans aggregate as numbers on every backend, no per-aggregate exception - over batch objectstack-ai#59's blanket "every other pair refused", which never named booleans. The four arithmetic / order rows gain the BOOLEAN_VALUE_TYPES members; the module TSDoc and the table's pending changeset no longer publish the refusal; the pins hold the boolean rows both literally and against AGGREGATION_CASES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * docs(spec): retract the d.ts byte-identity claim in the boolean-members changeset; derive the flag-case vocabulary pin from AggregationFunction (objectstack-ai#16685) Contract-review patch round. The changeset claimed dist/*.d.ts was byte-identical; it is not - the rewritten module TSDoc ships in dist/data/index.d.ts. It now states what holds: the exported declarations are unchanged, the private BOOLEAN_AGGREGATE_FIELD_TYPES constant is absent from the bundle, and api-surface / export-origins are untouched. The test header claims only what the cross-pin reaches (the boolean axis) and the flag-case vocabulary is derived from AggregationFunction.options instead of a literal six-member list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * docs(spec): state the boolean-members constant's absence as measured — dist/*.d.ts and the bundles' export lists, not the bundle (objectstack-ai#16685) Contract-review patch round 2. The constant does ship inside the bundles as a non-exported binding, so "absent from the bundle" over-claimed; the changeset now says only what was measured: absent from dist/*.d.ts and from the bundles' export lists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x --------- Co-authored-by: Claude <noreply@anthropic.com>
os-bill
pushed a commit
that referenced
this pull request
Sep 10, 2026
…at it Amendment by addition, in ADR-0058's own idiom: a new blockquoted block after Amendment II.2 recording the maintainer ruling (decision batch #59, 2026-09-06) that per-row `previous` on a predicate write may serve a row-invariant-in-effect rewrite, with MULTI_UPDATE_HOOK_KEY_DIVERGENCE (#14099) as the engine mechanism that makes it safe and the two shapes the rule does not admit. The superseded 2026-08 D3 sentence is left standing as the dated record and carries a forward pointer to the new block, so the AGENTS.md directive-13 grep lands on the pointer at the line that would otherwise read as the live rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
os-bill
pushed a commit
that referenced
this pull request
Sep 10, 2026
…view F1-F4) Four prose corrections from the delta contract review's non-blocking findings. No behaviour change, no contract change, no new argument. F1 — ADR-0058 Amendment II.3's ruling paragraph cited the date and the decision batch but not the recording comment. It now names comment `5560086928`, the comment on this card that records the maintainer reply the block quotes. F2 — round 3's rewrap left a stub line (`matched). So an`) mid-paragraph. The paragraph is rewrapped to the block's own idiom; the prose is word-identical. F3 — the block said it amends D3's closing SENTENCE. It amends the bullet's last two: the "rewrite *conditioned* on the row is out of contract" sentence is superseded for the in-place / same-key-set case alongside the "not so a rewrite can be aimed" one. Now "closing sentences". F4 — the changeset attributed the ruling to the director seat. The MAINTAINER ruled; the director seat recorded it. This text ships to consumers inside the package's CHANGELOG.md, so the misattribution was published. Now "Maintainer ruling (recorded by the director seat, decision batch #59, 2026-09-06)". Level re-derived rather than inherited: `packages/spec`'s files[] carries src/**/*.zod.ts and dist, while docs/adr/** is in no package's files[]. This round moves no published carrier and changes nothing behavioural, so the existing `@objectstack/spec: minor` stands. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…er-row previous on a predicate write, kept safe by the key-divergence refusal (objectstack-ai#17249) * feat(spec): HookContext admits a row-invariant-in-effect rewrite by per-row previous on a predicate write Amend the D3 clause in hook.zod.ts (and its mirror in bulk-write-hook-conformance.ts, plus the ADR-0058 anchor's invariant text) so that per-row `previous` on a predicate write is supplied for a guard to REFUSE and for a `before*` hook to make a row-invariant-in-effect rewrite — one whose written key set is the same on every matched row — naming the engine's MULTI_UPDATE_HOOK_KEY_DIVERGENCE (400, `keys`, `rows`) refusal as the mechanism that makes the shape safe, and stating plainly what that refusal looks like to an operator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(adr-0058): record the objectstack-ai#16074 ruling as Amendment II.3, point D3 at it Amendment by addition, in ADR-0058's own idiom: a new blockquoted block after Amendment II.2 recording the maintainer ruling (decision batch objectstack-ai#59, 2026-09-06) that per-row `previous` on a predicate write may serve a row-invariant-in-effect rewrite, with MULTI_UPDATE_HOOK_KEY_DIVERGENCE (objectstack-ai#14099) as the engine mechanism that makes it safe and the two shapes the rule does not admit. The superseded 2026-08 D3 sentence is left standing as the dated record and carries a forward pointer to the new block, so the AGENTS.md directive-13 grep lands on the pointer at the line that would otherwise read as the live rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(spec): correct two claims in the amended D3 clause (review REWORK F1/F2) F1 — the key-divergence refusal's message does not END with "Nothing was written". `buildMessage` (`packages/objectql/src/multi-update-hook-key- divergence.ts`) continues "Write those records individually, from inside the handler with 'ctx.api' or by id.", and the pin is `toContain(...)`. Say the message SAYS the phrase and then names the remedy, in `hook.zod.ts` and in the changeset that repeated it. F2 — the refusal was stated unconditionally. `dispatchPerRowBeforeHooks` only compares when `seal()` returned a key record, and `seal()` returns none when a hook REPLACED `ctx.input.data` instead of assigning into it. So the admitted shape is now qualified as an IN-PLACE assignment, the abstention is named where the refusal is claimed, and a row-conditioned REPLACEMENT is listed as a third shape the rule does not admit — it clears with no refusal at all. Mirrored in every carrier of the same clause that this PR authored: `bulk-write-hook-conformance.ts` (D3 docblock and its unenforced-residue note), ADR-0058 Amendment II.3, and the anchor JSON's `invariant` print text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(spec): name the recording comment and fix three prose claims (review F1-F4) Four prose corrections from the delta contract review's non-blocking findings. No behaviour change, no contract change, no new argument. F1 — ADR-0058 Amendment II.3's ruling paragraph cited the date and the decision batch but not the recording comment. It now names comment `5560086928`, the comment on this card that records the maintainer reply the block quotes. F2 — round 3's rewrap left a stub line (`matched). So an`) mid-paragraph. The paragraph is rewrapped to the block's own idiom; the prose is word-identical. F3 — the block said it amends D3's closing SENTENCE. It amends the bullet's last two: the "rewrite *conditioned* on the row is out of contract" sentence is superseded for the in-place / same-key-set case alongside the "not so a rewrite can be aimed" one. Now "closing sentences". F4 — the changeset attributed the ruling to the director seat. The MAINTAINER ruled; the director seat recorded it. This text ships to consumers inside the package's CHANGELOG.md, so the misattribution was published. Now "Maintainer ruling (recorded by the director seat, decision batch objectstack-ai#59, 2026-09-06)". Level re-derived rather than inherited: `packages/spec`'s files[] carries src/**/*.zod.ts and dist, while docs/adr/** is in no package's files[]. This round moves no published carrier and changes nothing behavioural, so the existing `@objectstack/spec: minor` stands. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…ld-type table — all 74 refused pairs answer DATASET_INVALID at one compile door (objectstack-ai#17560) (objectstack-ai#18011) Fixes objectstack-ai#17560 Executes the director-seat ruling on this card (decision batch objectstack-ai#127 item 3, comment 5651572190) in **one pass, not per field class**: `min` and `max` are judged by `AGGREGATE_FIELD_TYPE_COMPATIBILITY` like every other aggregate, and all 74 pairs that were refused-by-the-table-and-enforced-by-nothing now answer `DATASET_INVALID` / **400** at the compile door. ## What the tree said before this Four declarations, three answers, one pair: | declaration | `min` x `text` | |---|---| | `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (spec, objectstack-ai#16353) | refused | | `dataset-compiler`'s compile leg | never judged — `if (!DERIVING_AGGREGATES.has(aggregate)) return;` | | `measureResultType` (service-analytics, objectstack-ai#15768) | a supported `'string'` result | | two shipped test files, in prose | "ruled C — the table is to be AMENDED to accept it, tracked as objectstack-ai#17513" | The fourth row had nothing behind it: objectstack-ai#17513 is closed as a duplicate of this card carrying zero rulings, and the one recorded ruling on this table — decision batch objectstack-ai#59 on objectstack-ai#16099 — refuses those rows. The ruling settled all three sub-questions together because one shared fixture drove members of both halves. ## The ruling's Execution list, line by line - **`dataset-compiler.ts`** — the `DERIVING_AGGREGATES` scope condition is gone; `assertAggregateFieldTypeCompatible` judges all six aggregates through the same `DATASET_INVALID` / 400 door. The refusal message now names the divergence each aggregate class really has (`min`/`max` SELECT a stored value and diverge on ORDER — collation-dependent for text, absent altogether for `jsonb`; `sum`/`avg` DERIVE a number and diverge on arithmetic) and prescribes accordingly. The `sum`/`avg` sentence is byte-identical to what shipped, so objectstack-ai#16099's and objectstack-ai#16778's message pins are untouched. - **`measureResultType`** — asks `isAggregateCompatibleWithFieldType` before it answers, so the rule and the table agree **by construction** rather than by review. `STRING_SOURCE_FIELD_TYPES` and the `formula` branch are retired; `min`/`max` over the temporal class still answers `'time'`. - **Tests** — the shared fixture in `measure-result-type.test.ts` is re-aimed off refused pairs following the objectstack-ai#16737 precedent in the same file; the **two conditional pins are FLIPPED, never deleted** (`aggregate-nontemporal-measure-refusal.test.ts` "min / max are NOT judged by this gate", relocated by text rather than by the ruling's line number — see Deviations; `aggregate-datetime-measure-refusal.test.ts` "a min over a TEXT field still compiles here"), each naming batch objectstack-ai#59 and this ruling; the negative control that accepted pairs still compile (`min` x `number`) is **kept** and joined by four more; the eight `objectstack-ai#17513` citations are rewritten to this card. - **The table's TSDoc "override" paragraph** is rewritten as settled ground — the overridden opinion is retired, not standing beside it. - **Breaking** — `minor` under the launch-window convention with the BREAKING banner, plus a new ADR-0087 **semantic migration entry** (`dataset-measure-selecting-aggregate-field-type-refused`, protocol major 18) writing the structured TODO that names the measure and the field type. No lossless conversion exists, which is why it is a semantic TODO and not a D2 conversion. ## One consequence the ruling implies and did not name Retiring the `formula` branch left `measureResultType`'s third input (`formulaReturnType`, objectstack-ai#16236) with no reader, and `AnalyticsServiceConfig.sourceFieldMeta`'s `returnType` key with no consumer. Both are **removed**: a declared input nobody reads is the declared-not-enforced shape Prime Directive objectstack-ai#10 refuses. `FieldSchema.returnType` itself is untouched — display formatting and validation are its other declared consumers. ## Verification Every exit code captured **on the command** (redirect first, `EXIT=$?`, then read), never after a pipe. All figures below are from the FINAL tree, `035a41c7e` — the second merge of `origin/main` (`8261ff717`) that this branch carries, built whole. They were re-taken in full on this tree after the patch round; the pre-patch tree `399d12244` read the same shape. Build and tests, through `scripts/pm/os-verify-lock.sh` (`OS_VERIFY_LOCK_SLOT=issue-17560`; four holds across both rounds, VERDICT command-exit 0 on every one — 590s / 21s / 795s / 890s held): ``` pnpm install --frozen-lockfile :: exit 0 pnpm build (whole repo) :: exit 0 pnpm --filter @objectstack/spec check:generated :: exit 0 all 15 generated artifacts up to date pnpm --filter @objectstack/service-analytics exec vitest run :: exit 0 111 files / 2392 tests passed pnpm --filter @objectstack/spec test :: exit 0 476 files / 13556 tests passed pnpm --filter @objectstack/service-analytics typecheck :: exit 0 pnpm --filter @objectstack/spec typecheck :: exit 0 pnpm lint (whole repo) :: exit 0 ``` Gate families derived from the **actual** changed paths, not from a list: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — 15 paths, **87 commands**, no STALE TREE warning on this tree. All 87 re-run on `035a41c7e` after the patch round, each recorded as `COMMAND :: exit CODE`, and reconciled: ``` dispatch-gates --ran :: exit 0 87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED (a DERIVED zero — all 87 recorded an exit code and none of them is 3) ```⚠️ On the pre-merge tree four of them answered **exit 3 — PREREQUISITE NOT MET** (`check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`): each reads built output and the closure was not built yet. That is each gate's own "nothing was measured" code, neither a pass nor a finding. All four were re-run after the whole-repo build and all four exit 0. ⛔ Not a complete account of what CI runs: the 48 artifact-roster families, the 11 declared wide-population families, the 5 path-scheduled CI jobs and the always-runs tail are each outside the derived 87, as `dispatch-gates` prints. ### Reverse verification — direction predicted BEFORE running, and it held Ablation: the deleted scope condition put back as `if (aggregate !== 'sum' && aggregate !== 'avg') return;` in `dataset-compiler.ts`, on the committed fix, under the same verify lock. ``` HEAD blob 9062405 mutated blob b2935944651c57ba3cd8ec759a3303fb33ff105c (differs; an equal or empty hash was coded to abort) on-disk proof anchor occurrences 1 before / 1 after · injected marker 0 before / 1 after (occurrence counts on the mutated text — never an editor's exit code, never a --stat) MUTATED :: exit 1 4 files failed · 13 of 209 tests red RESTORED :: exit 0 4 files passed · 209 of 209 green restore proof blob back to 9062405… · marker count 0 · git diff HEAD clean · WHOLE-TREE git status --porcelain empty (trap '<restore>' EXIT INT TERM, paths absolute from git rev-parse --show-toplevel) ``` Predicted: red in the ordinary direction — with the scope back, the compile SUCCEEDS, so every refused-pair case fails on "expected a refusal, none was thrown" rather than passing vacuously on an empty result. Observed: exactly that, spread across all four files — the two flipped pins, the re-aimed section E, and the formula end-to-end section. `node scripts/ablation-dist-preflight.mjs @objectstack/service-analytics ABLATION-17560-SCOPE-RESTORED` **:: exit 1**, reported rather than worked around: the package has no `dist/` in this tree at all. Its prerequisite is inapplicable here rather than unmet — these suites import their subject relatively (`../analytics-service.js`), so they resolve to `src/`, and the 13 reds on the mutated tree beside 209 greens on the restored one are the direct evidence that the edit reached the subject. ## Acceptance notes — noted, not filed - The table's module TSDoc still calls itself "the contract both consumer legs execute — the compile-time refusal in the dataset compiler and the authoring-time lint rule". The authoring-time leg still does not exist; `packages/lint` never calls the predicate. Carrier: **objectstack-ai#16354**, open and labelled `pm:blocked`, and the ruling leaves it there ("The lint leg is unchanged"). Not filed. - A **ninth** `objectstack-ai#17513` citation exists outside `packages/`, in the still-pending changeset `.changeset/deriving-aggregate-nonnumeric-field-refused.md` (objectstack-ai#16099's). It ships in the same release as this one and its "min / max are still not judged here" section would contradict this entry in one compiled CHANGELOG, so it carries a superseded-within-the-same-release-window note and its tracker pointer is repointed. Reported rather than assumed in scope. - `packages/spec/src/migrations/entries/semantic/18.dataset-measure-aggregate-field-type-refused.ts` (objectstack-ai#16778's entry) carried two scope sentences this change makes false — "the compile leg is scoped to that class and to nothing else" and "a measure over a field of any OTHER class is neither refused nor certified here" — plus a pointer to objectstack-ai#16785, an issue number that does not resolve. objectstack-ai#16099's own changeset recorded that widening it "is a `packages/spec` edit this card is fenced out of and is reported to the `domain:spec` seat rather than done here"; this is that seat and this card is the carrier, so the two sentences are corrected in place and now name all three entries. ## Patch round — the at-tier contract review's three FAIL grounds, closed The review (comment `5653288459`) passed the behaviour in full — one door for all six aggregates, 74 re-derived off the raw enum, table unamended, `formula` refused on the storage ground, both pins flipped not deleted, controls kept, `Clause-②: no` correct — and upheld the `returnType` removal as compelled by the ruling's own words. What failed was the truth of the contract TEXT shipping beside it. All three fixes are text-only; ⛔ no behaviour, pin, control or the `returnType` decision was touched. **T1 — a same-release changeset said the opposite, twice.** `.changeset/16236-formula-return-type-measure-column.md` (still pending, so it compiles into the same CHANGELOG block as this entry) promised a typed formula measure column and a `returnType?: string` fourth member on `sourceFieldMeta`. It now carries the same superseded-within-the-release-window treatment already given to objectstack-ai#16099's, at the head and again on the `sourceFieldMeta` paragraph. This PR's own changeset now states that the removed key **was never released** and gives the host its one line. > Reading: released `packages/services/service-analytics/CHANGELOG.md` (17.4.0) — `16236` 0 hits, "fourth member" 0 hits. Lit controls on the same file: `15768` 1, `measureResultType` 2, `sourceFieldMeta` 4. Dark control `qzwxrt4419` 0. Stronger still, the one `returnType` hit in that released text says in as many words that the key "is not on `AnalyticsServiceConfig.sourceFieldMeta`'s return shape". And `git grep "fourth member" origin/main -- .changeset/16236-…` is 1 — the adding changeset is still pending on `main`, so no tarball ever carried the key. **T2 — a dead tracker pointer the ruling itself named.** `.changeset/dataset-measure-aggregate-field-type-refused.md` (objectstack-ai#16778's, pending) still said the string rows were "under objectstack-ai#16785, ruled C — the table itself is to be amended". Corrected where it stands: `16785` resolves to nothing, batch objectstack-ai#127 found no ruling C behind the citation, and the table is not amended. The file also gains the superseded banner, and its two other now-false sentences — `sum` over a `percent` "compiles exactly as it did before", and `avg`/`sum` over temporal being "the only pairs whose behaviour changes in this release" — are marked where they stand. > Reading: `git grep 16785` over the whole repo was **1** hit, all of it in that file — the ruling's own claim reproduced. It is now 2 in the same file, both naming it as the retired pointer; repo-wide it appears nowhere else. Dark control `qzwxrt4419` 0. **T3 — the corrected objectstack-ai#16778 registry entry miscounted itself.** It claimed to be "the FIRST of three" and pointed at "the two entries that widened it — objectstack-ai#16099 …". objectstack-ai#16099 registered **no entry**: its changeset declares `not-required (already-registered dataset-measure-aggregate-field-type-refused)`, so its widening rides this id. The `surface` now reads "ONE OF TWO", names that `not-required` relationship explicitly, and says there is no third; `acceptanceCriteria` keeps its one true sentence (every refused pair is refused at the compile door at major 18) and drops the phantom entry. ⛔ Its scope criterion itself is **not** widened — that is objectstack-ai#16099's open ask and the seat is tracking it separately. `registry.ts` regenerated. > Reading: semantic entries whose id contains `aggregate-field-type` = **2** (lit control: 211 entries in the directory; dark control `qzwxrt4419` 0). No entry file is named for objectstack-ai#16099 (`git grep -l deriving-aggregate|nonnumeric` over `entries/` exits 1). In the regenerated `registry.ts`: "ONE OF TWO" 1, "no third entry to look for" 1, "FIRST of three" 0, "the two entries that widened it" 0 — and repo-wide both stale phrases are 0.⚠️ Also taken, declared rather than smuggled: the review's **F5 nit** — the changeset's opening list read as exhaustive while naming 10 of 37 types. It now says "any of the **37** field types outside the numeric, temporal and boolean classes — for example …", and points at the ADR-0087 entry for the full list. One phrase, in a file T1 already reopens. ⛔ Not done, deliberately: widening objectstack-ai#16778's `acceptanceCriteria` to `sum`/`avg` over every class (objectstack-ai#16099's open ask, fenced out by the dispatch), and the four items the review listed as "not this PR's to fix". ## Landing ⛔ **Draft, and it stays that way until the seat's contract review clears it.** The claim grades this `Clause-②: no` — nothing starts being accepted, this pulls code back to the declared contract — but the **path** limb of the clause-② enqueue gate fires on `packages/spec/src/**` regardless of the declaration, so `needs:contract-review` is carried on both the PR and the card. ⛔ Not flipped ready, ⛔ auto-merge not armed, ⛔ not queued. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
os-elon-musk
pushed a commit
that referenced
this pull request
Sep 18, 2026
… cannot carry The authoring-time leg of the aggregate x field-type contract (director ruling, decision batch #59: "both legs, table in spec"). A dataset measure pairs an `aggregate` with a `field`; `AGGREGATE_FIELD_TYPE_COMPATIBILITY` in `@objectstack/spec` says which pairs every backend can answer identically, and until now nothing in the authoring path read it: `avg` over a `datetime` field validated clean, shipped, and became either a plausible wrong number (SQLite coerces the canonical UTC text and returns the average YEAR) or a query-time failure (PostgreSQL has no such function), decided by the deployment rather than by the document. `validateDatasetMeasureAggregates` walks `datasets[].measures[]`, resolves the field's declared type on the object graph lint already indexes, and refuses the pair when `isAggregateCompatibleWithFieldType` says no. The verdict is the shared predicate's on every pair — no second table here — and the message names the aggregate, the field, its declared type and the accepted set, with the way out computed from the same table. Silent wherever the type cannot be resolved rather than guessing: an unresolvable base object, a dangling field path (that is `dataset-field-unknown`'s finding), an untyped leaf, a non-string in either position, and an aggregate outside the table's own vocabulary. It reaches further than the compile leg in one direction only: a dotted `relationship.field` reference, whose leaf type authoring time can read and the compile leg's base-object field metadata cannot. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…carry (objectstack-ai#19138) Fixes objectstack-ai#16354 Clause-②: yes (narrowing) The lint leg of the aggregate × field-type contract (director ruling, decision batch objectstack-ai#59, 2026-09-06, 「both legs, table in spec」). A dataset measure pairs an `aggregate` with a `field`; `AGGREGATE_FIELD_TYPE_COMPATIBILITY` in `@objectstack/spec` declares which of those pairs every backend answers the same way, and nothing in the authoring path read it. New gating rule `measure-aggregate-field-type-refused` in `packages/lint/src/validate-dataset-measure-aggregates.ts`, registered in `AUTHORING_RULES`, so all three commands run it. The verdict is the shared predicate's on every pair — `isAggregateCompatibleWithFieldType`, the same call the compile leg makes — and there is no second table in this repo. ## The take-order test, as an artifact The maintainer's test for a contract-surface card: take a piece of author-written metadata, feed it in before and after the change, and see whether accept/reject flips. Both arms below were **run**, not reasoned from the diff: one probe script, byte-identical in both arms, driving the real shared registry (`runAuthoringRules('lint', …)` — the table `os lint` / `os validate` / `os build` all run). The sample (author-written metadata, three measures over one object): ```ts objects: [{ name: 'crm_opportunity', sharingModel: 'private', fields: { name: { type: 'text' }, units: { type: 'number' }, closed_at: { type: 'datetime' }, win_rate: { type: 'percent' }, } }], datasets: [{ name: 'opportunity_metrics', object: 'crm_opportunity', dimensions: [], measures: [ { name: 'avg_closed_at', aggregate: 'avg', field: 'closed_at' }, // positive control { name: 'avg_units', aggregate: 'avg', field: 'units' }, // negative control { name: 'total_win_rate', aggregate: 'sum', field: 'win_rate' }, // third control ] }] ``` | measure | BEFORE (`c70581bc8`, pristine worktree) | AFTER (this branch) | |---|---|---| | `avg(closed_at)` — `datetime` | **ACCEPTED**, no finding | **REFUSED**, `error` | | `avg(units)` — `number` | ACCEPTED, no finding | ACCEPTED, no finding (unchanged) | | `sum(win_rate)` — `percent` | ADVISED only — `measure-aggregate-incoherent`, `warning` | **REFUSED**, `error`, beside that same warning | | total findings on the sample | 1 | 3 | ⇒ accept/reject flips on author-written metadata. Contract surface. **BEFORE arm** — taken on the pristine worktree (`git status --porcelain` printed 0 lines) at the branch point, through the package source, with `@objectstack/spec` freshly built in this worktree beforehand: ``` $ pnpm exec tsx probe.mjs /home/user/wt-16354/packages/lint/src/index.ts total findings over the whole sample: 1 --- measure[0] avg(closed_at) "avg_closed_at" -> ACCEPTED (no finding) --- measure[1] avg(units) "avg_units" -> ACCEPTED (no finding) --- measure[2] sum(win_rate) "total_win_rate" -> ADVISED (warning) [warning] measure-aggregate-incoherent @ datasets[0].measures[2] ``` **AFTER arm** — same probe, unchanged, on this branch; run twice, through the source and through a freshly built `dist`, with identical verdicts: ``` $ pnpm exec tsx probe.mjs .../packages/lint/src/index.ts # and: node probe.mjs .../packages/lint/dist/index.js total findings over the whole sample: 3 --- measure[0] avg(closed_at) "avg_closed_at" -> REFUSED (error) [error] measure-aggregate-field-type-refused @ datasets[0].measures[0].aggregate --- measure[1] avg(units) "avg_units" -> ACCEPTED (no finding) --- measure[2] sum(win_rate) "total_win_rate" -> REFUSED (error) [warning] measure-aggregate-incoherent @ datasets[0].measures[2] [error] measure-aggregate-field-type-refused @ datasets[0].measures[2].aggregate ``` **Build provenance, because a stale `dist` reads exactly like a real reading.** Both arms resolve `@objectstack/spec` through its build. `packages/spec/dist` was absent in this fresh worktree and was built here (`pnpm --filter '@objectstack/lint^...' build`, run twice: once before the BEFORE arm, once after merging `main`); `packages/lint/dist` was built for the dist arm. **Nothing was served from a turbo cache: no `turbo` process was in either pipeline** — `pnpm --filter … build` invokes each package's own script directly, and `grep -ciE 'cache hit|turbo|FULL TURBO'` over the build logs returns 0. The spec `dist` timestamp is newer than every source it was built from. ## The three controls the card names All three are permanent tests in `validate-dataset-measure-aggregates.test.ts` (23 tests in the file; the whole package is 105 files / 3978 tests, green): 1. **positive control** — `avg` over a `datetime` field **fires**: one `error` at `datasets[0].measures[0].aggregate`, message pinned to name the aggregate, the field, its type and every accepted type. 2. **negative control** — `avg` over a `number` field is **silent**. Generalised rather than left as one case: `avg`/`sum` over the whole numeric class, all four arithmetic and order aggregates over the boolean class (maintainer ruling objectstack-ai#11152), `min`/`max` over the temporal class, and `count`/`count_distinct` over **every** declared `FieldType` are each asserted silent. 3. **`sum` over `percent`** **fires** — the pair `analytics-service.ts` already calls incoherent. It now carries two findings: the older advisory about meaning (suppressible) and this gating refusal about the contract. Deliberate, and documented in the rule's header: the two questions disagree elsewhere — `count_distinct` × `percent` is advised and **accepted** by the table, `avg` × `datetime` is refused here and not advised there. And the strongest anti-false-positive assertion, because a false positive here is worse than the gap being closed: the rule's verdict is compared against `isAggregateCompatibleWithFieldType` for **every aggregate × every declared `FieldType`** (6 × 44 = 264 pairs), with floors on *both* sides of the sweep (more than 50 refused, more than 50 accepted) so neither "stopped firing" nor "fires on everything" can satisfy the equality vacuously. ## The message the rule emits, verbatim ``` measure "avg_closed_at" applies aggregate "avg" to field "closed_at", which object "crm_opportunity" declares as `datetime`. That pair is refused by the aggregate × field-type compatibility table in @objectstack/spec, so the number a backend returns for it is a property of the SQL dialect rather than of the data — one coerces the stored form and answers something plausible, another has no such function and fails at query time. "avg" accepts: number, currency, percent, rating, slider, progress, summary, boolean, toggle. ``` hint: ``` Either point "avg" at a field of an accepted type, or aggregate "closed_at" with one its `datetime` type accepts: count, count_distinct, min, max. `count` / `count_distinct` accept every type because they read no arithmetic off the value; a quantity that must be added up or averaged has to be STORED as a numeric field (a computed column) and aggregated as one. The compile leg refuses this same pair with `400 DATASET_INVALID` before any SQL is emitted, so this is the same repair made earlier. ``` Both halves are **computed from the table** — the accepted set for the refused aggregate, and the aggregates that would accept this field's type — never prose restating it, so neither can drift from the rows. ## Where it stands down, and where it reaches further Silent wherever the type cannot be resolved, per the spec module's own instruction that a consumer must not hand the predicate a guess: a dataset naming no base object or one this stack does not define, an object with no readable field map, a field path that resolves to nothing (that is `dataset-field-unknown`'s finding — one typo must not also yield a type verdict), an untyped leaf, a non-string in either position, and an aggregate outside the closed `AggregationFunction` vocabulary. Each is a test. It reaches further than the compile leg in exactly one direction: a dotted `relationship.field` reference. The compile leg returns early on those because its declared-type source answers for the base object only; authoring time has the whole object graph, so the leaf's declared type is a read rather than an inference, and the refusal names the object the leaf lives on. Registry-injected columns are judged on the same axis as authored ones. ## Verification - `pnpm --filter @objectstack/lint test` — 105 files, **3978 passed, 0 skipped**. Re-run after merging current `main` into this branch (the merge landed sibling work inside this package), and again after the last commit. - `pnpm --filter @objectstack/lint typecheck` — clean, including `check:test-typecheck` over the test layer. - `pnpm --filter '@objectstack/lint^...' build` and `pnpm --filter @objectstack/lint build` — clean, dts emitted. - Gates run locally, each exit code captured before any pipe: `check:nul-bytes`, `check-empty-changeset --base origin/main`, `check-adr-0087-registration --base origin/main` (judges this changeset: `[BREAKING+clause-②-narrowing] not-required (already-registered)`), `check-changeset-no-major`, `check:changeset-gate-self-tests`, `check-changeset-fixed`, `check:doc-anchors`, `check-doc-frontmatter`, `check:docs-single-h1`, `check-docs-section-name`, `check:doc-authoring`, `check:docs-transcript-drift`, `check:docs-spec-enumerations`, `check:docs-redirects`, `check:docs-audit-scope`, `check:published-files`, `check:published-readme-links`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:type-check-coverage`, `check-undeclared-dep-imports`, `check-comment-mask-adoption`, `check-comment-mask-corpus`, `check-keyed-text-bounds`, `check-section-landing-index`, `check-doc-route-spelling --advisory`, `docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`, `check:cli-examples-parity`, `check:corpus-claim-drift`, spec `check:docs` and `check:yaml-examples`, and the lint package's own two doc gates. **All green.** - **Two of those gates went red on my first pass and drove real corrections**, both mechanical consequences of registering a rule: `check:doc-authoring` refused a tracker id inside the new entry's runtime `surfaceReason` string, and `check:docs-transcript-drift` derives the author-time rule count from the registry and found four CLI transcripts still printing the old one (45 → 46). - ESLint, as a declared narrowing rather than a farm run: `eslint --no-inline-config --format json` over the four touched TypeScript files — **4 files linted, 0 errors, 0 warnings**. The invariance that makes the narrowing a measurement rather than a skipped check is stated by the config itself: this repo runs one `eslint.config.mjs`, which *"never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file, test or not"* — so no verdict on an untouched file can move on account of this diff, which changes no config and no shared roster. The population is that config's own `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` blocks minus their `ignores`; the repo-wide run belongs to CI. - `check:type-check-debt` reported **PREREQUISITE NOT MET (exit 3)** — the whole-workspace closure is not built here, and it says in its own words that this "is NOT a pass and NOT a finding". Recorded as not measured, not as green. Its sibling `check:type-check-coverage` did run and passed. - The gate derivation (`scripts/pm/dispatch-gates.mjs --commands`) names 86 families for this change set; 26 were run locally and the rest — repo-wide scans and populations CI owns — are left to CI, as a declared narrowing. ## Acceptance notes Out of scope here, filed nowhere by this PR, recorded so they are not rediscovered: - **The `dataset` metadata type is not gated at the runtime publish door at all.** `dataset` is a registered type with `allowRuntimeCreate: true` and `supportsOverlay: true`, but `TYPE_TO_STACK_KEY` in `runtime-gate.ts` maps no `dataset` row, so a dataset write builds no per-write snapshot and **zero** author-time rules dispatch for it — including the existence rules whose whole failure mode is a chart that renders empty. That is why this rule declares `surfaces: cli` with a reason naming the type axis rather than the snapshot's contents: both collections it reads are carried, so the usual reason does not apply. Mapping that type is a card of its own, and it would hand the door every rule reading `stack.datasets` at once. - **The refusal's divergence and remedy prose lives only in `service-analytics`.** The compile leg builds its message from two local helpers; `@objectstack/lint` cannot import them (its dependency direction is lint → spec, never a service), so this rule states the same two facts in its own words. Two accounts of one refusal can drift. Moving that prose into `@objectstack/spec` beside the table would make both legs read one sentence. - **One pair now yields two findings.** `sum` × `percent` is reported by both this rule (`error`, contract) and `measure-aggregate-incoherent` (`warning`, semantics, suppressible). Consolidating them is a decision about a published rule id's severity and suppressibility, which is its own PR by this repo's own convention. **Note on the declaration — it diverges from the claim comment, deliberately.** The claim carries `Clause-②: no`, which is right about the refusal on its own: narrowing an accept set is a semantic surface and does not by itself touch clause ②. But this diff *also* adds published exports from `@objectstack/lint` (`validateDatasetMeasureAggregates`, `MEASURE_AGGREGATE_FIELD_TYPE_REFUSED`, `DatasetMeasureAggregateFinding`) and an `AUTHORING_RULES` entry — both widening tells against a `no` declaration — so the honest value is `yes`, and `yes (narrowing)` is the spelling for a diff that widens *and* narrows. Body and changeset carry the identical line. The changeset additionally carries the migration per refused class and the ADR-0087 disposition `not-required (already-registered …)`: the two semantic entries registering this exact surface already exist, and the compile-time leg declared the same disposition against the first of them. The claim comment is the seat's to amend, not this PR's. This PR opens as a draft and stays draft. --- _Generated by [Claude Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_ --------- 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
… dataset-*, hook-* and metadata-* migration entries states each lesson in words, not tracker numbers (stage 5) (objectstack-ai#20522) Part of objectstack-ai#20233 Stage 5: the field-, export-, api-, dataset-, hook- and metadata- families. Clause-②: no **Stage 5 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. One `surface` moves, under ruling A of the stage-1 ACCEPT (`5858839916`): it carried two tracker numbers. ## 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 is still a tracker number. This stage covers the next six families, `field-`, `export-`, `api-`, `dataset-`, `hook-` and `metadata-`: **110 sites → 0** in the three prose fields and **2 → 0** in `surface`, across 25 entry files. 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 seventeen families. ## Census — tracker ids in the author-shown fields **Instrument.** Stage 4's TypeScript-AST census, the same script: for each entry object literal under `packages/spec/src/migrations/entries/**` it evaluates `replacement`, `reason`, `acceptanceCriteria` and (separately) `surface`, joining string literals with `+`, and counts `#` followed by 4 or 5 digits at a word boundary. On base `9e9bb464` it reads the whole tree at **571** sites / 7 `surface` / 61 short, which is stage 4's recorded 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 objectstack-ai#20234's surface, and this PR touches none (proved below). **Base `9e9bb464`:** `field-` 10 entries, **28** sites (5 / 20 / 3); `export-` 3, **21** (0 / 21 / 0); `hook-` 4, **17** (0 / 17 / 0); `api-` 5, **16** (0 / 14 / 2); `metadata-` 7, **16** (1 / 15 / 0); `dataset-` 3, **12** (2 / 8 / 2), plus **2** in `surface`. **110** sites (8 / 95 / 7) in 24 of the 32 entries; 75 distinct ids (71 bare, 1 spelled `framework#`, 3 `objectui#`). Short numbers: 14. **After this PR:** all six families **0**, `surface` 0; the eleven earlier families still 0; whole tree **571 → 461**, `surface` **7 → 5**, short **61 → 50**. The PM's rough line count (126 sites, 25 files) is a wider instrument; the AST reading is 110 in 24 files, and the 25th file carries only short decision-batch numbers and ruling-record ids. | entry | sites (replacement / reason / acceptanceCriteria) | surface | short numbers | |---|---|---|---| | `17.api-runtime-create-withdrawn` | 9 (0 / 7 / 2) | | | | `17.export-axis-opt-in` | 7 (0 / 7 / 0) | | | | `17.export-field-meta-constraints-retired` | 8 (0 / 8 / 0) | | | | `17.field-runtime-create-withdrawn` | 9 (0 / 6 / 3) | | | | `17.hook-context-session-roles-retired` | 6 (0 / 6 / 0) | | | | `17.hook-register-empty-object-target-refused` | 8 (0 / 8 / 0) | | | | `18.api-assembled-entry-split` | 1 (0 / 1 / 0) | | 1 | | `18.api-error-retry-after-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | `18.api-runtime-config-durations-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | `18.dataset-filter-nested-relation-equality-array-refused-at-save` | 2 (0 / 2 / 0) | | | | `18.dataset-measure-aggregate-field-type-refused` | 5 (1 / 2 / 2) | 2 | 2 (1 kept) | | `18.dataset-measure-selecting-aggregate-field-type-refused` | 5 (1 / 4 / 0) | | 3 (1 kept) | | `18.export-job-family-retired` | 6 (0 / 6 / 0) | | 2 | | `18.field-currency-scale-refused` | 0 | | 2 | | `18.field-max-length-malformed-or-misplaced-refused` | 8 (3 / 5 / 0) | | | | `18.field-min-length-malformed-or-misplaced-refused` | 3 (1 / 2 / 0) | | | | `18.field-multiple-non-capable-type-refused` | 4 (0 / 4 / 0) | | 1 | | `18.field-predicate-reference-traversal-refused` | 2 (0 / 2 / 0) | | | | `18.field-scale-precision-integer-refused` | 2 (1 / 1 / 0) | | 1 (kept) | | `18.hook-register-undispatched-lifecycle-event-refused` | 3 (0 / 3 / 0) | | | | `18.metadata-customization-protocol-retired` | 4 (0 / 4 / 0) | | | | `18.metadata-endpoints-switch-radius-repartitioned` | 3 (0 / 3 / 0) | | | | `18.metadata-manager-config-cache-ttl-unit-in-key` | 3 (1 / 2 / 0) | | | | `18.metadata-manager-config-inert-cache-keys-retired` | 3 (0 / 3 / 0) | | | | `18.metadata-plugin-additional-types-retired` | 3 (0 / 3 / 0) | | | | **total, 25 entries** | **110 (8 / 95 / 7)** | **2** | **14 (3 kept)** | The seven entries of these families that carried no number are untouched: `api-endpoint-cache-ttl-unit-in-key`, `field-inline-and-related-list-columns-closed`, `field-master-detail-set-null-refused`, `field-reference-to-spelling-retired`, `hook-timeout-unit-in-key`, `metadata-changed-event-payload-retired`, `metadata-item-name-grammar-enforced`. ## Text only — proved by a base-vs-head AST comparison For every entry file this PR changes, both versions (`9e9bb464` and the head) are parsed and compared: every import declaration; every property other than the three prose fields, by evaluated value (so `id`, `from` / `to` and any matcher); every comment token in the file; and the code skeleton, token by token with each run of joined string literals collapsed to one. `surface` is allowed to differ only where the base value carried a tracker id and the head value carries none. **25 files compared, 0 with a non-prose change**; one note, the ruling-A `surface` of `18.dataset-measure-aggregate-field-type-refused`. The instrument is shown able to fail first: on an in-memory copy it reports DETECTED for a mutated `id`, a mutated comment, a mutated `surface` whose base carried no tracker id, a mutated code token and a mutated import, and stays dark on a prose-only mutation. 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: 72 in this repository (one spelled `framework#`, the repository's old directory name) and 3 in `objectstack-ai/objectui`. Ids are in code spans so this body posts no cross-references. **4 ids answer 404** on both the issues and the pulls endpoint (re-probed with a 200 control, `14478`); those sentences are rewritten from what `main` records, listed in Acceptance notes. **`api-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `5488` | Maintainer, 2026-08-07: flip `api` to `allowRuntimeCreate: false` and refuse at the write inlet (remove, not converge the read path); re-entry only with a real consumption path. | the measurement and the ruling were already in the sentence; the id is dropped | | `5040` | The declarative endpoint executor project; its acceptance step moved showcase's endpoints to the artifact route, live. | "showcase uses the artifact route, and its declared endpoints serve live" | | `4052` | `BatchOptions.validateOnly`, retired the same way (a runtime verdict, no D2). | named by the key, which the sentence already carried | | `5279` (PR), `5189`, `5203` (PR) | The publish gate for `api` drafts; `publishPackage` and load-time `buildEndpointIndex` running the endpoint gate. | named by the functions the sentence already carried | | `2657` | Studio metadata coverage: Part B asks which un-typed concepts (`apis` among them) become registered types. | "if the Studio metadata-coverage work promotes `apis` to a registered type WITH A REAL CONSUMPTION PATH" | | `5311` | The direct-active `saveMetaItem` write was a third path past the namespace and duplicate gates; closed as subsumed by the `5488` ruling. | "The same refusal closes the direct-active write too, which had been a third path past the endpoint namespace and duplicate-path gates." | | `18576` | Maintainer, 2026-09-17, option B: narrow the `./api` entry, rather than add a bundle-weight rule (A) or accept the weight (C). | "Maintainer ruling of 2026-09-17, option B (narrow the entry, rather than add a bundle-weight rule to the browser-reachability ledger or accept the weight as it stood)" | | `14478`, `15677` | Maintainer ruling B of 2026-09-02: a duration's unit lives in the key name, no offender grandfathered; the `api/` stack of that ruling. | "Maintainer ruling B of 2026-09-02 on duration-shaped keys"; trailing ids dropped | **`export-` (15 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `6350` | The stock reconciliation of the v17 train's breaking changesets against the ledger, which backfilled this entry. | "Registered (backfilled) by the stock reconciliation that compared the breaking changesets already on the v17 release train against this ledger" | | `3544`, `3710` | The user-level export axis, and its extension to the CSV attachments scheduled reports mail out. | "the export axis, and its extension to the CSV attachments scheduled reports mail out" | | `6148` | **404** — see Acceptance notes. | "the gate that makes a breaking changeset state its ADR-0087 disposition" | | `3956` (spelled `framework#`) | The import dry run skipped the field-level validation the real write ran; the hand-copied pre-check mirror was added to close it. | "added when the dry run was found skipping the field-level validation the real write ran" | | `4633`, `6532` (PR) | Maintainer, 2026-08-06, ruling D: a validate-only protocol operation, so the dry run's prediction is the engine's verdict; the mirror retired. | "the maintainer's 2026-08-06 ruling D (a validate-only protocol operation, so the dry run's prediction is the engine's verdict by construction)" | | `4484`, `5540`, `6011` | The `findStream`, `IStorageService.list` and `actor-user-roles-to-positions` retirements. | named by their surfaces, which the sentence already carried | | `6536` | The eight keys left read by nothing after the mirror retired, deferred to their own sweep. | "this is the removal the dry-run change deliberately deferred to a sweep of its own" | | `17158` | Maintainer, 2026-09-12, ruling A: retire the family, `IExportService` and `ScheduleExportInput`; `ScheduleState` with it unless a live consumer is measured. Landing route A, 2026-09-24; scope note, 2026-09-25. | "maintainer ruling A of 2026-09-12 (…), the landing route the maintainer ruled on 2026-09-24 (route A: …), and a scope note the maintainer agreed on 2026-09-25" | | `objectui#10247` | The console retires its own async-export path first. | "objectui retires its own side of the unimplemented async-export path first"; "which carries objectui's own retirement" | | `19543` | Three sibling list doors declared `limit` / `cursor` and never read them; the export-job list's door folded into this retirement. | "one of three sibling list doors found declaring them and never reading them" | | `16320` | The seven cron-typed positions nothing read, retired (three on these defs). | "once the retirement of the cron-typed positions nothing read had deleted theirs"; "Those earlier cron-position deletions" | **`field-` (20 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7893` | Maintainer, 2026-08-12: retire the runtime `field` write channel rather than build a read path. | the measurement and the ruling were already in the sentence; the id is dropped | | `5488`, `4052` | The `api` and `validateOnly` withdrawals. | "the `api` withdrawal's rationale reused (`api-runtime-create-withdrawn`)"; named by key | | `7743`, `7894` | The field overlay lock (`NOT_OVERRIDABLE`); the plural `/meta/fields/` spelling folded onto the singular. | "The field overlay refusal"; the plural door named by its path | | `8169` | The `_diagnostics` envelope asserts well-formedness only; it has no "in effect" axis. | "(the envelope has no "in effect" axis)" | | `11566`, `11989` (PR), `11950` | Maintainer, 2026-08-24: tighten both halves of `maxLength` (value shape and applicable types); shipped on 17.x; registered in a follow-up. | "Maintainer ruling of 2026-08-24, tightening both halves — the value's shape and the types the key applies to"; "registration was deferred to a follow-up" | | `11875` | Maintainer, 2026-08-25, option 1: the write seam enforces `maxLength` for `signature` / `qrcode`, then both join the bounded-string set. | "which joined once the write seam enforced a declared bound on them" | | `11431` | The SQL driver stops reading a malformed bound as authoritative (the `varchar(0)` plan). | "until it was taught to stop reading a malformed bound as authoritative" | | `8321` | `scale` / `precision` refused as non-integer or negative — the house pattern. | "the house pattern the `precision`/`scale` integer refusal set" | | `11949` | Maintainer, 2026-08-25, option B: `minLength` is `int().min(1)`, zero refused, the `maxLength` template in full. | "Maintainer ruling of 2026-08-25 (option B, the lower bound at 1) … the defect pair the 2026-08-24 ruling closed for `maxLength`" | | `17469`, `11437` | Maintainer, 2026-09-13, option 1′: `multiple: true` refused outside the multi-capable types — the earlier `radio` rule generalised; the driver derives its JSON column from the spec predicate. | "Maintainer ruling of 2026-09-13, option 1′ (the earlier rule refusing an authored `radio` with `multiple: true`, generalised)" | | `objectui#8886`, `objectui#8937` | The console's related list shaped its parent filter from the spec predicate; its follow-up recorded the driver half as owed. | "the console's related list pinned the divergence on the consumer side when it began shaping that filter from the spec predicate, and its follow-up recorded the driver half as owed and not filed" | | `20078` | Triage, 2026-09-25, remedy A: refuse the traversal at authoring with a prescription; hydrating the field level is a capability of its own. | "Triage routed this on 2026-09-25 to remedy A: refuse the traversal at authoring, with a prescription." | | `18682` | A validation rule or visibility predicate reads one hop through a lookup. | "is served, one hop deep, and stays accepted" | | `7501` | `scale` enforced at write time: an over-scale write refused, never rounded. | "when `scale` was made enforced at write time (an over-scale write refused, never rounded)"; "the write-time `scale` enforcement" | **`hook-` (12 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4839`, `5049` (PR) | Both `session.roles` admin exemptions removed; the record lock and the delegation guard back on the one permission vocabulary. | "An earlier fix removed both readers, returning the record lock and the delegation guard to the one permission vocabulary" | | `4579`, `4657` | The `openApi31` and `activationEvents` retirements. | named by their surfaces, which the sentence already carried | | `3733` | Measured: a key removed from a non-strict schema parses clean and is silently dropped. | "as a removed field key was measured to be" | | `5050` | This retirement's own card. | trailing id dropped | | `4281` (PR) | An empty hook target is not "no target": closed at the two metadata doors. | "An earlier breaking fix established that an empty hook target is not "no target""; "that fix's headline failure mode" | | `5928` | The `excludeObjects` face (global except named objects); it declined to change the matcher's read in passing. | "The later `excludeObjects` face (a hook global except for the objects it names)"; "the `excludeObjects` change declined to do it in passing" | | `6573` | **404** — see Acceptance notes. | trailing id dropped; the entry states the change | | `4001` | The unknown-key strictness campaign (ADR-0078). | trailing id dropped (ADR-0078 kept) | | `3195` | The hook taxonomy collapsed from 18 events to the 8 dispatched; a registration guard warns on the rest. | "the change that collapsed the hook taxonomy to the eight dispatched events made this branch a warn" | | `17713` | This refusal's own card. | trailing id dropped | **`metadata-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `12057` | Maintainer, 2026-08-29: retirement adopted, re-scope rejected (the card's ruling comment is no longer on it; `main` records it). | "the maintainer's ruling of 2026-08-29 adopted retirement and rejected a re-scope" | | `13135` | **404** — see Acceptance notes. | "executed widened to the full coupling set the fork report on that ruling measured" | | `11513` | **404** — see Acceptance notes. | "the 2026-08-24 lock-and-clone ruling (lock the packaged base, customize a clone) left deliberately unchartered" | | `15542`, `15854` | Maintainer, 2026-09-06, ruled together (2 + A): every `endpoints.*` switch gates exactly the face its name states; the whole-store family gets its own key. | "The maintainer ruled the two together on 2026-09-06 as one principle: …" | | `15543` | No shipped boot path constructs a `RestServerConfig`. | the measurement was already in the sentence; the id is dropped | | `15624` | The outer cache keys read by nothing, retired on their own (the owning seat's ruling). | "(the owning seat's ruling, conditioned on the measurement below and re-taken on the merged ref)"; "retired on its own under ADR-0049" | | `14478` | Maintainer ruling B of 2026-09-02 on duration units. | "Maintainer ruling B of 2026-09-02 on duration-shaped keys"; "the duration-unit rename" | | `8586`, `8421` | Maintainer, 2026-08-14, jointly: remove `additionalTypes`, and refuse unknown `/meta` types by the static registry. | "maintainer ruling of 2026-08-14: remove the key, jointly with refusing unknown types at the `/meta` boundary by the static registry" | | `4212` | Four of five declared plugin lifecycle hooks were never invoked, `onInstall` among them. | "the plugin lifecycle's `onInstall` (a documented hook with no invocation site)" | **`dataset-` (9 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `19889` | Ruling A of 2026-09-24: the schema door refuses what the compile face refuses; a field spec with no `$` key stays undescended. | "ruling A of 2026-09-24, which made the schema door refuse what the compile face refuses, drew the line there" | | `20080` | Triage, 2026-09-25, remedy A: refine the two analytics carriers; remedy B (stop the analytics door descending) changes what a nested list means. | "Triage on 2026-09-25 routed the fix to the two analytics carriers instead, rather than stop the analytics door descending, which would change what a nested list means" | | `16737`, `16099` | The measured defect: `AVG()` over a datetime is an average year on SQLite and an error on Postgres. | the measurement was already in the sentence; the ids are dropped | | `16353` | The aggregate × field-type table, declared in the spec. | named by the table, which the sentence already carried | | `16099` (in `surface` and `acceptanceCriteria`) | The widening of the refusal to `sum` / `avg` over every field class, registered `not-required` against this entry. | "followed in a later change"; "widened by a later change" | | `17560` | Director ruling B of 2026-09-13: the compile door enforces the table for every aggregate. | "Director ruling B of 2026-09-13: …"; the sibling entry named by id | | `15768`, `16236` | `measureResultType` typing `min` / `max` over strings and over a `formula` return type. | the sentence already states both | | `17513` | Closed as a duplicate with zero rulings on it. | "the card it cited is closed as a duplicate with zero rulings on it" | ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` gains `field-`, `export-`, `api-`, `dataset-`, `hook-` and `metadata-` (17 prefixes; `data-` still selects neither `datasource-` nor `dataset-`, and `api-` does not select `apimethod-`: the match is `startsWith`). The `REWRITTEN` floor rises from **88 to 113** ids: the 25 entries this stage rewrote. The three `it` blocks are textually unchanged. The file keeps its stage-1 name; the header lists the seventeen covered families. ## Ablation — the widened pin can fail on a new-family block From committed state, HEAD `d24a253bd0`, in one lock turn, with `scripts/ablation-replace.mjs` in wrap mode (it owns the mutation's restore; 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 `reason` of `hook-register-undispatched-lifecycle-event-refused`: anchor `made this branch a warn — ` → `made this branch a warn (objectstack-ai#3195) — `. The tool read anchor 1 → 0 and replacement 0 → 1, blob `94bc4938` → `b045dbfd`. - **Mutate leg.** Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `hook-register-undispatched-lifecycle-event-refused: the printed guidance cites a tracker id: expected 'objectstack-ai#3195' to be undefined`. - **Restore.** Tool-proven: blob `94bc4938` == 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 **`d24a253bd0`**; every reading below was taken there. Every heavy run went through `scripts/pm/os-verify-lock.sh`, with per-step exit codes recorded separately. - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 58 successful, 58 total`; the ten packages outside that closure (for `check:dual-build-cjs-loads`) give `Tasks: 68 successful, 68 total`. - **Pin with its neighbour:** `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`). - **CLI unit:** `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts` give `Test Files 2 passed`, `Tests 28 passed`. - **Spec, the whole `local` project:** `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gives `Test Files 573 passed (573)`, `Tests 16801 passed | 1 todo`. **The `repo` project:** `Test Files 38 passed`, `Tests 690 passed`. - **Typecheck:** `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 **89** families. `--ran` over the recorded exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16735 customer-facing string(s) across 1168 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (311 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` ("no removed session.tenantId alias"), `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`. `check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`: ten packages had no `dist/`); after the ten were built it exits 0, and that is the reading recorded. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 27 changed `.ts` files reports 27 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 27 are in it (no file-ignored notice). - 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 one commit past the base, to `0bbe4005`: the landed `20511` (a `qa-` entry, a retired key and `registry.ts`). A driver-free bare-clone `merge-tree --write-tree` of `d24a253bd0` against `0bbe4005` exits 0 with no conflicted path, and the census over that merged tree reads 0 sites in all six families (whole tree 461), so `main` was not merged in; CI's merge ref runs the registry gates on the merged tree. ## Acceptance notes - **Four dead ids, rewritten from what `main` records.** Each answers 404 on both the issues and the pulls endpoint, with a 200 control (`14478`). - `6148`: `scripts/check-adr-0087-registration.mjs`'s header (a declared-breaking changeset must state its ADR-0087 disposition in writing) — the same reading stage 4 used. - `6573`: the objectql CHANGELOG entry "`engine.registerHook` refuses an empty `object` target and a scope whose two faces cancel out", and `engine.ts`'s refusal docblocks. It is this entry's own change, so the trailing id is dropped; the entry already states the change. - `13135`: `packages/metadata/CHANGELOG.md` ("retire the paper metadata-customization protocol with its full coupling set … re-charter of" the `12057` ruling, "the maintainer adopted retirement … 2026-08-29 … and" it "charters the full coupling set the fork report measured"). - `11513`: ADR-0126 names it the lock-and-clone ruling of 2026-08-24 (Salesforce-style: lock the packaged base, customize a clone), and its section 9 records the per-field overlay layer as explicitly not chartered. - **One ruling read from `main`, not from its card.** `12057` answers 200 but carries only its triage comment; the 2026-08-29 ruling the entry names is recorded in `packages/metadata/CHANGELOG.md`, and the sentence says only what that record says. - **Short numbers, ruling-record ids and acknowledgements went too (invisible to the regex).** Eleven decision-batch numbers (`objectstack-ai#43` ×2, `objectstack-ai#59` ×2, `objectstack-ai#122`, `objectstack-ai#127`, `objectstack-ai#128`, `objectstack-ai#145`, `objectstack-ai#215`, `objectstack-ai#218`, `objectstack-ai#221`) and five ruling-record comment ids (in `field-currency-scale-refused`, `field-predicate-reference-traversal-refused` and `dataset-filter-nested-relation-equality-array-refused-at-save`) are numbers an author is shown and cannot follow, so each is dropped. So are five maintainer acknowledgements (「同意,其他也同意」 in `api-assembled-entry-split`, 「同意」 ×3 in `export-job-family-retired`, 「同意」 in `metadata-customization-protocol-retired`): they record only that a batch was approved, and each sentence now states the ruling's date and content instead. The two quotes that carry the lesson stay verbatim: "both legs, table in spec" and 「`min`/`max` numeric plus `date`/`datetime`; everything else refused」. - **Three `Prime Directive objectstack-ai#12` / `PD objectstack-ai#12` spellings are kept.** They name a rule in this repository's AGENTS.md, not a tracker item, like the ADR ids; the earlier stages kept the same spellings in the `ui-`, `plugin-` and `system-` families. - **`surface`, per ruling A.** `18.dataset-measure-aggregate-field-type-refused` was the only entry of these families whose `surface` carried tracker ids (two). Its header now names the later widening and the sibling entry in words, and the AST comparison shows nothing else in it moved. No test or tool reads that `surface`: outside the migration tree, the pin and the generated projections, the id appears only in three earlier changesets' ADR-0087 disposition markers (and this PR's changeset). - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** A scan of the six families' evaluated prose for any run of three or more digits and for `issue` / `card` / `PR` / `batch` / `record` / `summon` / `item` plus a number now finds only HTTP statuses, ports, byte counts, durations, dates, commit shas, SQLSTATE and TS error codes, ADR ids and example values. - **No test pinned a removed tracker number of these entries.** A search of test files for the 25 entry ids finds only the pin, `export-job-family-retirement.test.ts` (it asserts `not a D2 conversion` and the backtick-free `surface`, both unchanged) and comment lines; a search for the 75 cited numbers in `toMatch` / `toContain` assertions finds only unrelated digit runs and runtime strings outside this card (`api-endpoint-step.test.ts` and `endpoint-executor.test.ts` assert a `5040` hint, which is objectstack-ai#20513's surface). - **No open PR touches these six families.** Read twice: at the start of this stage, 10 open PRs and 634 file rows; again just before opening this one, 13 open PRs and 633 rows (the Version Packages PR `17076` included both times). None carries a `migrations/entries/semantic/NN.(field|export|api|dataset|hook|metadata)-*` file. PR `20512` adds a retired-key file `18.api__RestApiConfig__documentation.version.ts`; a retired key has no id and is not in `step.semantic`, so the widened `api-` prefix does not select it. PRs `20512`, `20504`, `20460` and `20458` add entries in other families (`rest-`, `turso-`, `stack-`, `cube-`) and regenerate `registry.ts`: ordinary concurrency, regenerate on merge. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1–4; only the protocol-17 entries appear in them. - **What later stages pick up** (whole tree at this head, same instrument): **461** prose-field sites in the other families, **50** short numbers, **5** `surface` sites. ## Line budget Entry files: **265 changed lines** (+147 / −118) across 25 files, against the stage-1 ≈400 budget. The whole diff is **631 lines** (+372 / −259) in 30 files. Of the rest, `registry.ts` is 265, the two projections are 44 (`spec-changes.json` 24, the upgrade guide 20), the widened pin is 29 and the changeset 28. --- _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
…mes a known repository, so pre-#N / post-#N are judged and framework#N is this repository (objectstack-ai#20554) Fixes objectstack-ai#20330 Clause-②: no ## What changed `scripts/check-issue-citations.mjs` read ANY `word#N` as a repository reference, so `pre-objectstack-ai#12248`, `post-objectstack-ai#6640`, `Pre-#N`, `POST-#N` and `Framework#N` were classed cross-repo and never judged. Its qualifier is now a **closed set**. A token joined to `#N` names a repository only when it is an `owner/repo` form or a name in the new `KNOWN_REPOSITORIES` table, matched case-insensitively. Any other prefix is prose, and the number after it is this repository's and is judged. One recogniser, `repositoryOf`, is asked in all three places: by extraction, by the board's probe set (`boardWanted`) and by the classifier (`namesThisRepository`). The three can no longer disagree about a token. - `KNOWN_REPOSITORIES` rows: `objectstack` and `framework` (both THIS repository), `objectui`, `ui` (an objectui alias, joined form only), `cloud`, `hotcrm`, `hotcrm-heimao`, `os-tianshun-mtc` and `os-project-titanwind-ehr`. Each row carries the measurement that put it in. - **Prose form.** `objectui PR objectstack-ai#10264`, `objectui objectstack-ai#2670`, `cloud objectstack-ai#2937` and `framework objectstack-ai#2679` read as their repository. The form is a known repository name, whitespace, an optional `PR` or `issue`, then `#N`. The alias `ui` is excluded from it, because `UI #N` is ordinary English. - **No carry across a pair.** In `objectui#6110 + objectstack-ai#6111` the second number stays this repository's. The convention is to qualify each number, and the one live site in the claim's surface is respelled: `packages/spec/src/data/field.zod.ts:370` now reads `objectui#6110 + objectui#6111`. This is comment-only, with a `patch` changeset. - **Ordinal heads.** Closing the set exposes ordinals that were hidden behind a fake qualifier. `NON_CITATION_HEADS` gains `OQ` (`ADR-0076 OQ#10`, 10 sites) and `PKCS` (`PKCS#11`, 1 site). A hyphen joining a head to its `#` is now read as the same head (`Prime-Directive-objectstack-ai#12`, 2 sites). `PD#12` (8 sites) was already covered by the existing `pd` head once its candidate is refused. - **Refusal text.** The `REMEDY` text now states the grammar that judged the author. ### A false red in the same seam, fixed because this change would have widened it `buildBoard` probed only UNQUALIFIED numbers, so a diff adding `objectstack#N` was classified against a board that never asked about N. Reproduced on unmodified `288611e3e5`: I appended `objectstack#20330` (this live card) to a swept file and ran `node scripts/check-issue-citations.mjs`. It exited **2**, reading `board: probed (0 citations)` and `[allocated-but-absent] ... objectstack#20330`. Reading `framework#N` as this repository would have inherited that false red on every site. The probe set is now `boardWanted`, meaning every citation judged here. The self-test pins it through `probeBoard` over a stub. The blocking rule is unchanged: findings still exit 2. ## Measurements the design rests on - **`framework` names this repository.** `git ls-remote https://github.com/objectstack-ai/framework` answered HEAD `288611e3e5`, identical to `objectstack-ai/objectstack`. The controls diverged: a nonexistent name under the same owner exited 128, and `objectstack-ai/objectui` answered its own HEAD `0eb9f36aca`. The REST and web routes to `framework` answered 403 from this session's proxy (bound to configured repositories), so git's rename redirect was the readable instrument. `framework#N` / `Framework#N` therefore read as THIS repository. - **Qualifier census on `288611e3e5`, over the declared surfaces.** There were 27 distinct candidates behind 1,655 sites: - This repository: `framework` 255, `objectstack` 111, `objectstack-ai/objectstack` 9, `Framework` 2. - Siblings: `objectui` 672, `cloud` 213, `hotcrm` 24, `objectstack-ai/objectui` 12, `ui` 11, `objectstack-ai/cloud` 9, `better-auth/better-auth` 3, `os-tianshun-mtc` 2, and 1 each of `hotcrm-heimao`, `os-project-titanwind-ehr`, `objectstack-ai/objectos`, `objectstack-ai/ats` and `objectstack-ai/hotcrm`. - Prose: `pre-` 284, `post-` 12, `Pre-` 4, `Post-` 4, `PRE-` 1 and `POST-` 1. - Ordinals: `OQ` 10, `PD` 8, `Prime-Directive-` 2, `PKCS` 1. - **`ui` is objectui.** `objectstack-ai/ui` does not exist, and `ui#6837`, `ui#6206` and `ui#6207` are objectui's records on its board (`objectstack#6206` answers 404, so it would have been a false death). - **Prose form, 27 sites.** For every objectui number I read objectui's board and this repository's. The objectui record is the one each sentence describes: `objectui objectstack-ai#2670` is "Flow designer: render loop / parallel / try_catch as nested", cited from `loop-node.ts`, and `objectui PR objectstack-ai#4264` diagnoses a path on the right side of `==`, cited beside `PATH_SHAPED_LITERAL`. This repository's same number is unrelated on every site. The `cloud` sites could not be read (private) and follow their context. There were zero false positives. - **Pair carry, 48 sites. The measurement refuses a carry rule.** - `,` and `and`: every cross-repo pair I could judge names THIS repository's second number. `cloud#1013 and objectstack-ai#10645` is this repository's cli `serve` issue (4 sites), `cloud#1020, objectstack-ai#5233` its org gate issue (6 sites), `objectui#2561, objectstack-ai#3021` its lazySchema PR, and `objectui#3136 and objectstack-ai#14492` answers 404 on objectui. - `/`: mostly carries, but not always. `objectui#3226 / objectstack-ai#4827` is this repository's objectstack-ai#4827, a conversion entry handed over from objectui and cited from `conversions/registry.ts`. - `+`: exactly one distinct pair exists in the corpus, which is too thin to establish a convention. ## Census of the newly judged spellings Taken with the gate's own `--census --json` at `a3c14755f8` against an enumerated board (184 pages, frontier objectstack-ai#20551). The per-site transition comes from the gate's `--list` before and after the change. Dead means `allocated-but-absent`. Four of the numbers (14657, 12998, 10194 and 8692) were re-probed directly and answered 404. | spelling | sites now judged | dead | |---|---:|---:| | `pre-#N` | 284 | 26 | | `framework#N` | 255 | 0 | | `post-#N` | 12 | 0 | | `Pre-#N` | 4 | 1 | | `Post-#N` | 4 | 0 | | `Framework#N` | 2 | 0 | | `PRE-#N` | 1 | 0 | | `POST-#N` | 1 | 0 | | **total** | **563** | **27** | Other readings, same run: - **Newly deferred (25 sites):** the 24 prose-form sites plus the respelled `field.zod.ts:370`. Four of them were base census deaths that were never deaths: `objectui PR objectstack-ai#8758` three times, and the respelled `objectstack-ai#6111`. - **No longer extracted (21 ordinals):** `OQ#10` ×10, `PD#12`/`PD#10` ×8, `Prime-Directive-objectstack-ai#10`/`objectstack-ai#12` ×2 and `PKCS#11` ×1. - **Whole-census tally:** - Base `288611e3e5`: 38,109 judged. resolves 32,202 · resolves-as-pull 1,863 · cross-repo-unjudged 1,535 · allocated-but-absent 2,509. - Branch `a3c14755f8`: 38,088 judged. resolves 32,689 · resolves-as-pull 1,891 · cross-repo-unjudged 976 · allocated-but-absent 2,532. - The board moved between the two runs, so the per-site transition above is the reading, not the tally difference. The cross-repo count reconciles exactly: 1,535 − 563 − 21 + 25 = 976. ## Verification All gates below ran at `a3c14755f8`, the branch head. - **Self-test.** `node scripts/check-issue-citations.mjs --self-test` exits 0 with 114 cases across 8 batteries (base: 73 cases across 7). The new battery `qualifier` (floor 40) pins every spelling both ways: lit on a live number and a FINDING on a dead one for `pre-` `post-` `Pre-` `Post-` `PRE-` `POST-`, and for `framework` `Framework` `objectstack-ai/framework` `objectstack`. It also pins cross-repo even when dead for `objectui` `OBJECTUI` `ui` `cloud` `hotcrm` `objectstack-ai/objectui` `better-auth/better-auth`, and covers: - an unknown word prefix read as prose; - the four ordinal heads; - the prose form, lit and dead, including `PR #N` and `UI #N` NOT being the prose form; - the pair, no carry (dead second number red) and qualified number by number (both deferred); - `boardWanted` and a probed-board resolution of `objectstack#20330`; - registry hygiene. `live-corpus` gains a pin that every qualifier the live corpus keeps names a repository. - **Ablations.** Six mutations went through `scripts/ablation-replace.mjs`, each landing on disk with the anchor count 1 → 0 and the blob changed, each red on its own case, and each restored with blob equal to HEAD `8b6cf12653dd` and `git diff HEAD` empty: - M1: the recogniser returns any bare candidate. The self-test reds on "`pre-` is prose". - M2: the probe set reverts to unqualified-only. It reds on "the board's probe set". - M3: the `framework` row is renamed. It reds on "`Framework` is THIS repository". - M4: the hyphen head is off. It reds on "`Prime-Directive-objectstack-ai#12`". - M5: the prose form is off. It reds on "`objectui PR #N` names objectui". - M6: the `OQ` head is removed. It reds on "`ADR-0076 OQ#10`". - **Diff-scoped verdict** (`node scripts/check-issue-citations.mjs`, as CI runs it): exit 0 on this branch. It judged the respelled line's 2 citations, both `cross-repo-unjudged`. - **One-time end-to-end proof** (no permanent test; injected uncommitted and restored with blob equal to HEAD and `git diff HEAD` empty). I appended `pre-objectstack-ai#12248` (dead) and `framework#20330` (live) to `packages/cli/src/commands/generate.ts` and ran the diff verdict twice: - The branch gate exits **2**. `pre-objectstack-ai#12248` is `allocated-but-absent`, `framework#20330` resolves on a board `probed (2 citations)`, and the respelled pair stays cross-repo. - The `288611e3e5` gate, from a temporary copy, exits **0** with all 4 `cross-repo-unjudged`. That is the hole this closes. - **Census** (`--census --json`): exit 0. It is report-only and never fails. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 97 commands, and all 97 ran. 96 exit 0, including `pnpm check:pm-dispatch-gates`, whose `dispatch-gates.mjs --self-test` passes 1,976 cases. That self-test pins this file's `:204 local-env` declaration line, and every edit here stays below it or is line-neutral. `--ran` reconciliation reads "97 derived famil(ies) accounted for — 96 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)". - **NOT MEASURED: `check:dual-build-cjs-loads`.** Reason: it loads every package's built CJS entry, and this box holds no dist for 80+ packages. The diff changes no emitted code. `@objectstack/spec` was rebuilt, and its entry gates (`check:browser-reachable-entries`, `check:entry-nameability`) exit 0. CI's Lint and Repo Gates owns this one. - **Spec package.** `pnpm --filter @objectstack/spec typecheck` returned VERDICT command-exit 0. See the report comment for the test run. ## Acceptance notes - **Dead sites the census hands over, not rewritten here.** One is under `packages/spec/src/**` and is input for the staged sweep on objectstack-ai#20234: `packages/spec/src/meta-spelling/manifest-collection-spelling.ts:71` `pre-objectstack-ai#10194`. The other 26 are outside `packages/spec/src/**`, and no card names them: - `packages/cli/src/commands/generate.ts:1842` `pre-objectstack-ai#14657` - `packages/cli/src/utils/storage-driver.ts:206` `pre-objectstack-ai#6345` - `packages/drivers/driver-sql/src/schema-drift.ts:2458`, `:2474` `pre-objectstack-ai#12998` - `packages/drivers/driver-sql/src/sql-driver.ts:3872`, `:16545` `pre-objectstack-ai#17590` - `packages/drivers/driver-sql/src/sql-driver.ts:18285`, `:18324` `pre-objectstack-ai#12998` - `packages/drivers/driver-sql/src/sql-driver.ts:20095` `Pre-objectstack-ai#12380` - `packages/drivers/driver-turso/src/remote-transport.ts:2624` `pre-objectstack-ai#12380` - `packages/lint/src/validate-searchable-fields.ts:312` `pre-objectstack-ai#8404` - `packages/metadata-protocol/src/protocol.ts:2793` `pre-objectstack-ai#10888` - `packages/metadata-protocol/src/seed-loader.ts:1947` `pre-objectstack-ai#11674` - `packages/objectql/src/action-governance.ts:339` `pre-objectstack-ai#14423` - `packages/plugins/plugin-auth/src/auth-manager.ts:5629` `pre-objectstack-ai#14762` - `packages/plugins/plugin-security/src/bootstrap-platform-admin.ts:266`, `:630` `pre-objectstack-ai#8692` - `packages/plugins/plugin-security/src/per-organization-catalog.ts:314` `pre-objectstack-ai#8692` - `packages/plugins/plugin-security/src/permission-set-projection.ts:482` `pre-objectstack-ai#6483` - `packages/plugins/plugin-sharing/src/backfill-sys-record-share-organizations.ts:5` `pre-objectstack-ai#14484` - `packages/runtime/src/domains/mcp.ts:364` `pre-objectstack-ai#8726` - `packages/runtime/src/sandbox/body-runner.ts:548`, `:735` `pre-objectstack-ai#14758` - `packages/runtime/src/sandbox/script-runner.ts:440` `pre-objectstack-ai#14758` - `packages/types/src/driver-error-classification.ts:608` `pre-objectstack-ai#13324` - `packages/types/src/node.ts:1428` `pre-objectstack-ai#10943` - **The same `objectui#6110 + objectstack-ai#6111` pair outside the claim's surface.** It still reads `objectstack-ai#6111` as this repository's (404 here), so these are existing census deaths: `packages/spec/src/ui/view.zod.ts:3634` (for the objectstack-ai#20234 sweep), `packages/metadata-core/src/form-predicate-root-policy.ts:14`, `:120` and `:205`, and `packages/metadata/src/plugin.ts:910`. Each respells to `objectui#6110 + objectui#6111`. - **Pairs that read silently wrong, not dead.** Several `REPO#N / #M` pairs name the qualifier's own second number, which resolves here as an unrelated record. Examples: `hotcrm-heimao#35/objectstack-ai#40/objectstack-ai#59`, `objectui#2715/objectstack-ai#2717`, `objectui#2711/objectstack-ai#2722`, `objectui#4648/objectstack-ai#4901`, `objectui#5018 / objectstack-ai#6469`, `cloud#957 / objectstack-ai#962` and `cloud#930/objectstack-ai#944`. No gate can see these, because they resolve. The convention in the refusal text (qualify each number) is the remedy when someone next touches the line. - **Seat 4's `objectui PR objectstack-ai#10264` specimen.** `packages/spec/src/api/export-job-family-retirement.test.ts:25` sits on a DEFERRED surface (`packages/**/*.test.ts`), and `surfaceFor` answers `null` for it. The census never judged that site. The prose form it names is now read correctly wherever the census does look. - **Observed once: a truncated board enumeration accepted as a reading.** My first branch `--census` read `enumerated (126 pages)` with frontier objectstack-ai#13977, against 184 pages and objectstack-ai#20551 on the re-run minutes later, and reported 9,160 `never-issued` phantoms. `enumerateBoard` stops at the first page without `rel="next"` and trusts the maximum it saw as the frontier. This diff does not touch that code. The diff-scoped verdict enumerates only past 400 distinct numbers. Recorded, not filed; the seat decides. - **Scope declaration.** `NON_CITATION_HEADS` (two rows) and `nonCitationHead` (the hyphen) are grammar next to the qualifier, not the qualifier itself. They are here because closing the qualifier made those 13 ordinal sites judged citations of this repository's objectstack-ai#10, objectstack-ai#11 and objectstack-ai#12, which is false. No gate was added, and the diff-scoped blocking rule is unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 29, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…commits that decided them (objectstack-ai#20729) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the eighth stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/services/service-analytics/src/**` and nothing else. By the seat's census at the claim (`5899485578`), it is the largest package in the lane that no in-flight work holds. Later stages cover the other packages, so this PR says `Part of` and the card stays open. 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), by the method of stages 1 to 7 (PR objectstack-ai#20609 as `422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as `9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`). That is **76 sites on 76 lines in 22 files, covering 14 numbers**: - 42 census sites (every census site this package has); - 34 sites in test comments, which the census defers. The raw scan found no dead site the gate's grammar cannot see (see Acceptance notes), so there is no third class this time. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and says in its own words what was decided: **13 distinct shas**. No number in this package has an ADR or ruling record of its own in the repository (a grep of `docs/adr/` for all 14 finds none, and a grep of the rest of `docs/` finds none either), so every anchor is a commit, per ruling C's order. No number was dropped. Only comments changed. Every touched source file keeps its line count (78 lines out, 78 in, over 22 files), so no line citation into these files moves. 2 of those 78 lines hold no dead citation: they are reflow lines, listed under Wordings below. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces: `objectstack-ai#10861` (5 lines), `objectstack-ai#12776` (3), `objectstack-ai#10413` (2), `objectstack-ai#16750` (2), and `objectstack-ai#10759`, `objectstack-ai#11152`, `objectstack-ai#5716` and the decision-batch ordinal `objectstack-ai#59` once each. Each tracker number among them resolves. Over the whole diff, added minus removed is 0 or negative for every number, and no number is new to the diff. No PR number is the citation on an added line: the two `PR #N` spellings in scope became their pull request's squash commit, and `objectstack-ai#16750` stays only as the convenience link beside `ed7243d52`, on the line it already stood on. Eight dead sites are left on purpose, all of them test strings (see the list below). One more file: a `patch` changeset for `@objectstack/service-analytics`, because the rewritten docblocks and inline comments ship (see Changeset below). The `AnalyticsResultWithDrill` type and its four sidecar members are not touched: its docblocks carry no dead number (`objectstack-ai#20644`, `objectstack-ai#3214` and `objectstack-ai#1752` all resolve). ## Census: `service-analytics`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/services/service-analytics/`. Each run counts as a reading only because its board frontier equals the newest issue number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | service-analytics sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `cbaf04c1f`, run 2026-09-29T21:41:53Z to 21:45:05Z | enumerated, 186 pages, frontier objectstack-ai#20721 (newest objectstack-ai#20721 before and after), 18,548 numbers | 1,161 | **42** | 42 | 10 | 10 | | after | head `967d73531`, run 21:55:23Z to 21:58:36Z | enumerated, 186 pages, frontier objectstack-ai#20723 (newest objectstack-ai#20723 before and after), 18,550 numbers | 1,119 | **0** | 0 | 0 | 0 | The before count matches the seat's census at the claim and A1 (42 sites): the two comments PR objectstack-ai#20712 rewrote in `analytics-service.ts` did not move it. The whole-repo drop is 42, exactly this diff's census sites. The `resolves` tally is 32,991 in both runs, and `resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did not move either. The after run was taken on `967d73531`; the head `82d2b40b2` adds only the changeset. No run was truncated or discarded: both enumerations read 186 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `service-analytics/src` (162 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when that census judged it on this board anywhere (its `--list` extraction) and did not report it. The 21 numbers the census never saw, because they stand only in test files or strings here, were read one by one on the issues endpoint: 17 answer 200, and `objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#16918` and `objectstack-ai#17125` answer 404. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `cbaf04c1f` | 3,514 | **84** | 42 | 34 | 0 | 8 | | after, `967d73531` | 3,438 | **8** | 0 | 0 | 0 | 8 | Its src-comment column equals the census's 42, which is the control on the second instrument. The 3,410 live citations and the 20 cross-repo citations are the same in both readings, and the drop of 76 citations is exactly the rewritten sites. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 3,598 occurrences and 84 dead before, 3,522 and 8 after; its residue equals the gate's residue site for site, and it sees no dead site beyond the gate. ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject, and `git blame` at the base puts every rewritten line in its anchor commit or in a later commit that descends from it (`merge-base --is-ancestor` exit 0 for each pair). | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#11461` | 20/2 | 19/1 | `399ecad58`: a cross-object leaf in one measure's own `filter` (the third producer, lowered onto `aggregations[].filter`) is refused on both ObjectQL doors with `INVALID_FIELD` / 400 naming the measure, folded into the one member view, with insertion order keeping every earlier refusal's message. The last line of its message names `objectstack-ai#11461` as the card it settles. New to the sweep | | `objectstack-ai#17130` | 17/5 | 13/4 | `54b3d1d4a` (PR objectstack-ai#17336): the row-scope resolution refusals carry `READ_SCOPE_COMPILE_FAILED` / 500 through one constructor, so `queryDataset`'s catch re-throws them instead of reading their words, every message byte-unchanged; plus the source-derived wording-collision guard. Named in its diff only (18 added lines carry the tag). New to the sweep | | `objectstack-ai#17124` | 12/8 | 10/2 | `86c505286` (PR objectstack-ai#17593): `explicitDateRangeWindow` is the one reading of `dateRange`'s array arm on all four faces, and an array that is not two string bounds is refused with `ANALYTICS_DATE_RANGE_UNRECOGNIZED` / 400. Named in its diff only (its changeset file is `17124-daterange-array-arm-arity.md`). New to the sweep | | `objectstack-ai#12209` | 10/5 | 10/0 | `017130a09` (PR objectstack-ai#12318): a custom-SQL measure is refused on the ObjectQL aggregate path with `INVALID_FIELD` / 400, keyed on the `EXPRESSION_METRIC_TYPES` partition shared with `NativeSQLStrategy`. Its message records the two failure modes the lines describe (`driver-sql` blaming a `function` key, the in-memory evaluator answering `null` per bucket). Named in its diff only. New to the sweep | | `objectstack-ai#16778` | 5/1 | 4/1 | `357f4992b`: the compile-leg refusal of an aggregate a datetime measure's field type cannot carry, scoped to temporal source fields. The squash commit of the pull request that was `objectstack-ai#16778`; its subject carries the number. New to the sweep | | `objectstack-ai#12940` | 4/2 | 4/0 | `aa16721b6` (PR objectstack-ai#13361): this package's consumer-local `executeAggregate` config mirrors (the plugin options and `AnalyticsServiceConfig`) narrow `aggregations[].method` to `AggregationFunction`, after `objectstack-ai#12776` narrowed the contract. Named in its diff only. New to the sweep | | `objectstack-ai#17015` | 4/2 | 4/0 | `0da638cd9`: the closed `dateRange` preset vocabulary is lowered once and the rest refused, the `[range, range]` fallback is removed from the faces it reached, and the shared conformance kit holds them. The squash commit of the pull request that was `objectstack-ai#17015`. New to the sweep | | `objectstack-ai#16860` | 3/1 | 3/0 | `041d9fdc6`: the object-level read grant is asked at the analytics door, and its bridge to the `security` service resolves an explicit three-way (absent admits; throwing or method-less denies at `error`, finding F3 in its message). The squash commit of the pull request that was `objectstack-ai#16860`. New to the sweep | | `objectstack-ai#12248` | 2/1 | 2/0 | `8425c17cc`: the five ruled engine members, `getDriverForObject?` and `resolveEffectiveDatasource` among them, adopted onto `IDataEngine`, and `getObject` typed. Its subject names it. Stage 5's and the spec stage's anchor | | `objectstack-ai#16685` | 2/2 | 2/0 | `ed7243d52` (PR objectstack-ai#16750): `boolean` / `toggle` accepted for `sum` / `avg` / `min` / `max` in the aggregate × field-type table, holding maintainer ruling `objectstack-ai#11152`. Its subject names it. The spec stage's anchor | | `objectstack-ai#17125` | 2/2 | 2/0 | `5d12b16e7`: the row-scope bridge tells an absent security service from a broken one, so a broken one refuses the query. The squash commit of the pull request that was `objectstack-ai#17125` (404 on the pulls endpoint too). New to the sweep | | `objectstack-ai#16918` | 1/1 | 1/0 | `5d12b16e7`: the same commit. Its changeset's headline names `objectstack-ai#16918` as the card it answers, and its diff writes the line (`admission-bridge-resolution.test.ts:120`) | | `objectstack-ai#6123` | 1/1 | 1/0 | `59d1933f9`: `err.code` lands at `error.code`, not `error.details.code`; the commit that wrote this very line. The `runtime` stage's anchor | | `objectstack-ai#13279` | 1/1 | 1/0 | `6a180e42d`: permission-store read failures fail loud, and the same commit renames `metadata/src/utils/schema-sync-errors.ts` to `packages/types/src/driver-error-classification.ts`, the move the line describes. The anchor of stages 2, 5 and 6, and of the `types`, `rest` and `runtime` stages | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each of the 13), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13; control leg: stage 1's landing `422db788a` exit 0; the history is complete, `--is-shallow-repository` false, 15,135 commits). Each of the 14 numbers answers 404 on the issues endpoint, read one by one; `objectstack-ai#16778`, `objectstack-ai#16860`, `objectstack-ai#17015` and `objectstack-ai#17125` answer 404 on the pulls endpoint too. ## Wordings to check - **Bracket tags.** `[#N]` became `[commit SHA]`, as in stage 7; `[objectstack-ai#10861 / objectstack-ai#11461]` and `[objectstack-ai#10861, objectstack-ai#11461]` keep the live `objectstack-ai#10861` beside the new sha. - **The boolean rows, `measure-result-type.ts:115-116` and `aggregate-datetime-measure-refusal.test.ts:65-66`.** 「objectstack-ai#16685 ruled A, landed as objectstack-ai#16750」 and 「objectstack-ai#16685 was ruled A and objectstack-ai#16750 added」 became 「commit ed7243d (objectstack-ai#16750) added those rows」 and 「commit ed7243d (objectstack-ai#16750) added」. 「ruled A」 named an option on the dead card; `ed7243d52`'s message records the decision itself. Line 116 of the first file and line 66 of the second are the 2 reflow lines: each keeps the `objectstack-ai#16750` it already carried. - **PR numbers, `read-scope-resolution-envelope.test.ts:25` and `refusal-wording-collision.test.ts:21`.** 「PR objectstack-ai#17125's refusal」 became 「Commit 5d12b16's refusal」, the pull request's squash commit. - **`read-scope-refusal.ts:29`.** 「objectstack-ai#17130 exists to remove it」 became 「commit 54b3d1d was made to remove it」, the form stage 6 used. - **`refusal-wording-collision.test.ts:49`.** 「the exact move objectstack-ai#17130 forbids」 became 「the exact move commit 54b3d1d ruled out」; its message says the fix is the declaration, not a luckier string. - **`read-scope-resolution-envelope.test.ts:161`.** The verb after the number moved from present to past tense with the sha. - **`measure-expression-both-strategies.test.ts:45` and `:166`.** 「deleting the objectstack-ai#12209 arm in」 became 「deleting the arm commit 017130a added in」, and 「every objectstack-ai#12209 refusal」 became 「every custom-SQL refusal (commit 017130a)」. - **`dataset-executor.ts:609`.** 「objectstack-ai#17015's kit」 became 「commit 0da638c's kit」, the conformance kit that commit built. - **`plugin.ts:116`.** 「and in objectstack-ai#12209:」 became 「and in commit 017130a:」, whose message records the two ways the engine failed. - **`analytics-service.ts:238`.** 「objectstack-ai#13279 moved it there」 became 「commit 6a180e4 moved it there」; that commit's diff is the rename. ## The 8 sites left - **Test strings, 8 sites**, left as stages 1 to 7 left theirs, all `describe` / `it` titles: - `crossobject-conjunct-refusal.test.ts:589` (`objectstack-ai#11461`); - `aggregate-nontemporal-measure-refusal.test.ts:243` (`objectstack-ai#16778`); - `date-range-array-arm-arity.test.ts:213` and `:294` (`objectstack-ai#17124`); - `read-scope-resolution-envelope.test.ts:155`, `:199` and `:226`, and `refusal-wording-collision.test.ts:336` (`objectstack-ai#17130`). - There is no operator string, generated file or quoted ruling carrying a dead number in this package. It has no generated file at all. ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes never visited, base `cbaf04c1f` against head. Template literals are therefore read in context. It ran over all 22 touched `.ts` files. - Real run: 26,705 base leaf tokens, **0 files with a token change** (exit 0). - Comment control in `plugin.ts` (「refusal buys is in」 to 「refusal earns is in」): 0 files changed, as expected (exit 0). - Positive control, a code token added in `plugin.ts` (`field: a.field,` given `as string`): DIFFER (exit 1). - Positive control, one digit changed inside a kept test title (`date-range-array-arm-arity.test.ts:213`): DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs`, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (`ad3dc9fff4d3`, `a606ffbb6ead`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/service-analytics` (`.changeset/20596-service-analytics-provenance-anchors.md`) is included. Its body is stage 7's, word for word, with the package name changed. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After the build (a cache miss for this package, so `dist` is this head's source), the rewritten comments reach `dist`: `399ecad58` 6 times in each of `dist/index.js`, `index.cjs`, `index.d.ts` and `index.d.cts`; `86c505286` twice in each JS file and once in each declaration file; `54b3d1d4a` once in all four; `aa16721b6` once in each JS file and twice in each declaration file; `017130a09` once in each JS file. Positive controls: the unchanged line 「none of the coverage: a compiled measure's own」, in the same docblock as the shipped rewrite at `objectql-strategy.ts:744`, is found once in each of the four files, and the unchanged line 「back into line. Widening it here again would not be a local matter」 beside the shipped rewrite at `analytics-service.ts:559` once in each declaration file. A never-written negative phrase appears nowhere in `dist`. None of the 14 dead numbers is left anywhere in `dist`. ## Gates (head `82d2b40b2`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` (self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 11 citations across 10 files; 10 resolve and 1 resolves as a pull request (`objectstack-ai#16750`, the convenience link that already stood on its line). - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `82d2b40b2` derived 62 commands: all 56 derived at dispatch, plus `check:engine-double-contract`, `check:objectql-double-limit`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. Each ran with its exit code captured before any pipe, and all 62 exit 0. `--ran`, fed each command with its exit code, reports 62 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in directories this diff touches): `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit 0. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/service-analytics test`: 137 files pass and 3,216 tests pass. That is every test file in the package, the 12 touched ones included. - `pnpm --filter @objectstack/service-analytics typecheck` exits 0 (`tsc --noEmit` on `tsconfig.json`). `--listFiles`: the program holds all 162 files under `src/`, the 137 test files and all 22 touched files included. - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 22 touched `.ts` files gives 22 files, 0 errors and 0 warnings. All 22 are in eslint's own population (`isPathIgnored` is false for each; a `dist` file, as the control, is ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 23 changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked.** `CITATION_RE` refuses a hyphen after the digits and a `/` before the `#` (objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word 「option」. In this package: - `#N-word`: 8 lines by a plain grep, and 7 once a hyphen before the `#` is excluded too, which is the claim's 7. The eighth is 「pre-objectstack-ai#10413-phase-2」 (`execution-context-bridge.test.ts:223`). The numbers, `objectstack-ai#10413`, `objectstack-ai#5298`, `objectstack-ai#13570` and `objectstack-ai#13640`, all resolve. - `#A/#B`: 29 lines, the claim's 29, over 28 distinct numbers. All resolve; `objectstack-ai#2149`, which the census never judged, was read on its own. - `option #N`: none. So nothing here needed a rewrite beyond the gate, and the raw scan agrees. - **「This card」 phrases are left.** 113 lines in 39 files of this package speak of 「this card」, 「that card」 or 「the card」. They carry no number, neither instrument sees them, and most sit in blocks whose numbers still resolve. Stage 7 rewrote two such lines as lost referents; here none is changed, because the phrase runs through the whole package and rewriting a subset would be arbitrary. - **Prose that names `queryDataset`'s catch, not changed.** Nine comment lines say `queryDataset`'s catch. Since `10c36cc43` that catch sits in the private `answerDataset`, whose docblock calls it the body of `queryDataset`, so the lines still hold at the level of the public method. This is not a dead citation, so it is outside this stage. - **The census instrument did not truncate in this stage.** Both enumerations read 186 pages at the newest frontier. - **Anchors the next stages can reuse**, each checked here: `objectstack-ai#11461` → `399ecad58`; `objectstack-ai#17130` → `54b3d1d4a`; `objectstack-ai#17124` → `86c505286`; `objectstack-ai#12209` → `017130a09`; `objectstack-ai#16778` → `357f4992b`; `objectstack-ai#12940` → `aa16721b6`; `objectstack-ai#17015` → `0da638cd9`; `objectstack-ai#16860` → `041d9fdc6`; `objectstack-ai#17125` and `objectstack-ai#16918` → `5d12b16e7`. - **Base.** The branch is on `main` at `cbaf04c1f`. `main` has since moved four commits (`3711e0b76`, `61455de27`, `6afccda5a`, `671d4c164`). They touch `packages/spec`, `packages/metadata/package.json`, `pnpm-lock.yaml`, docs and changesets, and no file under `service-analytics` or in this diff, so no merge was taken; the merge queue rebuilds on the merged generation. One of them, `671d4c164`, declares the four drill-through sidecars on `AnalyticsResult` in the spec. This diff leaves the local `AnalyticsResultWithDrill` untouched, as the claim requires. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This branch had an error being 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.
Implements comprehensive validation protocol enhancements: cross-field validation for multi-field business rules, async validation for remote checks, and conditional validation for context-aware rules.
Changes
Cross-Field Validation (17 tests)
end_date > start_date)Async Validation (14 tests)
Conditional Validation (14 tests)
when/then/otherwiseDocumentation
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.