Skip to content

fix(lint): the translation rule's remaining collection rungs read what the artifact's packages[] provide - #19625

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-19349-translation-target-collection-levels
Sep 22, 2026
Merged

os-warren merged 3 commits into
mainfrom
claude/issue-19349-translation-target-collection-levels

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #19349

translation-target-unknown built the universe for six collection rungs from the top-level collection alone, while the same file already read packages[].manifest.… for three others. On os build's per-package leg a package translating what a SIBLING package of the same artifact declares was reported as an orphan at error — a loud, wrong refusal whose remedy deletes a translation the runtime honours.

Clause-②: no

Line numbers, re-derived by SHAPE on my own tree — and the card's divergence resolved

The card records two seats' numbers differing by about 90 lines and deliberately does not guess the cause. Re-derived at merge base 0b4022b41c, by grepping for recordsOf(stack.CONST) and its recordsOf(stack.packages) siblings rather than by coordinate:

reads packages[] ALREADY          only the TOP-LEVEL collection
:733  navigationContributions     :964  objects   (closed by #19064 / PR #19347)
:828  objectExtensions            :1034 views
:886  objects (#19064)            :1040 pages
                                  :1060 actions
                                  :1091 apps
                                  :1146 dashboards
                                  :1177 flows

⭐ The divergence is arithmetic, not a mystery. Every one of the six shifted by exactly +96:

views 938->1034 · pages 944->1040 · actions 964->1060
apps  995->1091 · dashboards 1050->1146 · flows 1081->1177      all delta = +96

PR #19347's net change to this one file is +104 / -8 = +96, and it inserted artifactProvidedObjects and its docblock ABOVE every collection rung. So the triage seat measured on 2277d1fcd1 (before #19347 landed) and the construction seat's numbers were taken with #19347 applied. Neither set was wrong; they named different trees. My tree is post-merge and matches the construction seat exactly.

The reproduction — both layers probed first-hand, ⛔ nothing inherited

A throwaway probe (deleted in the same act; git status --porcelain verified empty, 0 lines) ran every level on the per-package leg packageBodyAsStack(body, entries) builds. Layer A re-reproduced rather than relayed, layer B probed where the card explicitly had not:

layer level probe result
A dashboards 1 error · translations[0]["zh-CN"].dashboards.crm_overview
A flows 1 error · ….flows.lead_conversion
A globalActions 1 error · ….globalActions.export_all
⭐ B views 1 error · ….objects.crm_order._views.board
⭐ B pages 1 error · ….objects.crm_order._tabs.mine
⭐ B apps 1 error · ….apps.crm_app

apps is partly covered, and the card was right to demand that be judged first — but BOTH halves were broken. #18442 closed the contributed-app level, which reads manifest.navigationContributions: contributions, never declarations. A sibling's apps[] declaration was invisible either way, so a second probe was run:

#18442 covers neither half of the DECLARATION question. Both are closed here.

⚠️ views and pages are deliberately bound to crm_order, the object the TRANSLATING package declares, so these readings do not ride on #19064's object fold. If that rung regressed, the cases would fail for the wrong reason and the _views / _tabs readings would be worthless.

The carrier is proven, not assumed

Every one of the six keys carries disposition concat in COMPOSE_KEY_DISPOSITIONS, which is exactly what puts it inside ASSEMBLED_PACKAGE_BODY_DISPOSITIONS and so inside an ADR-0130 D4 entry's assembled body — the same proof objectExtensionsByTarget (#18441) and #19064's fold rest on. objects carries disposition objects, also in that set. ⛔ And there is no stack.manifest.KEY form to read beside it: all six are STACK collections, not manifest keys.

artifactProvidedObjects is generalised to artifactProvidedRecords(stack, collection) — one reader of that carrier for all seven call sites, rather than six copies free to disagree about the entry shape.

Records or names — decided PER LEVEL, with three different reasons

⛔ Not one assumption applied six times. All six fold records, but not for the same reason:

levels why RECORDS, and why names would be wrong
dashboards, flows, apps keyed by their own name, with a SUB-RUNG derived from the record (widget ids and header actionUrls; screen node ids and each screen's config.fields[].name; navigation ids). Names alone would resolve the top key and then judge that sub-rung against an EMPTY set — #19064's trap, moved one rung down. For flows the record also carries otherNodes, which is a DIAGNOSIS ("declares it as a TYPE node, not a screen") a name-only fold would lose.
actions keyed by name, but the stored RECORD is read downstream — checkActionParams judges params.NAME off it. ⚠️ And the OWNER is read from the record, which is what keeps the widening honest: a sibling action bound to an object joins that object's _actions, never globalActions. Registering it globally would make legal a key the resolver never reads.
views, pages there is no name to fold. These have no bundle rung of their own at all — they contribute _views, _sections and _tabs FACTS under the object they bind to, so the record is the only thing carrying both the fact and its binding.

Precedence follows #19064: where the stack in hand and a sibling declare the same name, the declaration this leg is JUDGING keeps the slot (ownDeclaration), for actions, dashboards and flows — the three whose fold carries a value. views and pages contribute only Set adds, so two adds are one add and no precedence rule is needed. apps accumulates nav ids into one Set per name, which is the merge the declaration loop already performs for two same-named entries and what concat composition delivers at runtime.

⚠️ Order is load-bearing for apps: the artifact fold runs BEFORE the contributed-only pass, so a sibling-declared app is a DECLARED app and never acquires the contributedOnly diagnosis. An app owned OUTSIDE the artifact keeps #18442's wording, pinned below.

Both control directions, per level

A false-positive fix without a false-NEGATIVE control is the shape this repo keeps carding, so every acceptance sits beside its control in the same block:

direction pin
the false orphan is gone each level's sibling-declared key, and its sub-rung keys, resolve
⭐ CONTROL — the context does the work for every level, the same bundle judged ALONE still errors
⭐ NON-DEGENERACY a dashboard / flow / action / app / view / tab NO package of the artifact declares still errors, and each remedy enumerates what the artifact provides
⭐ NON-DEGENERACY, one rung down a widget id, header action, screen id, screen field and nav id the sibling does not declare still error under the now-resolved parent
⭐ routing an object-BOUND sibling action keyed under globalActions still errors, with its "is bound to object" redirect
⭐ precedence a sibling's same-named dashboard does NOT make its widget ids addressable under this package's dashboard
#18442 kept intact an app contributed into but owned OUTSIDE the artifact keeps the contributed-only wording
the widening is not a path an entry with no readable body (a segment reference) makes nothing addressable, on every rung
no collateral widening the single-defineStack shape is untouched — all six are stack collections with no stack.manifest form

One fixture correction is worth naming: the record:details case first used a regions spelling the shared page walk does not recognise, so it failed after the fix. That was a wrong FIXTURE, not a wrong fix — corrected to the regions[].components[] shape the file's own section-anchor block uses, and the case then passed in both directions.

Reverse verification, from the COMMITTED fix

scripts/ablation-replace.mjs, one anchor, guarding the fold so that ONLY the six new rungs are neutralised and #19064's object fold survives — which makes the reds provably this PR's:

anchor  "    provided.push(...recordsOf(body[collection]));"  x1 -> x0
blob    d6b2706ecbd3 -> 42ee18972671                      (the mutation LANDED)
Tests   18 failed | 114 passed (132)                      (expected direction: RED)
restore blob == HEAD (d6b2706ecbd3) and `git diff HEAD` is empty

Checks — real exit codes, captured before any pipe

command exit
pnpm --filter '@objectstack/lint^...' build (dependency closure) 0
pnpm --filter @objectstack/lint test — 108 files, 4106 passed / 5 skipped 0
pnpm --filter @objectstack/lint typecheck (incl. check:test-typecheck) 0
pnpm lint — repo-wide eslint . --no-inline-config, ⛔ not narrowed 0
56 of the 59 families scripts/pm/dispatch-gates.mjs --commands derives for this diff 0 each
pnpm check:docs-transcript-drift, re-run after pnpm --filter '@objectstack/lint...' build 0

dispatch-gates --ran reconciles: 59 derived, 56 run, 3 NOT-MEASURED, 0 UNRUN, with every family carrying a recorded exit code so the NOT-MEASURED count is derived rather than claimed. The derivation was re-taken after git fetch origin main and was byte-identical.

NOT MEASURED locally, declared to CI — each exited 3, which these gates define as "nothing was measured", neither a pass nor a finding. All three refuse without a whole-repo pnpm build: check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt.

Acceptance notes

  • content/docs/protocol/kernel/i18n-standard.mdx's key table describes objects.{object} as "an object this stack defines, or a platform object". fix(lint): the translation rule's OBJECT rung reads what the artifact's packages[] provide #19347 noted the same row as already understating the universe and named "the next rung card on this rule" as its natural carrier — that is this card, and this change widens the same table's understatement further, now across six more rungs. ⛔ It is still not filed and not fixed here: the declared file surface for this card is this rule, its tests and .changeset/, and content/docs/ is outside it. Noted for whoever owns that page.
  • The file header documented the packages[] reach for the object ladder only, so the implementation would have reached wider than the declaration — the same mismatch, reversed, that got the object rung carded. One paragraph in the header now names the single carrier reader, the disposition table that decides which collections may be asked of it, and why apps reads both that carrier and navigationContributions.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx


Generated by Claude Code

…inst a sibling package

Test-first, red before the fix: 18 failed | 114 passed.

Every one of the six collection rungs of `translation-target-unknown`
builds its universe from the top-level collection alone, so on `os build`'s
per-package leg a package translating what a SIBLING package of the same
artifact declares is reported as an orphan at `error`.

Each rung is pinned in both directions in one describe block — the false
orphan that must disappear, and the true orphan that must remain — plus the
CONTROL leg (the same bundle judged alone) so "no findings" cannot be
confused with the rung going quiet.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
…t the artifact's packages[] provide

`translation-target-unknown` built the universe for `views`, `pages`,
`actions`, `apps`, `dashboards` and `flows` from the top-level collection
alone, while the same file already read `packages[].manifest.…` for
`navigationContributions`, `objectExtensions` and `objects`. So on `os
build`'s per-package leg a package translating what a SIBLING package of the
same artifact declares was told the target does not exist, at `error`, with a
remedy that deletes a translation the runtime honours.

All six fold RECORDS, for three distinct reasons: `dashboards` / `flows` /
`apps` carry a sub-rung derived from the record; the `actions` record is read
downstream by `checkActionParams` and carries the owner that keeps a bound
action out of `globalActions`; `views` and `pages` have no bundle rung of
their own at all and contribute facts under the object they bind to.

`apps` is the half #18442 did not cover: that change reads contributions, so
a sibling's declaration was invisible whether or not this package contributed
into the app. Folding the records before the contributed-only pass also stops
such an app acquiring a diagnosis that points at a package in this artifact.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
…ngeset

The file header documented the `packages[]` reach for the object ladder only,
which left the implementation reaching wider than the declaration — the same
mismatch, in the opposite direction, that this rule's object rung was carded
for. One paragraph now names the single carrier reader, the disposition table
that decides which collections may be asked of it, and why `apps` reads both
that carrier and `navigationContributions`.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 744a0a3f1d501e18385d7d9b43aa1792db6e23e3packageMentionDocs.

Which tree this was computed on

This run read content/docs from bd681fbe4436c82020cde74744f885df0c9d100a — the merge of head 1ef60553c7f227254a30ad09184d494d6c414254 into base 744a0a3f1d501e18385d7d9b43aa1792db6e23e3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bd681fbe4436c82020cde74744f885df0c9d100a && git checkout bd681fbe4436c82020cde74744f885df0c9d100a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 744a0a3f1d501e18385d7d9b43aa1792db6e23e3 1ef60553c7f227254a30ad09184d494d6c414254 && git checkout -B drift-repro 744a0a3f1d501e18385d7d9b43aa1792db6e23e3 && git merge --no-ff 1ef60553c7f227254a30ad09184d494d6c414254

node scripts/docs-audit/affected-docs.mjs --json 744a0a3f1d501e18385d7d9b43aa1792db6e23e3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Collaborator Author

Test Core (6/6) is red, and it is not this PR's — diagnosed, carded, one re-run spent

Read at 2026-09-22T02:23Z by the domain:spec seat (session session_01UDXER3sdqfeVYpEWZs5mZx) on head 1ef60553c7f227254a30ad09184d494d6c414254, check run 106588685916, workflow run 35678045120.

The failure. One test of 76, in a package this PR does not touch:

FAIL packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:154
  > #5262 … > legacy-boolean-only dev stack keeps working — back-compat via the posture resolver
Error: Test timed out in 5000ms.

It is a member lane rather than an aggregator, so it is a real failure and not a refusal. That is why it was diagnosed instead of re-queued.

Why it is not this PR's. This PR changes one .changeset entry and two files under packages/lint/.

  • packages/plugins/plugin-dev/package.json on origin/main declares 15 dependencies and 4 devDependencies, none of them @objectstack/lint. The seven workspace manifests that do declare it are packages/cli, packages/cloud-connection, packages/mcp, packages/metadata-protocol, packages/platform-objects, examples/app-showcase and packages/lint itself. No byte this PR changes is imported or executed by the failing test.
  • main was green across the same window: scheduled run 35677909278 at 02:01Z on 744a0a3f1d, eight minutes before this failure and on the same shard matrix, concluded success.
  • Test Core shards 1/6 through 5/6 on this same head all concluded success. Only 6/6 is red.

What the cause is. The test file's own header declares this exact hazard and treats it as known: init() dynamically imports about ten real workspace packages "whose vite transforms alone can blow the test timeout under a parallel pnpm test" (#3060). Twelve packages are mocked for that reason, and @objectstack/organizations is deliberately left unmocked because its real resolution failure is the signal the file reads. So an unbounded real module resolution on the hot path is by design, and five of the file's six cases take it. The job's own timing agrees that this is load and not logic: transform 77.73s, import 136.96s, tests 13.33s against a 53.68s wall — a ten-to-one ratio with a 5000ms per-test budget sitting inside it.

The one thing I did not rule out, stated rather than hidden. @objectstack/cli was scheduled on this same shard 6, and packages/cli does declare @objectstack/lint. So a packages/lint diff invalidates the workspace's heaviest test package on exactly the shard that timed out. That is a mechanism by which any change to packages/lint raises this shard's load, and it makes the test fragile against a whole class of unrelated diffs — it does not make the defect this PR's.

What was done.

If the re-run is green this PR proceeds on its normal preconditions. If it reds identically, that is the confirmation that the failure is deterministic at this load and still not this PR's, and #19631 carries it. Either way the outcome is reported here, and no further re-run will be spent.


Generated by Claude Code

os-warren commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 204/204 CONTRACT_REVIEW_TIER
Head-sha: 1ef60553c7f227254a30ad09184d494d6c414254

⚠️ The stamp control was measured by the adopting seat, not carried from the reviewer. The isolated reviewer returned it as a literal, unfilled N/N — no digits, so the stamp reader cannot read it as a control at all. ⛔ A self-reported tier is not a reading in any case. The seat measured it over the reviewer's own transcript: all 204 assistant rows carry the model the constant CONTRACT_REVIEW_TIER names, read from origin/main at 2026-09-22T02:27Z (scripts/pm/dispatch-gates.mjs:12142). Total, non-zero, no fallback row. The cause of the placeholder was this seat's own dispatch brief, which handed the shape with the control unfilled; it is not a defect of the review. ⚠️ Correction, 2026-09-22T02:39Z: this note first said the shape would be carded because it "degrades silently". Measured afterwards against the reader itself, that is FALSE and no card is owed. readServedTier on Served-tier: N/N returns value: "N" — it takes the placeholder's first token as the tier VALUE, not the constant name — and servedStampsHold returns false. Per references/contract-review.md 〈复核归属与资格〉, a Served-tier: that is not the constant name is exit 4 at --pair, i.e. before any carrier is stripped. The tooling is fail-closed on this shape; only the seat's brief needed fixing.

Everything below is the reviewer's own text, adopted by this seat, with one addition marked SEAT under ③.

① Derived judgments

1 · One reader, seven call sites — RE-DERIVED. artifactProvidedRecords is defined once (:919) and called at :1019, :1081, :1114, :1152, :1211, :1282, :1331. The generalisation is body.objectsbody[collection] and nothing else; recordsOf (packages/lint/src/object-graph.ts:215) returns [] for absent/non-record/glob-string values, so the widened reader degrades safely. #19064's object rung is behaviourally identical (run: a sibling-declared object still resolves). objectExtensionsByTarget and contributedNavItemsByApp are untouched. The one thing that looks like a quiet change is not one: the page rung's walk label moved from an indexed pages[i] spelling to '', and walkPageComponents uses pagePath only to build the emitted path label, which that loop never reads — the same call shape page-envelope-audit.ts:323 already uses. One real behaviour change to an existing rung, and it is declared: a sibling-declared app no longer acquires #18442's contributedOnly diagnosis. It is in the header, in the changeset, and pinned by a test that was re-run.

2 · Records, not names, three reasons — RE-DERIVED, and each reason holds for its own level. The mechanical basis is real: packages/spec/src/stack.zod.ts:977+ gives views/pages/actions/apps/dashboards/flows disposition concat and objects disposition objects; ASSEMBLED_PACKAGE_BODY_DISPOSITIONS (:1150) is ['concat','objects','functions']; none of the seven is in ASSEMBLED_PACKAGE_BODY_ENVELOPE_KEYS. The views/pages reason is independently confirmed by enumerating every top-level rung the checker judges — rawData.objects, .globalActions, .apps, .dashboards, .flows and nothing else — so views and pages genuinely have no bundle rung and the record is the only carrier of fact and binding. No level would have been correct with names: the ablation shows each name-only fold would resolve the parent and then judge the sub-rung against an empty set. The apps-unions-nav-ids vs dashboards/flows-first-wins asymmetry is justified rather than sloppy: the apps declaration loop already unioned same-named entries and contributed items before this PR, so the per-package leg now matches the union leg, while dashboards/flows keep #19064's "the declaration being judged keeps the slot". Own-declaration ordering (last-wins) is preserved in every rung.

3 · False-negative controls — PRESENT FOR ALL SIX, and they were run. Every one still reports, at exactly the right path: dashboard name, widget id, flow name, screen id, action name, object-bound sibling action routed to its owner rather than made globally addressable, view name, tab name, app name, nav id. Plus precedence (a sibling's widget id still errors under this package's same-named dashboard), #18442-intact, segment-reference-makes-nothing-addressable, and single-defineStack-untouched. The hint assertions are non-vacuous — they require the artifact-provided names in the remedy, which is what proves the fold reached. This is not half a proof.

4 · #18442 and apps — RE-DERIVED, both halves. #18442 reads manifest.navigationContributions: contributions, never declarations. Both halves reproduced at head. Half 1 (contributing nothing): the app NAME was the orphan. Half 2 (contributing one item): the name resolved through #18442 while the owner's own nav ids were orphaned with the contributed-only wording that advises moving keys to a package in the same artifact. Both now return []. And #18442's own case survives: an app contributed into but owned outside the artifact still errors with contributes into in the message. The fix is right on both halves.

5 · The ablation — ESTABLISHED, by the reviewer's own mutation rather than by the claim's shape. The single anchor was guarded on a private copy so only the six new rungs are neutralised: all six revert to the false orphan, and #19064's object rung still returns []. That is liveness and targeting, measured. The arithmetic corroborates independently: 132 it( at head vs 105 at merge base ⇒ 27 new tests; from the test semantics alone the reviewer predicted 18 reds (dashboards 3, flows 3, actions 4, views/pages 4, apps 4), leaving 9 new greens + 105 pre-existing = 18 failed / 114 passed (132) — the reported figures, predicted rather than accepted. The no-stale-dist/ claim is structural, not a promise: the test imports ./validate-translation-references.js, a relative specifier vitest resolves to the source.

6 · Scope fence — HELD, and the line-drawing was right. 3 files, +628/−22 (GitHub agrees), the 455 test lines additive onto a pre-existing 3045-line file, no probe residue. Not touching content/docs/** or skills/** is now provably right, not merely defensible: PR #19600 is open and modifies both content/docs/protocol/kernel/i18n-standard.mdx (+18/−8) and skills/objectstack-i18n/SKILL.md (+3/−1) — editing either here would have been a serial collision on a governed surface.

7 · Semver and Clause-② — CORRECT. @objectstack/lint patch; artifactProvidedRecords is module-private (not exported, and packages/lint/src/index.ts is untouched); no packages/spec schema moved; the change only narrows a false-positive set. Which side Check Changeset can fail on: check-changeset-no-major.mjs fails on an introduced major, on a Clause-② declared yes with no package graded minor-or-above, on a Clause-② declaration left unreadable, and on a missing changeset. It cannot fail on a no that should have been a yes. So its green is evidence that a changeset exists, is not major, and carries a readable, non-contradictory declaration — it is not evidence that Clause-②: no is the right answer. That was judged independently; it is right.

9 · NOT-MEASURED declarations — SOUND. Exit 3 is PREREQUISITE NOT MET in each named script's own source: check-dual-build-cjs-loads.mjs:203/:481/:1116, check-lean-entry-closure.mjs:169/:615, check-type-check-coverage.mjs:583 (the check:type-check-debt entry point), docs-audit/check-docs-transcript-drift.mjs:144/:194/:476 — each refuses on an unbuilt tree in the words "neither a pass nor a failure", and dispatch-gates.mjs:15904 reads exit 3 as NOT MEASURED. docs-transcript-drift exiting 3 with no dist/ and a real 0 after a build is exactly what that script does. The family count was re-derived independently — 59 families, exit captured before any pipe — so 59 − 3 = 56 reconciles.

10 · Self-test-only families — one genuine gap. All five root scripts are --self-test only. Four are in this diff's derived set. Which half ran for each: issue-citationsboth (lint.yml:4893 runs the alias and then the bare diff-scoped scan; lane Lint & Repo Gates green); closing-target-claimdiff-scoped (closing-target-claim-guard.yml:140, lane green); partof-closing-keyworddiff-scoped (partof-closing-keyword-guard.yml:100, lane green); commit-card-trailers — self-test half ONLY. lint.yml:1126 runs pnpm check:commit-card-trailers and no bare invocation exists anywhere in .github/workflows/, so neither the dev's 0 nor the green lane says anything about this branch's commits. Measured: node scripts/check-commit-card-trailers.mjs --range 0b4022b4..HEAD, exit 0 captured before any pipe — 3 commits, no card relation, and the trailer pair is clean. This is a repo CI property, not a defect of this PR.

11 · The PR body — every checkable sentence true at head. The merge-base coordinate table (:733, :828, :886, :1034, :1040, :1060, :1091, :1146, :1177) re-derived by shape; #19347's net change to this file is +104/−8 = +96 and every "before" coordinate is its "after" minus 96; 2277d1fcd1 exists and predates #19347's merge, so the card's two coordinate sets are explained rather than guessed. The six-row probe table reproduced identically. "108 files" matches 108 test files under packages/lint. "59 derived" matches.

12 · The docs sweep — ADEQUATE, and fully reproducible. Under one consistent convention (case-insensitive line hits over content/docs/** + skills/**) all eight of its counts reproduce exactly: translation bundle 27 (lit control) · orphan 40 · per-package 14 · packages[] 13 · does not define 4 · sibling package 4 · this stack defines 2 · rule id 2. Denominators re-derived: 405 pages, 44 skill files. Radius: content/docs/** + skills/**; the known target deliberately outside it is packages/lint/src/validate-translation-references.ts, which carries the rule id and correctly did not appear in the in-radius listing — so the radius is real and the instrument live (27 and 40 on the broad shapes), which makes the 2s readings and not silence. An independent sweep found the same three carriers and no fourth. Both no-action rulings are right: skills/objectstack-i18n/SKILL.md:196-203 states the reject condition by existence, so the fix moves the implementation toward it (alignment, not drift); content/docs/deployment/validating-metadata.mdx:625-645 states the mechanism correctly and only its instance list is incomplete. #19626 is accurate and goes further than the PR body — it grades row 1 as actively false and pre-dating this PR, enumerates the six now-understated rows separately, and its #19600 serialization warning is factually correct.

② Semver level

patch on @objectstack/lint, Clause-②: no — both correct. No export added or moved, no packages/spec schema touched, no accept set widened on the public surface; a lint rule's false-positive set narrows.

③ Boundary flags

⛔ FAIL BASIS — CI was red and incomplete at this head when the review was taken. Latest run per check NAME, 33 runs, all names distinct, run_attempt: 1:

  • Test Core (6/6) = failure (job 106588685916). A member lane, not an aggregator, so it is correct aggregation and a real red — not a refusal. Cause: packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:154, Test timed out in 5000ms.
  • Test Core (5/6) = in_progress at the time of reading ⇒ NOT MEASURED.
  • 3 skippedBuild Docs, Console Pin Gate, Packed-tarball smoke (opt-in) — all three rostered in EXPECTED_SKIPS on origin/main (scripts/pm/check-expected-skips.mjs:257, :300, :307), so expected skips, not NOT MEASURED. 28 success.

Remedy: no code change. The failing package declares no dependency on @objectstack/lint (neither dependencies nor devDependencies), so it is outside this diff's reachable blast radius, and main was green at both the merge base 0b4022b41c and the PR base 744a0a3f1d. Recorded as a fail basis because a head whose required test gate carries an uncleared member-lane failure and an unreported lane cannot be certified — not because the work is defective.

SEAT — the state since that reading, and one thing the blast-radius argument does not cover. The seat had reached the same diagnosis independently before this review returned, recorded it at comment 5770324358, and spent the one sanctioned re-run of job 106588685916. Since then Test Core (5/6) has concluded success (02:19:38Z), so the NOT-MEASURED half of the fail basis is discharged, and the re-run of 6/6 is in flight as job 106592147592. ⚠️ The seat does not carry the reviewer's "timeout flake" reading unqualified: @objectstack/cli was scheduled on this same shard 6 and packages/cli does declare @objectstack/lint, so a packages/lint diff invalidates the workspace's heaviest test package on exactly the shard that timed out. That makes the test fragile against a whole class of unrelated diffs rather than merely unlucky here, and it is filed as #19631 with the ordered remedy stated — ⛔ a testTimeout raise is the last rung, not the opening move. The fail basis stands until 6/6 reports green.

Correctable before a maintainer reads it, not fail bases:

  • The PR is draft: true.
  • packages/lint/src/validate-translation-references.ts:871 — the claim that no manifest form exists beside this one is loose for objects: ManifestSchema (packages/spec/src/kernel/manifest.zod.ts) does declare objects, as glob patterns. The operative claim — that no manifest form carries these RECORDS — is true, and recordsOf returns [] on a glob array, so nothing is wrong in behaviour. Inherited from the pre-existing twin at :914; a one-clause edit.
  • The apps/dashboards precedence asymmetry is documented and justified, but a reader meeting :1211 before :1282 will feel the tension; one cross-reference would settle it.
  • Repo-level, not this PR's: check:commit-card-trailers has no diff-scoped CI invocation, so that family's green is self-test-only everywhere it is cited. ⚠️ SEAT — checked, and NO card is owed (2026-09-22T02:41Z). The CI observation is literally true and incomplete: this family is enforced pre-push by design, not at PR time, and the script's own header argues why at length ("the PRE-PUSH refusal over what a commit message may not carry" — a card relation, and a model identifier in the harness trailer pair). The wiring is live, not nominal: .githooks/pre-push:39 pipes the refs into scripts/check-commit-card-trailers.mjs --pre-push, root package.json:18 sets "prepare": "node scripts/setup-git-hooks.mjs", and git config core.hooksPath answers .githooks in a real checkout. The header also records the measurement behind that placement: PR-time detection of a pre-push mistake cost two rounds a full extra round each, because the only correct response to the red is to start over rather than force-push. Nothing to file.

Implemented-by: claude/issue-19349-translation-target-collection-levels
Reviewed-by: session_01UDXER3sdqfeVYpEWZs5mZx

VERDICT: FAIL


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Re-run outcome: Test Core (6/6) is green, and the failure did NOT reproduce

Reported as promised at comment 5770324358. Read at 2026-09-22T02:37Z on the unchanged head 1ef60553c7f227254a30ad09184d494d6c414254 — ⛔ no commit was pushed for this.

The re-run. Job 106592147592 (Test Core (6/6), the re-run of 106588685916) concluded success at 02:31:20Z.

The whole head, by job conclusion, latest run per check NAME. 34 check runs, all names distinct, so latest-per-name is the whole set: 31 success, 3 skipped, 0 failure, 0 cancelled. The three skips are the rostered ones — Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in) — all three in EXPECTED_SKIPS on origin/main, so expected rather than NOT MEASURED. The aggregate Test Core lane is success and every one of its six member shards is independently success, so the aggregate is corroborated rather than substituted.

What the outcome means, stated as it was set out in advance. It did not reproduce ⇒ the failure is intermittent, not deterministic at this load. ⛔ That is not a clean bill of health and it does not retire the finding: "flake" is not a root cause, and the fragility is measured from the code and the logs rather than from this one outcome. #19631 carries it, ungraded and unrouted, with the ordered remedy stated — ⛔ a testTimeout raise is the last rung, never the opening move.

⚠️ And the one thing the re-run does not settle, restated so it is not quietly lost: @objectstack/cli was scheduled on this same shard 6, and packages/cli does declare @objectstack/lint. A change to packages/lint therefore invalidates the workspace's heaviest test package on exactly the shard that timed out. "Unlucky here" and "fragile against a whole class of unrelated diffs" are different claims, and the evidence supports the second. That belongs to #19631, ⛔ not to this PR.

No further re-run will be spent on this head, as stated.

The contract-review record at comment 5770384505 reads VERDICT: FAIL on a sole basis that was this CI state. The reviewer has been asked to re-read CI at this same unchanged head and re-render, rather than having the verdict flipped for it — ⛔ the seat does not convert another session's FAIL into a PASS by assertion. Landing preconditions stay unmet until that record lands.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 225/225 CONTRACT_REVIEW_TIER
Head-sha: 1ef60553c7f227254a30ad09184d494d6c414254

Re-render at the SAME head, superseding the FAIL at comment 5770384505. ⛔ No commit was pushed; only the CI state moved. The seat did not convert that FAIL into a PASS by assertion — the reviewing session was resumed and asked to re-read CI itself and re-render, which it did. The stamp control is the seat's own measurement over the reviewer's transcript after the re-render: all 225 assistant rows carry the model the constant CONTRACT_REVIEW_TIER names, read from origin/main at 2026-09-22T02:27Z (scripts/pm/dispatch-gates.mjs:12142). Total, non-zero, no fallback row. This time the reviewer returned the control filled with real digits (204/204, its count at first render); the higher figure here is the same measurement taken over the longer transcript.

Everything below is the reviewer's own text, adopted by this seat.

① Derived judgments

CI at this head — RE-READ, and the fail basis is DISCHARGED. 34 check runs, all names distinct, latest run per check NAME, by job conclusion:

  • Test Core (6/6) = success — check run 106592147592, run_attempt: 2, head_sha confirmed 1ef60553c7f227254a30ad09184d494d6c414254, step 11 "Run this shard's tests" green, and steps 18–19 ("Attest this shard ran and passed", "Publish this shard's attestation") both green, so the shard's credential was issued rather than merely not-failed.
  • Test Core (5/6) = success (completed 02:19:38). The NOT MEASURED half of the basis is gone.
  • Test Core aggregate = success (check run 106593972540) — reported for the first time, and noted only as corroboration; the verdict is taken from the member lanes, ⛔ not from the roll-up.
  • Shards 1/6, 2/6, 3/6, 4/6 all success.
  • 0 failure, 0 cancelled, 0 in_progress. 31 success.
  • 3 skippedBuild Docs, Console Pin Gate, Packed-tarball smoke (opt-in) — all three rostered in EXPECTED_SKIPS on origin/main (scripts/pm/check-expected-skips.mjs:257, :300, :307), so expected skips and not NOT MEASURED, judged against the roster exactly as before.

The flake reading — the reviewer AGREES with the seat's correction, and sharpens it. The mechanism was verified rather than accepted. At head, exactly seven workspace manifests declare @objectstack/lint: packages/cli, packages/cloud-connection, packages/lint, packages/mcp, packages/metadata-protocol, packages/platform-objects, examples/app-showcase — the same seven #19631 names, and packages/cli is among them. @objectstack/cli was scheduled on this same shard 6, first-hand from the original failing job's own completeness reporter ("@objectstack/cli was scheduled but never reached"). And @objectstack/cli is the heaviest test package in the workspace by recorded weight, ahead of @objectstack/spec. The failing file's header carries the declaration at packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:36 (#3060), naming dynamic imports of real workspace packages whose transforms alone can blow the test timeout under a parallel run, with @objectstack/organizations deliberately left unmocked on the hot path.

So the correction is right, and the first record was incomplete in a way worth naming precisely: the finding that the failing package declares no dependency edge to @objectstack/lint stands exactly, and it remains true that no byte of this diff is imported, loaded or executed by the failing test — that is a statement about execution. What it does not cover is scheduling load: a packages/lint diff invalidates the workspace's heaviest test package on the very shard carrying a test documented as vulnerable to concurrent load. "Unlucky" and "fragile against a whole class of diffs" are different claims, and the evidence supports the second.

One refinement the re-run now licenses, by #19631's own stated criterion ("a pass means intermittent, a failure means deterministic at this load"): the re-run passed at the same head with the same diff, so the reading is intermittent, not deterministic. The composite the evidence supports is fragile against a class of diffs that includes this one, and probabilistic rather than deterministic at this load — which is exactly why #19631's refusal to open with a testTimeout raise is the right ordering.

⚠️ One figure in #19631 did not reproduce, stated with the measurement. The card said "partition-test-shards.mjs records @objectstack/cli at 548.6s / 474.4s measured, file-sharded two ways." At head, scripts/test-shard-timings.json records @objectstack/cli at 458.15 (heaviest; @objectstack/spec second at 403.65), and neither 548.6 nor 474.4 occurs anywhere in that file. The card's mechanism is unaffected — cli is the heaviest package, is co-scheduled on shard 6, and does depend on @objectstack/lint, all verified — but that parenthetical should be corrected so a taker measuring against it is not chasing a number the tree does not carry. SEAT: confirmed first-hand and corrected on the card. The two figures are real but were cited off the wrong instrument — they live only in the HEADER of scripts/partition-test-shards.mjs:29, inside the historical table arguing why test-file COUNT was the wrong weight.

Everything else from the first record stands, re-derived and unchanged. One reader, seven call sites (:919 defined; :1019, :1081, :1114, :1152, :1211, :1282, :1331 called), with #19064's object rung behaviourally identical and the page-walk label change inert. Records-not-names correct at every level for three genuinely distinct reasons, with views/pages confirmed to have no bundle rung of their own by enumerating the checker's only five top-level reads. False-negative controls present and passing for all six rungs, run first-hand. Both halves of the apps question closed, with #18442's own case preserved. The ablation established by the reviewer's own guarded mutation — six rungs revert, the object rung survives — and independently corroborated by predicting 18 failed / 114 passed (132) from 132-vs-105 test counts before reading it. Scope fence held at 3 files, and not touching content/docs/** or skills/** provably right given open PR #19600. The docs sweep reproduced on all eight counts with a live radius control.

② Semver level

patch on @objectstack/lint, Clause-②: no — both correct. No export added or moved (artifactProvidedRecords is module-private and packages/lint/src/index.ts is untouched), no packages/spec schema moved, no accept set widened; a lint rule's false-positive set narrows. Check Changeset can fail on an introduced major, on a Clause-② declared yes with no package graded minor-or-above, on an unreadable declaration, and on a missing changeset — it cannot fail on a no that should have been a yes, so its green is not evidence that no is right. Judged independently; it is right.

③ Boundary flags

No fail basis remains. The sole basis in the first record was the CI state, and it is discharged at the same head by measurement, not by argument.

Correctable, none blocking:

  • The PR is draft: true. It will need un-drafting to merge; nothing else gates it.
  • packages/lint/src/validate-translation-references.ts:871 — "all of them are STACK collections, not manifest keys" is loose for objects, which ManifestSchema does declare, as glob patterns. The operative claim (no manifest form carries these RECORDS) is true and recordsOf returns [] on a glob array, so nothing is wrong in behaviour. Inherited from the pre-existing twin at :914; a one-clause edit.
  • The apps-unions vs dashboards/flows-first-wins precedence asymmetry is justified and documented, but one cross-reference between :1211 and :1282 would spare the next reader the reconstruction.
  • The commit-card-trailers bullet is WITHDRAWN — verified answered by design, and no card is owed. The family is enforced pre-push, not at PR time: .githooks/pre-push pipes the refs into scripts/check-commit-card-trailers.mjs --pre-push and exits non-zero on refusal; root package.json sets "prepare": "node scripts/setup-git-hooks.mjs"; and git config core.hooksPath answers .githooks in a real checkout. CI's self-test-only invocation at lint.yml:1126 is therefore the designed division of labour, ⛔ not a gap. What survives is only the citation discipline, unchanged: a local pnpm check:commit-card-trailers exit 0 is the self-test half and must not be cited as the diff-scoped verdict. For this branch the real half is measured — node scripts/check-commit-card-trailers.mjs --range 0b4022b4..HEAD, exit 0 captured before any pipe: 3 commits, no card relation, and the trailer pair is clean.
  • For the other three self-test-only families in this diff's derived set, the diff-scoped half demonstrably ran: issue-citations both halves at lint.yml:4893 (lane Lint & Repo Gates green), closing-target-claim at closing-target-claim-guard.yml:140 (lane green), partof-closing-keyword at partof-closing-keyword-guard.yml:100 (lane green).

Implemented-by: claude/issue-19349-translation-target-collection-levels
Reviewed-by: session_01UDXER3sdqfeVYpEWZs5mZx

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants