Skip to content

fix(lint): refuse a present non-array packages at all five stack.packages readers - #20229

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20206-lint-packages-non-array-refused
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20206-lint-packages-non-array-refused

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20206

Clause-②: yes (narrowing)

Both facts are true and both are read. yes: ERROR_CODE_LEDGER['@objectstack/lint'] (@objectstack/spec, published) is a new per-package face, present where it was absent before (rework round 1, fixing a red check:error-code-provenance). narrowing: packages/lint is published, and os lint now refuses a stack.packages shape it used to accept silently — the spec packages array declaration itself does not move; only this reader now honours it (os validate and os build already refuse a malformed packages earlier, at ObjectStackDefinitionSchema.safeParse, before ever reaching packages/lint's rules — this PR does not change that door). Two changesets carry the two facts separately: .changeset/20206-lint-packages-non-array-refused.md (@objectstack/lint: minor, Clause-②: no (narrowing), the ADR-0087 disposition) and .changeset/20206-lint-error-code-provenance-row.md (@objectstack/spec: minor, Clause-②: yes).

What changed

Ruling A on #15293 (comment 5634034754): a present packages that is not an array ({}, 0, 'x', a keyed object) is malformed, not absent, and every reader must refuse it. Four packages/lint readers fell through recordsOf(stack.packages) to [] instead:

  • validate-object-references.ts:165 (artifactProvidedObjectNames)
  • validate-translation-references.ts:753, 848, 921 (contributedNavItemsByApp, objectExtensionsByTarget, artifactProvidedRecords)

They now share one small reader, packagesOf(stack) (object-graph.ts), which:

  • returns [] for an absent packages (undefined ONLY — see the null leg below);
  • reads a well-formed array exactly as recordsOf did (junk entries dropped, unchanged);
  • throws INVALID_ARTIFACT_PACKAGES (ADR-0112, status: 422) for anything else present.

recordsOf itself is untouched: it stays the shared map-or-array reader objects/sections/tabs need, where a keyed map is legitimate. packages never has a map form, so this is a second, narrower reader rather than a branch on the first one.

A fifth site, found on re-reading origin/main

The card's site census was taken at origin/main 1c8b320. This worktree forked from a later origin/main that already carries #20208 (merged), which added a fifth copy of the identical recordsOf(stack.packages) pattern: validate-mapping-target-fields.ts:95 (extensionFieldsByTarget). Fixed here under the in-place-fix exemption — same defect class as this card, a mechanical fix with the form already pinned by the other four, the file held by no other claim (#20208 is merged), same gate family, no new verification surface. This report amends the claim's declared file surface to include validate-mapping-target-fields.ts and its test.

Rework round 1 — the null leg (ruling A on #19926, 5805260775)

null is malformed, everywhere. This card originally put packages: null out of scope with a pointer to #19926, but #19926's own claim fenced packages/lint out as its surface — the lint leg had no owner. Per the seat's review comment on #20206 (5855890525), that leg moves here as the execution of an existing ruling, not a new decision:

  • packagesOf now treats only undefined as absent; null falls to the same INVALID_ARTIFACT_PACKAGES refusal as {} / 0 / 'x' / a keyed object.
  • The refusal message names null as itself (`packages` of type null) rather than typeof null's 'object', which would name a {} the author never wrote — the naming packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228) gives resolveArtifactPackageOrder once it lands, adopted early here.
  • Every null pin flipped from a silence control to a refusal assertion: packagesOf directly (object-graph.test.ts), and each of the three public functions its five readers sit behind (validateObjectReferences, validateTranslationReferences, validateMappingTargetFields). undefined (absent) and a well-formed array stay green controls.
  • Ablated: || declared === null restored into the absent branch via scripts/ablation-replace.mjs — all four null pins (one per test file) went red (expected function to throw an error, but it didn't), 228/232 still green, mutation and restore both verified on disk (blob hash back to HEAD, git diff HEAD empty). See the report comment for the full readings.

Rework round 1 — CI fix (error-code provenance)

CI was red on the prior head (bd29ec386f), job Lint & Repo Gates, step 120 "Error-code provenance guard" (pnpm --filter @objectstack/spec check:error-code-provenance) — reproduced locally first, quoting the gate's own message:

FAIL — 1 stamp site(s) of a registered code with no provenance row:
  @objectstack/lint stamps 'INVALID_ARTIFACT_PACKAGES' (assign) at packages/lint/src/object-graph.ts:278 — not listed under its own owner key

packagesOf's err.code = 'INVALID_ARTIFACT_PACKAGES' is a genuine, independent stamp of an already-registered code (deliberately reused from @objectstack/core's resolveArtifactPackageOrder, never minted new) — not a case where "a door in another package names the wire vocabulary" (the gate's waiver shape), since packages/lint's rules are pure (stack) => Finding[] functions with no door of their own. Fixed the way the gate's own message prescribes: a new '@objectstack/lint' row in packages/spec/src/api/error-code-ledger.zod.ts listing INVALID_ARTIFACT_PACKAGES, with a comment recording the wire path (door: 'none', the #16449 reading already used for @objectstack/spec's own STACK_* rows) — no allowlist, no waiver, no new code. Precedent: 3f9e2eaa1c, "list plugin-security's class-field error codes under its own ledger key," which used the identical remedy and the identical @objectstack/spec: minor / Clause-②: yes changeset shape for a new owner-key row.

Rework round 2 — pin gap and wording (at-tier record 5856823202, items 1–2)

  • Pin gap (item 1): validate-object-references.test.ts's non-array refusal test pinned only [null, 42, 'core'], while the other two validators already pinned {}. Added {} and a keyed object ({ a: { manifest: {} } }, the one shape recordsOf read as a map). (Round 3 found validate-mapping-target-fields.test.ts still lagged both on the same front — see below; only once that landed did all three validators reach parity.)
  • Count, corrected (item 2a): "four call sites" / "four copies" in the packagesOf docblock and the object-graph.test.ts describe-block comment now read "five … three files", matching the fifth site (validate-mapping-target-fields.ts, above) this PR already fixes.
  • Core parity, qualified (item 2b): on main, resolveArtifactPackageOrder (packages/core/src/artifact-packages.ts:208) still reads null as absent and names a refusal with typeof; PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 (packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926) changes both, and has not landed. Every sentence claiming lint reads null "the way resolveArtifactPackageOrder does" or raises "the SAME code … for the identical defect" was only ever true for {} / 0 / 'x' / a keyed object today — for null it is qualified with "once packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228) lands" instead, in the packagesOf docblock, its inline naming comment, this changeset and the ledger-row comment. Wording only; no code changed.
  • Door reachability, corrected (item 2c): re-read validate.ts:293, compile.ts:356, lint.ts:673/1140, and format.ts:369 (printError) directly. os validate and os build both run ObjectStackDefinitionSchema.safeParse before the lint readers ever run, and a malformed packages refuses there first — the lint reader is unreachable from those two doors for this defect. Only os lint reaches it: exit 1, the message on stdout via printError (not stderr), code present under --json. The lint changeset and this PR body (above) are corrected to say exactly that, not "os validate / os lint / os build … on stderr".
  • Re-ablated (same anchor as round 1, now against commit 68398a0e7e, which carries every round-2 edit): || declared === null restored into packagesOf's absent branch — all four null pins (one per test file) went red, 228/232 still passed, mutation and restore both verified on disk (blob hash back to HEAD, git diff HEAD empty). The two new pins from item 1 ({} and a keyed object in validate-object-references.test.ts) stayed GREEN through this run — they assert a different branch (the non-null refusal path, untouched by this anchor), confirming the mutation is null-specific. Full readings in the report comment.

Rework round 3 — the mapping validator's own pin gap (in-seat ruling 5857515836, at-tier record 5857513507, item 1)

  • Pin gap: validate-mapping-target-fields.test.ts's non-array refusal loop pinned only [{}, 0, 'x', null] — no keyed object, and no explicit packages: undefined control (only the array control at the objectExtensions test above it). Added { a: { manifest: {} } } to the loop, and a dedicated packages: undefined control test beside it — targeting full_name (a field contact declares directly), not sla_tier (which resolves via the STACK's own top-level objectExtensions, not a package's — region, in that same fixture, is the package-supplied one — and this control's minimal fixture declares no objectExtensions at all, so full_name, a field contact declares directly, is the one target that resolves regardless). All three validators now pin the same shape classes: {}, a number, a string, null and a keyed object, plus an array control and an explicit undefined control.
  • Wording, corrected: the round-2 sentence above and validate-object-references.test.ts's matching comment both said "the identical set"/"the same set" before this fix landed, which was false — validate-mapping-target-fields.test.ts was still short two cases. Reworded to "the same shape classes" in both places; true now that this round closes the gap.
  • Ablated: pointed scripts/ablation-replace.mjs at packagesOf's array-vs-everything-else branch (if (Array.isArray(declared)) return declared.filter(isRec);), replacing it with a version that also accepts any isRec value the old recordsOf-style way. There is no narrower branch to anchor on than this — {} and a keyed object share the exact same guard in the implementation, so an ablation of one is necessarily an ablation of both. 5 tests went red: object-graph.test.ts's {} and keyed it.each cases explicitly, plus all three validators' refusal loops (each stops at its first affected element — {} is first in the mapping and translation loops, so the new keyed assertion at the end of the mapping loop is covered by that same failing test rather than isolated on its own). 228/233 still passed; 0 / 'x' / null stayed refused throughout, untouched by this anchor. Mutation and restore both verified on disk (blob hash back to HEAD, git diff HEAD empty, git status clean). Full readings in the report comment.

Landing order: PR #20228 landed as a9fb83ef06; merged into this branch at 82dc0c9c01 (round 4 below, merge commit 3fef33e23b plus one wording-fix commit) — null-packages-follows-resolver.test.ts leg 1 is green now.

Pins

packagesOf is pinned exhaustively in object-graph.test.ts: an array is the control (junk-dropping behaviour unchanged), an absent (undefined) packages stays silent, and {} / 0 / 'x' / a keyed object / null are each refused with code: 'INVALID_ARTIFACT_PACKAGES', status: 422 (the null case additionally pins the message names null, not object). Each of the three public functions these readers sit behind (validateObjectReferences, validateTranslationReferences, validateMappingTargetFields) gets its own throw-pin proving the wiring reaches the shared reader, since all three now call the identical function.

One existing pin asserted the OLD fall-through semantics and is flipped: validate-object-references.test.ts's 'ignores a packages value that is not a list of entries' (null, 42, 'core' all silently ignored) is now two tests — undefined stays the silence control, and null / 42 / 'core' / {} / a keyed object (the last two added in round 2) now assert the refusal (code + status), matching the same shape classes the other two validators pin.

Census (H2)

Grepped the whole tree for a non-array packages fixture reaching any of the five readers: none besides the one flipped test above. packages/spec/src/stack-artifact-packages.test.ts tests the spec schema's own refusal at a different layer and is untouched.

Gates

Local, targeted (container under heavy multi-agent contention — most runs this round queued 5-20+ minutes on the shared os-verify-lock, one holder held it ~1150s straight; retried with a stable slot rather than enumerating the whole farm, per contract):

  • pnpm --filter @objectstack/spec build && check:generated — green, all 15 generated artifacts up to date (measured post-merge, against a tree that also absorbed 137 files' worth of unrelated origin/main movement — see "Post-merge" below).
  • pnpm --filter '@objectstack/lint^...' build (dependency closure incl. @objectstack/spec DTS + @objectstack/formula) — green.
  • pnpm --filter @objectstack/lint typecheck (tsc --noEmit + check:test-typecheck) — green, no new debt.
  • pnpm --filter @objectstack/spec exec vitest run src/api/error-code-ledger.test.ts — 21/21 passed.
  • pnpm --filter @objectstack/lint exec vitest run the five test files — 232/232 passed, both before and after the merge.
  • check:error-code-provenance, check:error-code-casing, check:dispatcher-error-vocabulary, check:strictness-ledger — all green (the four families dispatch-gates.mjs newly derives once the diff touches packages/spec/src/api/error-code-ledger.zod.ts).
  • check:adr-0087-registration / check:changeset-no-major — green with the updated yes (narrowing) declaration (verified with a synthetic pull_request event carrying this PR's own line).
  • check:nul-bytes, check:issue-citations, check:cross-package-test-inputs, check:test-source-alias, check:doc-authoring, check:type-check-coverage, check:published-files, check:watch-hint-literal — all green (unchanged from round 0).

Post-merge: origin/main moved on packages/spec/src/api/error-code-ledger.zod.ts (137 files total, mostly unrelated) between round-0 and this round; merged (a4b05d6e15, no rebase, no force-push) — clean, no conflicts, our new '@objectstack/lint' row and both stamp sites survived intact. Full rebuild + check:generated + typecheck + the six test files above all re-run and green against the merged tree.

dispatch-gates.mjs --ran reconciliation this round: 13 of 86 now-derived families measured locally (the ones above, check:error-code-provenance included — this is the family whose local absence let the CI failure through last round); the rest are left to CI, mostly repo-wide --self-test checker-health invocations and generated-artifact sub-checks already covered wholesale by the green check:generated run above.

Round 2 (head 68398a0e7e, wording-only + the item-1 pin addition — packagesOf semantics unchanged): re-derived dispatch-gates.mjs --commands after a fresh git fetch origin main — identical 86-family list to round 1 (diff empty), so nothing new to run and nothing skipped. origin/main re-checked three times this round (before the commit, before the ablation, and again here): still has not touched any file this PR touches (only validate-rls-predicate-enforceability.* and unrelated changesets) — no merge needed this round. Container restarted mid-round (~15:05Z) and killed the in-flight background verification; the worktree and its uncommitted diff survived, the diff was re-verified complete and committed (as 7be6204521, tree identical to this head) before any ablation or long run, then re-run from scratch, all in the foreground under the shared lock with the same stable slot (issue-20206-dev-r2; two queue-timeout (exit 99) attempts before it landed — recorded as NOT MEASURED, not as failures, per contract). check:commit-card-trailers then refused the first push over a model name in the co-author trailer (this session's own harness-attribution reminder, which the repo's model-free-trailer contract overrides); the tip commit was unpublished, so amended in place to the model-free pair — git commit --amend, no force-push, tree byte-identical — landing as 68398a0e7e:

  • pnpm --filter '@objectstack/lint^...' build — green.
  • pnpm --filter @objectstack/lint typecheck — green, same pre-existing debt as round 1 (2 files / 6 errors / 2 pinned signatures, unrelated, shrink-only), no new debt.
  • pnpm --filter @objectstack/lint exec vitest run the four packagesOf-reaching test files — 232/232 passed (object-graph.test.ts, validate-object-references.test.ts, validate-translation-references.test.ts, validate-mapping-target-fields.test.ts).
  • pnpm --filter @objectstack/spec exec vitest run src/api/error-code-ledger.test.ts — 21/21 passed.
  • check:error-code-provenance — green: self-test OK, then scanned 2477 files; 328 registered-code stamp site(s): 312 listed, 16 waived … every registered-code stamp site is listed under its own owner key or carries a recorded waiver (9 waiver(s), all live).
  • check-adr-0087-registration.mjs --base origin/main and check-changeset-no-major.mjs --base origin/main --event (a synthetic pull_request payload carrying this PR's real body, byte for byte) — both green, re-run post-commit; readClause2Line on the live body reads {"kind":"declared","value":"yes","arm":"narrowing"} — a clean declaration, not the near-miss the seat flipped to its own paragraph round 1 (still on its own line here).

Round 3 (head 42b3bfbf2e, one bounded patch — packagesOf's function body and the ledger's row entry unchanged since a4b05d6e15; their surrounding comments moved in round 2, 68398a0e7e): PR #20246 (443b2f4fdc) entered the merge queue at 15:58:56Z and reached main at 16:17:46Z: after round 3's check and commit (16:09:36Z, amended 16:17:08Z) and before its push (about 16:20Z). Round 4's merge picked it up cleanly. Under the shared lock (stable slot issue-20206-dev-r3, lock free both times, no queueing this round):

  • pnpm --filter '@objectstack/lint^...' build — green.
  • pnpm --filter @objectstack/lint typecheck — green, same pre-existing debt, no new debt.
  • pnpm --filter @objectstack/lint exec vitest run the four packagesOf-reaching test files — first pass caught a bug in the new control test itself (its mapping target sla_tier resolves via the STACK's own objectExtensions, not a package's — the control's minimal fixture declares no objectExtensions at all, so packages: undefined correctly produced a real finding rather than staying silent — fixed by retargeting the control at full_name, a field contact declares directly, amended into the same unpushed commit); re-run 233/233 passed.
  • Ablation: see "Rework round 3" above — mutation landed, 5 tests red ({} and keyed pins across all four files, 0/'x'/null unaffected), 228/233 passed, restore verified byte-identical to HEAD.
  • check:commit-card-trailers — green (model-free trailers carried through the amend).
  • Landing-order addendum: PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 has not merged as of this push (checked via the REST API right before pushing); pushed as planned, per the addendum's instruction for that case.

Round 4 — merge (PR #20228 landed as a9fb83ef06)

PR #20228 merged at 16:40Z. git fetch origin main && git merge origin/main (3fef33e23b, no rebase, no force-push) — clean, no conflicts. Diff stat, old head (42b3bfbf2e) → the merge commit: 287 files changed, 8926 insertions(+), 1939 deletions(-), split:

  • From main: all 287 files — verified by set-equality against git diff --name-only a9fb83ef06 origin/main computed from the pre-merge merge-base (ab820016b): identical file lists both directions (comm -23/comm -13 both empty). Nothing else moved.
  • Anything else: empty, by construction — the merge introduced no manual conflict resolution (git status was clean immediately after git merge, no file was hand-edited as part of it).

One shared file, validate-mapping-target-fields.ts + its test, was touched by both sides: PR #20246 (443b2f4fdc, "an import mapping target may name a declared part of a compound field") reached main at 16:17:46Z, between round 3's commit and its push. Git merged it automatically with no conflict — the new address-part-mapping tests PR #20246 adds sit above our round-3 additions in the test file, which are untouched by the diff (confirmed directly: git diff 42b3bfbf2e HEAD -- packages/lint/src/validate-mapping-target-fields.test.ts shows only PR #20246's own hunks).

A separate at-tier record on the round-3 head (42b3bfbf2e, before this merge) found the round-3 comment mis-attributing which objectExtensions source resolves sla_tier — fixed in 82dc0c9c01, its own commit, folded into this same push: the fixture's STACK-level objectExtensions supplies sla_tier; region is the one that needs a package. The full_name retarget was already correct. That same correction is threaded through this PR body's round-3 bullets above.

Proof, under the shared lock (stable slot issue-20206-dev-r4; severe contention — the @objectstack/cli^... dependency closure needed six attempts: four queue-timeout (exit 99) (NOT MEASURED, place kept each time), one killed by this session's own 590s foreground wrapper at 54/55 tasks cached from the partial run before it, then a clean finish):

  • pnpm exec turbo run build --filter='@objectstack/cli^...' --concurrency=2 — green, 55/55 tasks.
  • pnpm --filter @objectstack/cli exec vitest run test/null-packages-follows-resolver.test.ts — 16/16 passed, both legs. Leg 1 (`#19925 leg 1: each reader answers `packages: null` the way the real resolver does`) includes the named case `os lint` lintConfig (READERS[3], :107) — GREEN, now that resolveArtifactPackageOrder genuinely refuses null post-fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228, matching lintConfig's own refusal. Leg 2 (the resolver-double leg) is unaffected either way and stayed green throughout every round.
  • pnpm --filter @objectstack/lint typecheck — green, no new debt.
  • pnpm --filter @objectstack/lint exec vitest run the four packagesOf-reaching test files — 237/237 passed (up from 233: PR feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 added 4 tests to validate-mapping-target-fields.test.ts; none of the new tests touch packages).
  • check-adr-0087-registration.mjs --base origin/main — green, re-run post-merge.
  • check:commit-card-trailers — green on both commits (the merge commit and the wording-fix commit).

origin/main moved once more after this merge (17bd318771, unrelated InlineAction/ViewMetadataParsed spec types) — checked, touches none of this PR's files; not re-merged, since nothing to pick up.

Round 5 (head a38a259df6, wording only): with PR #20228 in the head, every null-parity sentence now states core's refusal in the present tense: the packagesOf docblock, its @throws, the inline naming comment, the lint changeset and the ledger-row comment. The round-1/2 sections above that say "once … lands" are history. At a38a259df6 none of the PR's files carries a conditional claim about #20228 (git grep sweep: 0 hits). The seat corrected this body's #20246 timing (the queue build at 15:58:56Z versus the landing on main at 16:17:46Z).

Acceptance notes

None. This PR's scope is exactly the five recordsOf(stack.packages) readers described above, their null leg (ruling A on #19926), and the CI-fix ledger row the first two require — the ledger edit is outside the claim's originally declared file surface (packages/lint/** + .changeset/) but is the coordinator's explicit rework instruction, reproduced and fixed the way the gate itself prescribes.


Generated by Claude Code

…rdsOf(stack.packages) readers

Ruling A on #15293 (comment 5634034754): a `packages` that is present but not
an array (`{}`, `0`, `'x'`, a keyed object) is malformed, not absent, and
every reader refuses it. Four `packages/lint` readers fell through
`recordsOf(stack.packages)` to `[]` instead of refusing:
`validate-object-references.ts`'s `artifactProvidedObjectNames`, and
`validate-translation-references.ts`'s `contributedNavItemsByApp`,
`objectExtensionsByTarget` and `artifactProvidedRecords`. A fifth copy of the
same pattern (`validate-mapping-target-fields.ts`'s `extensionFieldsByTarget`)
landed via #20208 after this ruling's site census and is fixed the same way.

All five now share one small reader, `packagesOf(stack)`
(`object-graph.ts`): absent stays `[]`, an array is read exactly as
`recordsOf` read it (junk entries dropped, unchanged), and anything else
present throws `INVALID_ARTIFACT_PACKAGES` (ADR-0112, status 422) -- the same
registered code `@objectstack/core`'s `resolveArtifactPackageOrder` already
raises for the identical defect. `recordsOf` itself is untouched: it stays
the shared map-or-array reader `objects`/`sections`/`tabs` need, where a
keyed map is legitimate -- `packages` never has a map form, so this is a
second, narrower reader rather than a branch on the first one.

Pins: `packagesOf` is pinned exhaustively (array control, absent/null
silence, refusal on `{}`/`0`/`'x'`/a keyed object, with code + status). Each
of the three public functions these readers sit behind
(`validateObjectReferences`, `validateTranslationReferences`,
`validateMappingTargetFields`) gets one throw-pin proving the wiring reaches
the shared reader. One existing pin asserted the old fall-through semantics
(`validate-object-references.test.ts`, "ignores a `packages` value that is
not a list of entries") and is flipped: `null` stays a silence control,
`42`/`'core'` now assert the refusal.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 9 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/plugins/packages.mdx (via INVALID_ARTIFACT_PACKAGES (literal, a string literal in ERROR_CODE_LEDGER; a string literal in a comment on a changed line; a string literal in packagesOf))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 136 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 17bd31877109b7cc692e7e54c4fe39f82a5c32d5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cd8fcf47b9bf2b9fae0233211a7b2e91d8e00e76 — the merge of head a38a259df60e106370f2ea9e9060e12bab8f59b6 into base 17bd31877109b7cc692e7e54c4fe39f82a5c32d5, 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 cd8fcf47b9bf2b9fae0233211a7b2e91d8e00e76 && git checkout cd8fcf47b9bf2b9fae0233211a7b2e91d8e00e76
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 17bd31877109b7cc692e7e54c4fe39f82a5c32d5 a38a259df60e106370f2ea9e9060e12bab8f59b6 && git checkout -B drift-repro 17bd31877109b7cc692e7e54c4fe39f82a5c32d5 && git merge --no-ff a38a259df60e106370f2ea9e9060e12bab8f59b6

node scripts/docs-audit/affected-docs.mjs --json 17bd31877109b7cc692e7e54c4fe39f82a5c32d5

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 17bd31877109b7cc692e7e54c4fe39f82a5c32d5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…stamp under @objectstack/lint's ledger key

Rework round 1. Ruling A on #19926 (5805260775): `null` is malformed,
everywhere. `packages/lint`'s site census (#20206, ruling A on #15293) put
`null` out of scope with a pointer to #19926, but #19926's own claim fenced
`packages/lint` out as this card's surface -- the lint leg had no owner.

`packagesOf` (object-graph.ts) now treats only `undefined` as absent;
`null` falls to the same INVALID_ARTIFACT_PACKAGES refusal as `{}`/`0`/`'x'`/
a keyed object. The message names `null` as itself rather than `typeof`'s
`'object'`, matching the type label PR #20228 uses in
packages/core/src/artifact-packages.ts. Every `null` pin flips from a
silence control to a refusal assertion, at `packagesOf` directly and at
each of the three public functions its four (now five, with
validate-mapping-target-fields.ts) call sites sit behind. `undefined`
(absent) and a well-formed array stay green controls.

CI fix: `check:error-code-provenance` (job "Lint & Repo Gates", step 120)
was red on the prior head -- `packages/lint` stamps the registered code
`INVALID_ARTIFACT_PACKAGES` (object-graph.ts's `err.code = ...`) without
being listed under its own owner key in `ERROR_CODE_LEDGER`. Fixed the way
the gate prescribes: a new `'@objectstack/lint'` row in
packages/spec/src/api/error-code-ledger.zod.ts, with a comment recording
the wire path (door: 'none' -- packages/lint's rules are pure
`(stack) => Finding[]` functions called from `os validate`/`os lint`/
`os build`, never through an HTTP boundary). No code minted or duplicated;
the existing registered code is reused, provenance is merely now recorded
under a second owner (precedent: 3f9e2ea, "list plugin-security's
class-field error codes under its own ledger key").

Two changesets: `@objectstack/lint: minor` (Clause-②: no (narrowing) --
unchanged from round 1) for the behavior change, and a new
`@objectstack/spec: minor` (Clause-②: yes) for the ledger row -- a new
per-package face on a published payload, modelled on the 3f9e2ea
precedent's own spec-only changeset. The PR-level declaration becomes
`Clause-②: yes (narrowing)`, carrying both facts.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 86/86 CONTRACT_REVIEW_TIER
Head-sha: a4b05d6e1576e3bc3cecf95851d724f4ebb4e448

① Derived judgments

  1. BLOCKING — card item 3 pin gap. validate-object-references.test.ts:313 pins only [null, 42, 'core']; {} (and the keyed object, the one shape recordsOf read as a map) is unpinned at this validator, while the other two validators pin {}. Card: "Pin: packages: {}, 0 and 'x' are refused at each validator." Fix: add {} and { a: { manifest: {} } } to that list.
  2. BLOCKING — text untrue at head. (a) object-graph.ts docblock "every one of these four call sites" / "replacing four copies … across validate-object-references.ts and validate-translation-references.ts", and object-graph.test.ts:299-302 "the four … call sites … four private copies": five sites in three files (validate-mapping-target-fields.ts:95 included). Fix: "five … three files". (b) Docblock "read the way resolveArtifactPackageOrder reads it", the inline "null is named as itself (matching resolveArtifactPackageOrder)", the lint changeset "the same way … resolveArtifactPackageOrder already does", and the ledger comment "the SAME registered code … raises for the identical defect": at this head packages/core/src/artifact-packages.ts:215 reads if (declared === undefined || declared === null) return [artifact]; and its message uses typeof declared; true only once PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 lands. Fix: qualify with "once packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228) lands". (c) Lint changeset "os validate / os lint / os build surface it as a refusal on stderr … instead of reporting the stack as clean": validate.ts:293-296 and compile.ts:356-361 run ObjectStackDefinitionSchema.safeParse and exit before runAuthoringRules, so the lint reader is unreachable at those two doors (the card says they "refuse it first"); only os lint (lint.ts:673, no schema parse) reaches it, and its text face is printError = console.log (stdout, format.ts:369-371), exit 1. Fix: "os lint: exit 1, message on stdout, code under --json; os validate / os build already refuse at schema parse".
  3. packagesOf (object-graph.ts:262-278): undefined → []; array → declared.filter(isRec), identical to recordsOf's array branch (:215); null/{}/0/'x'/keyed → Error with code: 'INVALID_ARTIFACT_PACKAGES', status: 422. recordsOf (:215-221) unchanged. git grep -n -E 'recordsOf\([^)]*\.packages' a4b05d6e -- packages/lint/src → two hits, both test comments (object-graph.test.ts:302, validate-mapping-target-fields.test.ts:108); all five readers call packagesOf (validate-object-references.ts:165, validate-translation-references.ts:753/848/921, validate-mapping-target-fields.ts:95).
  4. Doors: acceptable shape, not a stack-trace crash. runAuthoringRules (authoring-rules.ts:1930) has no per-rule try/catch; the throw reaches lint.ts:1140-1160's catch-all: --json → { error, ...errorCodeFields(error) } (code passes; status is dropped, format.ts:282-284 reads httpStatus), exit 1. Card allows "the existing code"; neither card nor rulings require a Finding. Ruling packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 A "null becomes a refusal at every reader, in the non-array envelope" is met.
  5. Ledger row: the gate's own remedy (check-error-code-provenance.ts:351: a row under the owner key "with a comment recording the wire path", OR a waiver when another package's door names the wire). Consistent: 30th owner key appended (keys are in historical, unsorted order); the owner keys are package names regex passes; @objectstack/lint is published (publishConfig.access: public, no private). Mints nothing: the code already sits under @objectstack/core (:878); union unchanged. door: 'none' mirrors the spec: register every error code that ships in dist — the nine unregistered defineStack / ownership / namespace codes enter ERROR_CODE_LEDGER / StandardErrorCode (the ledger is the published face, per the #16404 ruling) #16449 spec rows (:1306-1307). "never through an HTTP boundary" holds (no importer of @objectstack/lint under packages/**/src outside cli/lint); the "SAME code core raises" sentence is item 2(b) for null.
  6. Message names null as null (declared === null ? 'null' : typeof declared), pinned by not.toThrow(/of type object/). Spec changeset sentences are true. Pins: packagesOf has {}/keyed/0/'x'/null with array and undefined controls; translation has all five plus the undefined control; mapping has {}/0/'x'/null; object-references lacks {} (item 1). No refusal pin stays green without the guard: each asserts toThrow, and the old [null, 42, 'core'] silence pin was flipped, not kept.
  7. CI at head: Lint & Repo Gates success (the provenance red on bd29ec386f is cleared); Type Check · workspace, Test Core (5/6) and (6/6) still in progress at review time.

② Semver level

Correct. Probe of scripts/pm/clause2-line.mjs on the live body → {"kind":"declared","value":"yes","arm":"narrowing"}. Probe of check-adr-0087-registration.mjs: lint changeset → breaking: true, signals: ["clause-②-narrowing"], disposition not-required (no-migration-prescription) ok, prescription null (the "Fix" bullet is not framed as a rewrite); spec changeset → breaking: false, no marker owed. check-changeset-no-major level axis (:1640-1646): yes or a narrowing arm carries the axis; both moved packages graded minor → discharged; a narrowing ships minor in the launch window (:1463-1464). The row is a real widening: ledger header :101-102 "Registering a code widens this face and is therefore a Clause-② change, door or no door"; precedent 3f9e2eaa1c used the same @objectstack/spec: minor / Clause-②: yes shape for a new owner-key row.

③ Boundary flags

Implemented-by: claude/issue-20206-lint-packages-non-array-refused
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: FAIL

…ording

REWORK round 2 for #20206 (at-tier record 5856823202, items 1-2):

1. `validate-object-references.test.ts`'s non-array refusal test pinned
   only [null, 42, 'core']; add `{}` and a keyed object so all three
   validators pin the identical set `packagesOf` itself does.
2. Text corrected to match the head, wording only:
   (a) "four call sites" / "four copies" -> "five ... three files" in
       the `packagesOf` docblock and the `object-graph.test.ts` comment,
       now that `validate-mapping-target-fields.ts` is a fifth site.
   (b) every sentence claiming lint reads `null` "the way
       `resolveArtifactPackageOrder` does" or raises "the SAME code ...
       for the identical defect" is qualified for `null` specifically:
       on `main`, core still reads `null` as absent and names it via
       `typeof`; that only changes once #19926 (PR #20228) lands. Fixed
       in the docblock, the inline naming comment, the lint changeset
       and the ledger-row comment in `error-code-ledger.zod.ts`.
   (c) the lint changeset's door-reachability claim corrected: only
       `os lint` reaches `packagesOf`'s refusal (exit 1, message on
       stdout via `printError`, `code` under `--json`); `os validate`
       and `os build` already refuse a malformed `packages` earlier, at
       `ObjectStackDefinitionSchema.safeParse`, before these rules run
       (re-read `validate.ts:293`, `compile.ts:356`, `lint.ts:673/1140`,
       `format.ts:369` directly to confirm).

No `packagesOf` semantics changed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 145/145 CONTRACT_REVIEW_TIER
Head-sha: 68398a0e7e7e75474dc75003449e2f047daa7064

① Derived judgments

  1. BLOCKING — pin parity claimed, not delivered. validate-mapping-target-fields.test.ts:113 loops [{}, 0, 'x', null]: no keyed object, and no explicit packages: undefined control (absent-key only, e.g. array control at :97). object-refs loops [null, 42, 'core', {}, keyed], translation [{}, 0, 'x', keyed, null], packagesOf all five plus array and undefined. So PR body "all three validators now pin the identical set" and validate-object-references.test.ts:312-314 "pins the same set packagesOf and the other two validators do" are FALSE at head. Card item 3 ({}/0/'x' + array at each validator) is met. Fix: add { a: { manifest: {} } } (and packages: undefined as a control) to the mapping loop; reword "identical set" to "the same shape classes". No pin stays green without its guard: every refusal pin is toThrow; the not.toThrow(/of type object/) at object-graph.test.ts:341 is guarded by its sibling toThrow at :337.
  2. Text. (a) "five … three files" TRUE everywhere; git grep packagesOf at head → validate-object-references.ts:165, validate-translation-references.ts:753/848/921, validate-mapping-target-fields.ts:95; residual recordsOf(...packages) only in two test comments. (b) Base ab820016 and origin/main tip e4621867 both read artifact-packages.ts:208 if (declared === undefined || declared === null) return [artifact]; and :217 typeof declared; fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 open/unmerged, its diff drops the null branch and adds declared === null ? 'null' : typeof declared. Docblock, inline comment, lint changeset, ledger-row comment: all conditioned "once packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228) lands" → TRUE. object-graph.test.ts:334-335 "(matching resolveArtifactPackageOrder … PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228)" conditions by citation only; true as conditioned. (c) Doors: validate.ts:293 safeParse → exit(1) at :310/:327 before runAuthoringRules :377; compile.ts:356 → :361/:377 before :420; lint.ts has no safeParse; lintConfig :393 runs runAuthoringRules('lint') :673, called :972 inside try :939/catch :1138; --json → emitJson({ error, ...errorCodeFields }) exit 1 (code passes, status dropped: format.ts:278-288 reads httpStatus); text → printError = console.log (format.ts:369-370), exit 1; isReportedError needs reportedToStderr === true, unset. Also checked: scaffold-validate.ts:76 parses and returns before :94; runPerPackageAuthoringRules only after those gates (compile.ts:517, validate.ts:460, lint.ts:731); scoreMetadata only under os lint; no non-cli src importer of @objectstack/lint. Every door sentence TRUE. normalizeStackInput never touches packages.
  3. Code. git diff a4b05d6e15 HEAD --stat → 5 files, 41+/29-; packagesOf body :266-283 unchanged (hunk is comment-only); undefined → [], array → declared.filter(isRec) (= recordsOf :216), else Error with code: 'INVALID_ARTIFACT_PACKAGES', status: 422, null named 'null'. recordsOf :215-221 byte-identical to base. Ledger: gate prints "Fix: EITHER add the code under the stamping package's owner key … with a comment recording the wire path"; row at :1377 with wire-path comment; code already at :878 under core; union :1386 is new Set(...flat()); key matches /^@objectstack\/[a-z0-9-]+$/; lint publishConfig.access: public.
  4. Trailers: Co-Authored-By: Claude <noreply@anthropic.com> + Claude-Session: https://claude.ai/code/session_…; MODEL_ID_FORM (check-commit-card-trailers.mjs:320) no match; key lowercased :397. Model-free. 7be6204521 tree equals head tree; no remote ref contains it.

② Semver level

Correct. readClause2Line on live body → {"kind":"declared","value":"yes","arm":"narrowing"}. breakingDeclaration (check-adr-0087-registration.mjs:629-641) signals clause-②-narrowing for the lint changeset → marker owed and present (not-required (no-migration-prescription)); findMigrationPrescription → null for both changesets; spec changeset carries no breaking signal → no marker owed. check-changeset-no-major.mjs:1502 "narrowing — a BREAKING change; during the launch window it ships minor"; both packages minor. Ledger header :101-102 "Registering a code widens this face and is therefore a Clause-② change, door or no door" → spec Clause-②: yes; precedent 3f9e2eaa1c = @objectstack/spec: minor + Clause-②: yes.

③ Boundary flags

Implemented-by: claude/issue-20206-lint-packages-non-array-refused
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: FAIL

REWORK round 3 for #20206 (in-seat ruling 5857515836, at-tier record
5857513507, item 1 only):

1. `validate-mapping-target-fields.test.ts`'s non-array refusal loop
   pinned only [{}, 0, 'x', null] -- no keyed object, and no explicit
   `packages: undefined` control (only the array control at :97). Add
   the keyed object `{ a: { manifest: {} } }` to the loop, and a
   dedicated `packages: undefined` control test beside it, matching
   what the other two validators already pin.
2. Reword the one place that over-claimed the result before this fix
   landed: `validate-object-references.test.ts`'s comment said "pins
   the same set `packagesOf` and the other two validators do" -- now
   "pins the same shape classes ...", since the concrete number/string
   representatives differ across validators (42/'core' vs 0/'x') even
   though the shape classes now match everywhere. The PR body carries
   the matching correction.

Nothing else moves: packagesOf semantics, the five readers, the
ledger row, the door sentences and the null-parity conditioning are
all unchanged since 68398a0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 69/69 CONTRACT_REVIEW_TIER
Head-sha: 42b3bfbf2ee9beb4c806b051c2e9597009695e01

① Derived judgments

  1. BLOCKING — text untrue at head, introduced by this round's patch. validate-mapping-target-fields.test.ts:107-108 (the new control) says "sla_tier only resolves via the objectExtensions a package supplies (the test above)", and the PR body repeats it twice (round-3 bullet "only resolves through the objectExtensions a package supplies"; round-3 gates "the packages-supplied objectExtensions the array-control test provides"). In that fixture (:96-97) sla_tier comes from the STACK's own objectExtensions; region is the package-supplied field; extensionFieldsByTarget (validate-mapping-target-fields.ts:94-95) reads stack.objectExtensions before packagesOf. The retarget to full_name is still right (the first control carried no objectExtensions at all). Fix: reword the comment and both body phrases to "via an objectExtensions entry — the stack's own in the test above; region is the package-supplied one". Two body-only sentences from this round are also false; fold into the same fix. (a) "packagesOf and the ledger untouched since a4b05d6e15": git diff a4b05d6e15 HEAD moves object-graph.ts (31 lines, the docblock and inline naming comment) and error-code-ledger.zod.ts (25 lines, the row comment) in 68398a0e7e; only the function body and the row entry are unchanged, as the body's own round-2 section says. (b) "origin/main … has not touched either edited test file, no merge needed": 443b2f4fdc (feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246, committed 15:58:56Z) rewrote validate-mapping-target-fields.ts and its test before this head's 16:09:36Z author time. The merge is clean (scratch git merge: both auto-merge; the fixture, both new tests and the packagesOf read at merged :115, computed at :138 before any mapping is judged, survive; mergeable: true), and CI ran on the merge ref. Say that instead.
  2. Delta (git diff 68398a0e7e HEAD --stat: the two test files, 19+/4-). Mapping loop :129 [{}, 0, 'x', null, { a: { manifest: {} } }], each toThrow(objectContaining({ code: 'INVALID_ARTIFACT_PACKAGES', status: 422 })); array control :93-104; explicit packages: undefined control :106-117 is genuine: full_name is declared on contact, an unknown target yields a finding (:32-51), a refusal would throw, so toEqual([]) passes only on silence. Parity sentence TRUE: object-refs :315 [null, 42, 'core', {}, keyed] + undefined control :300-303 (perPackageStack sets packages: undefined) + array controls; translation :546 [{}, 0, 'x', keyed, null] + undefined :557 + array :663; packagesOf object-graph.test.ts:304-342. No refusal pin survives its guard: the only INVALID_ARTIFACT_PACKAGES stamp under packages/lint/src is object-graph.ts:283, no lint source imports @objectstack/core, and a recordsOf-style read of any of these shapes returns []/entries without throwing, so every toThrow pin reds; :341 not.toThrow(/of type object/) is guarded by :337. Count 41+47+134+11 = 233 matches "233/233"; 233-5 = 228.
  3. Landing-order line TRUE, and the known red is exactly the known red. On main: lintConfig → authoringRuleUnionStack → declaredPackageEntries(null) → resolver (artifact-packages.ts:208 returns [probe]) → [] → stack returned by identity with packages: null → runAuthoringRules('lint') (no per-rule catch) → validateReferenceIntegrity (commands: ALL, input: 'parsed'; lowerCallables shallow-clones, so packages survives) → validateObjectReferences (reference-integrity-suite.ts:224, runner has no catch) → artifactProvidedObjectNames (validate-object-references.ts:210, unconditional) → packagesOf throws. 6/6 log tail: @objectstack/cli Test Files 1 failed | 140 passed, Tests 1 failed | 1746 passed; the one failure is null-packages-follows-resolver.test.ts:153 leg 1 'os lint' lintConfig ("expected { kind: 'refused' } to deeply equal { kind: 'answered', value: [] }"); leg 2's os lint passes. Once fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 (open, unmerged, head 0706ffef7c) is on main, the resolver refuses inside declaredPackageEntries before any rule runs, with the same code/status, so leg 1 expects refused/refused → green. Census: no other test on main feeds a non-array packages into the lint rules. Every other check-run at head is success or skipped (Lint & Repo Gates, four Type Check jobs, Build Core, Dogfood, Temporal); the red Test Core rollup is the shard aggregate (Verify test shard results, empty output).
  4. Nothing else moved since 68398a0e7e; the full diff vs origin/main is the same 11 files (245+/12-). Trailers: Co-Authored-By (generic author, no model id) + Claude-Session; MODEL_ID_FORM (check-commit-card-trailers.mjs:320) does not match. Model-free.

② Semver level

Unchanged and correct. readClause2Line (scripts/pm/clause2-line.mjs) on the live body → {"kind":"declared","value":"yes","arm":"narrowing","line":"Clause-②: yes (narrowing)"}. Changesets untouched this round: @objectstack/lint: minor + Clause-②: no (narrowing) + adr-0087: not-required (no-migration-prescription); @objectstack/spec: minor + Clause-②: yes. Check Changeset green at head.

③ Boundary flags

Implemented-by: claude/issue-20206-lint-packages-non-array-refused
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: FAIL

… named

Addendum on the merge-only round for #20206: the round-3 CONTROL test's
comment said `sla_tier` "only resolves via the objectExtensions a
package supplies" -- in the fixture it actually cites (:173-174),
`sla_tier` comes from the STACK's own top-level objectExtensions, and
`region` is the package-supplied one. Reword to say that. The
`full_name` retarget itself was already correct and is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 43/43 CONTRACT_REVIEW_TIER
Head-sha: 82dc0c9c012ce3eb1ff24a4bb9a2675fad594a4c

① Derived judgments

  1. BLOCKING — null-parity text is now FALSE at head in shipped artifacts. The head carries fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 (a9fb83ef06, also on origin/main): packages/core/src/artifact-packages.ts:215 reads if (declared === undefined) return [artifact]; and :226 names null as 'null', so core refuses null exactly as packagesOf does. Yet object-graph.ts:244-247 says null "disagrees with resolveArtifactPackageOrder … which on main still reads null as absent; the two readers align once packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228) lands"; :262-264 "for null, only once … lands does core raise it too"; :277-279 "the naming … gives resolveArtifactPackageOrder once it lands, adopted early"; the lint changeset "resolveArtifactPackageOrder still reads it as absent on main, and the two readers align only once … lands"; ledger :1366-1368 "core still reads it as absent on main; core raises this code for null too only once … lands". Present-tense claims in source, a CHANGELOG entry and the ledger, not dated history. Fix (wording only, five places): core has refused null the same way since fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 (a9fb83ef06); drop "once … lands" and "adopted early". object-graph.test.ts:335 "(matching … PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228)" is TRUE as written. The body's round-1/round-2 "has not landed" / "once it lands" sit in dated round sections beneath a "Landing order" paragraph recording the merge: history, acceptable.
  2. BLOCKING — body round-3 gates sentence false in sequence: "origin/main re-checked before the round-3 commit … untouched at that time … (origin/main has since moved … via PR feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 443b2f4fdc)". 443b2f4fdc was committed 15:58:56Z; round 3 answers the record posted 16:06:53Z; 42b3bfbf2e was authored 16:09:36Z. origin/main carried feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 for the whole of round 3; nothing moved "since". Fix: "feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 had already rewritten both files (15:58:56Z) before round 3 began; the round-3 check missed it; picked up cleanly in round 4".
  3. Merge 3fef33e23b = 42b3bfbf2e + a9fb83ef06, pure: driver-free merge-tree --write-tree of those parents yields tree 09471c383c, identical to the merge's tree, so nothing beyond origin/main was added. 42b3bfbf2e..3fef33e23b: 287 files, 8926+/1939-, same file set as ab820016b..a9fb83ef06 (comm empty both ways). git diff a9fb83ef06 HEAD: the PR's 11 files, 246+/12-; HEAD minus merge is only 82dc0c9c01 (mapping test, 6+/5-). origin/main has since moved two commits (4d7e740d3b fix(formula)!: refuse comparands that are not one value at the CEL lowering and the write-check evaluator #20259, 17bd318771 fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260; the body says "once more", naming only the second), 16 files, none of the PR's 11 (so git diff origin/main HEAD shows 27 files: main's own reverse). Driver-free merge-tree --write-tree origin/main HEAD → ffb5f371b9, exit 0, no conflict.
  4. The four prior text items. (a) TRUE: validate-mapping-target-fields.test.ts:184-186 "sla_tier resolves via the STACK's own top-level objectExtensions (the test above, :173) — region is the one that needs a package (:174)"; :173 is objectExtensions: [{ extend: 'crm_contact', fields: { sla_tier … } }], :174 packages: [{ manifest: { objectExtensions: [… region …] } }]. (b) TRUE: body round-3 bullet "resolves via the STACK's own top-level objectExtensions, not a package's — region, in that same fixture, is the package-supplied one"; gates "resolves via the STACK's own objectExtensions, not a package's"; no "package supplies" residue. (c) TRUE: body now "packagesOf's function body and the ledger's row entry unchanged since a4b05d6e15; their surrounding comments moved in round 2": git diff a4b05d6e15 HEAD -- object-graph.ts has no non-comment line; the ledger diff touches no '@objectstack/lint' or 'INVALID_ARTIFACT_PACKAGES' line. (d) item 2.
  5. Everything previously passed holds. packagesOf object-graph.ts:266-283 unchanged: undefined → [], array → filter(isRec) (= recordsOf :215), else Error code: 'INVALID_ARTIFACT_PACKAGES', status: 422, null named 'null'; recordsOf :215-221 untouched. Five readers by git grep packagesOf: validate-object-references.ts:165, validate-translation-references.ts:753/848/921, and after feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246's rewrite validate-mapping-target-fields.ts:115 inside extensionFieldsByTarget (:103), called at :138 before any mapping is judged. Ledger row :1360-1378 intact. Pins: packagesOf (object-graph.test.ts:304-342: array and undefined controls, it.each {}/keyed/0/'x', null with /of type null/ and not.toThrow(/of type object/)); object-refs :300 undefined control, :315 [null, 42, 'core', {}, keyed]; translation :546 [{}, 0, 'x', keyed, null], :557 undefined, :663 array; mapping :170-181 array, :183-194 undefined control on full_name, :205 [{}, 0, 'x', null, keyed]; every refusal toThrow(objectContaining({ code, status: 422 })); 41+47+134+15 = 237.
  6. Leg 1 from source at head: lintConfig (lint.ts:398) → authoringRuleUnionStack → packageBodies (stack-collections.ts:175) → declaredPackageEntries(null) (:141-147: not undefined, not array → resolveArtifactPackageOrder({ packages: null })) → core :215-227 throws refuse('INVALID_ARTIFACT_PACKAGES') with status: 422 (:158-162); outcomeOf yields refused/422 for the verdict (:135-136) and the reader (:148-151) → green. CI on this SHA (one run set, started 17:51Z): 18 success, 5 skipped, 16 in progress incl. Test Core 6/6; nothing red.
  7. Trailers: 3fef33e23b carries no trailer (check-commit-card-trailers.mjs:130-131 treats trailer-less merges as clean); 82dc0c9c01 carries the generic co-author plus the Claude-Session: URL; MODEL_ID_FORM (:320) matches neither. Model-free.

② Semver level

Correct. readClause2Line (scripts/pm/clause2-line.mjs) on the live body → {"kind":"declared","value":"yes","arm":"narrowing","line":"Clause-②: yes (narrowing)"}. Both changesets byte-identical to 42b3bfbf2e: @objectstack/lint: minor + Clause-②: no (narrowing) + adr-0087: not-required (no-migration-prescription); @objectstack/spec: minor + Clause-②: yes. Check Changeset green at head.

③ Boundary flags

Implemented-by: claude/issue-20206-lint-packages-non-array-refused
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: FAIL

…20228 landed

REWORK — one more wording round for #20206 (seat ruling 5857515836,
extended). PR #20228 (#19926) merged as a9fb83e in round 4, so every
sentence still conditioning lint's null-refusal parity with
resolveArtifactPackageOrder on "once #20228 lands" was false at head.
Restated in the present tense, wording only, no code changed:

- object-graph.ts's packagesOf docblock (the null bullet and the
  @throws block) and its inline naming comment.
- the lint changeset's parity sentence.
- the ledger row comment in error-code-ledger.zod.ts.

git grep -n -E "once (it|#20228|#19926|PR #20228)[^.]*lands|still
reads (it|\`null\`) as absent|adopted early" over this PR's changed
files now returns 0 hits.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 79/79 CONTRACT_REVIEW_TIER
Head-sha: a38a259df60e106370f2ea9e9060e12bab8f59b6

① Derived judgments

  1. BLOCKING — the body's feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 ordering is still false, now in the other direction, and the body contradicts itself. GitHub timeline for feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246: added_to_merge_queue 15:58:56Z (the moment 443b2f4fdc was built; its merge_group runs on gh-readonly-queue/main/pr-20246-… start 15:59:14Z), merged 16:17:46Z, push run on main 16:17:48Z. main did not carry feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 before 16:17:46Z. Round 3: record 16:06:53Z, 42b3bfbf2e authored 16:09:36Z, amended 16:17:08Z, pushed about 16:20:12Z (earliest check-run start). So body line 97 "had already rewritten … before round 3 began; the round-3 check missed it" is false: at check time there was nothing on origin/main to miss (the pre-round-5 "has since moved" wording was right; the previous record's item 2 took the queue-build time for the landing time). Line 112 "landed on main after this PR's round 3" contradicts line 97 and holds only against the commit, not the push or the record. Smallest fix, two body edits: line 97 "feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 entered the merge queue 15:58:56Z (443b2f4fdc built then) and reached main at 16:17:46Z, after round 3's check and commit (16:09:36Z, amended 16:17:08Z) and before its push (about 16:20Z); round 4's merge picked it up cleanly"; line 112 "reached main at 16:17:46Z, between round 3's commit and its push".
  2. Delta git diff 82dc0c9c01 a38a259df6 --stat: 3 files, 10+/14-. .changeset/20206-lint-packages-non-array-refused.md line 5 (prose only; frontmatter, Clause-②: no (narrowing) and the adr-0087 marker byte-identical), object-graph.ts :244-246 (docblock), :261-262 (@throws), :275-277 (inline), error-code-ledger.zod.ts :1366-1367. TypeScript scanner with comment trivia dropped: object-graph.ts 696 tokens before and after, ledger 1996 and 1996, sequences identical. No code token moved.
  3. Every rewritten sentence TRUE at head. packages/core/src/artifact-packages.ts:215 if (declared === undefined) return [artifact];, :219-227 refuse('INVALID_ARTIFACT_PACKAGES', …) with status = 422 (:158-162), :226 declared === null ? 'null' : typeof declared; the file is identical to origin/main, and a9fb83ef06 (fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228, ancestor of head) is the commit that dropped || declared === null. Docblock "refused here exactly as resolveArtifactPackageOrder refuses it, same code and status, since packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228, a9fb83ef06)", @throws "core raises the same code for null too (packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926)", inline "the same naming resolveArtifactPackageOrder uses (null named as null)", changeset "resolveArtifactPackageOrder refuses null the same way (packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926)", ledger "core raises this code for null too (packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926)": each matches that code. The prescribed regex over the 11 PR files at head: 0 hits. Wider scan (20228|19926|lands|landed|not yet|has not|once|until|pending) over the same files: no conditional or not-yet-landed claim remains; object-graph.test.ts:335 "(matching … PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228)" and the three validator-test comments cite the ruling only.
  4. Body otherwise: round-1 :31 "once it lands, adopted early here", round-2 :50 "has not landed … it is qualified with 'once … lands' in the docblock …", round-3 :103 "has not merged as of this push" sit in dated round sections; the Landing-order paragraph :60 and "Round 4 — merge (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228 landed as a9fb83ef06)" :105 record the merge (merged_at 16:40:10Z, as the body says). History, acceptable. Clause-②: yes (narrowing) is body line 3, alone; readClause2Line (scripts/pm/clause2-line.mjs at head) returns {"kind":"declared","value":"yes","arm":"narrowing","line":"Clause-②: yes (narrowing)"}.
  5. Merge state: git --attr-source=4b825dc6… merge-tree --write-tree origin/main a38a259df6 writes tree bd2e4e5fd8, exit 0, no conflicts. origin/main is 1207baf01d, three commits past the merge-base a9fb83ef06 (4d7e740d3b fix(formula)!: refuse comparands that are not one value at the CEL lowering and the write-check evaluator #20259, 17bd318771 fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260, 1207baf01d feat(lint,metadata-protocol,cli)!: RLS read scopes are admitted by the engine judge when authored, at the save door and at os validate / build / lint #20265), 34 files, none of the PR's 11 (comm -12 empty). feat(lint,metadata-protocol,cli)!: RLS read scopes are admitted by the engine judge when authored, at the save door and at os validate / build / lint #20265 touches packages/lint but adds no stack.packages reader: git grep on origin/main for recordsOf(…packages) or .packages in packages/lint/src (non-test) finds only the five sites this PR replaces. Body :125 names 17bd318771 alone; stale on main's side, not this PR's.
  6. CI at head (18:29:17Z): 40 check-runs, 30 success, 5 skipped, nothing red. Lint & Repo Gates success, Build Core success, all four Type Check jobs bar one success, Check Changeset success (both runs), Test Core (2/6) and (4/6) success. Still in progress: Test Core (1/6), (3/6), (5/6), (6/6) and Type Check · workspace; the cli shard carrying packages/cli/test/null-packages-follows-resolver.test.ts is among the pending shards, so its result at this head is not yet measured (leg 1 is green by construction: core :215-227 refuses null before any lint rule runs).
  7. Trailers on a38a259df6: the generic co-author (bare name Claude with the harness address) plus Claude-Session: https://claude.ai/code/session_…. MODEL_ID_FORM (scripts/check-commit-card-trailers.mjs:320, /\bclaude-[a-z]+(?:[-.][a-z0-9]+)*\b/i, read over the value only, :314-319) matches neither value; the display name equals DECLARED_COAUTHOR's bare name; the session value starts with DECLARED_SESSION_PREFIX. Model-free.

② Semver level

Unchanged and correct. The delta to .changeset/ is one prose line; grades and markers byte-identical to 82dc0c9c01: @objectstack/lint: minor + Clause-②: no (narrowing) + adr-0087: not-required (no-migration-prescription); @objectstack/spec: minor + Clause-②: yes. Body Clause-②: yes (narrowing) reads declared/yes/narrowing (item 4). Check Changeset success at head, both runs.

③ Boundary flags

  • packages/cli/src/utils/stack-collections.ts:172 (and :119), at head and on origin/main: "for null while the resolver still reads it as absent, answered []" / "until it does, the resolver still": the cli lane's comment, stale since fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228; not this PR's file.
  • The body carries no line recording a38a259df6, so round-2 :50 still describes the four places as qualified; one sentence under Round 4 closes it, foldable into item 1's edit.
  • The 15:58:56Z premise in the previous record (and in this round's brief) is the merge-queue build time of 443b2f4fdc; the landing time on main is 16:17:46Z.

Implemented-by: claude/issue-20206-lint-packages-non-array-refused
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: FAIL

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 107/107 CONTRACT_REVIEW_TIER
Head-sha: a38a259df60e106370f2ea9e9060e12bab8f59b6

① Derived judgments

  1. Body re-read live (updated_at 18:31:10Z; head unchanged, mergeable: true). Diff against the body judged in the prior record: exactly line 97, line 112 and a new Round 5 paragraph at line 127; nothing else moved. Line 97 now "PR feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 (443b2f4fdc) entered the merge queue at 15:58:56Z and reached main at 16:17:46Z: after round 3's check and commit (16:09:36Z, amended 16:17:08Z) and before its push (about 16:20Z). Round 4's merge picked it up cleanly." TRUE against the feat(spec,rest,lint): an import mapping target may name a declared part of a compound field (mailing_address.street) #20246 timeline (added_to_merge_queue 15:58:56Z, merged 16:17:46Z, push run on main 16:17:48Z), 42b3bfbf2e (author 16:09:36Z, commit 16:17:08Z) and its earliest check-run start 16:20:12Z. Line 112 now "reached main at 16:17:46Z, between round 3's commit and its push": TRUE, and it agrees with line 97. No "after this PR's round 3" or "missed it" residue. Round 5 paragraph: "every null-parity sentence now states core's refusal in the present tense: the packagesOf docblock, its @throws, the inline naming comment, the lint changeset and the ledger-row comment" (TRUE, item 3), "the round-1/2 sections above that say 'once … lands' are history" (consistent with item 4), "git grep sweep: 0 hits" (TRUE, item 3), "the queue build at 15:58:56Z versus the landing on main at 16:17:46Z" (TRUE). Nothing else in the body is now false: Landing order :60 and Round 4 :105 ("merged at 16:40Z" = merged_at 16:40:10Z) hold; :126 "moved once more (17bd318771)" was true when written and is main-side (item 5).
  2. Delta git diff 82dc0c9c01 a38a259df6 --stat: 3 files, 10+/14-, all comment or prose: changeset line 5 (frontmatter, Clause-②: no (narrowing) and the adr-0087 marker byte-identical), object-graph.ts :244-246, :261-262, :275-277, error-code-ledger.zod.ts :1366-1367. TypeScript scanner with comment trivia dropped: object-graph.ts 696 tokens before and after, ledger 1996 and 1996, sequences identical. No code token moved.
  3. Every rewritten sentence TRUE at head. packages/core/src/artifact-packages.ts:215 if (declared === undefined) return [artifact];, :219-227 refuse('INVALID_ARTIFACT_PACKAGES', …) with status = 422 (:158-162), :226 declared === null ? 'null' : typeof declared; file identical to origin/main; a9fb83ef06 (fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228) is the ancestor that dropped || declared === null. Docblock "since packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926 (PR fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228, a9fb83ef06)", @throws "core raises the same code for null too", inline "the same naming resolveArtifactPackageOrder uses (null named as null)", changeset "refuses null the same way (packages: null on a release artifact: the schema and composeStacks refuse it, while every reader reads it as absent #19926)", ledger "core raises this code for null too" each match that code. Prescribed regex over the 11 PR files: 0 hits; wider sweep (20228|19926|lands|landed|not yet|has not|once|until|pending): no conditional claim remains.
  4. Round-1 :31, round-2 :50, round-3 :103 "once it lands" / "has not landed" / "has not merged as of this push" sit in dated round sections under the Landing-order paragraph, Round 4 and now Round 5: history. Clause-②: yes (narrowing) is line 3, alone; readClause2Line (scripts/pm/clause2-line.mjs at head) on the live body returns {"kind":"declared","value":"yes","arm":"narrowing","line":"Clause-②: yes (narrowing)"}.
  5. Merge state: driver-free merge-tree --write-tree origin/main a38a259df6 writes bd2e4e5fd8, exit 0, no conflicts. origin/main 1207baf01d is three commits past the merge-base a9fb83ef06 (fix(formula)!: refuse comparands that are not one value at the CEL lowering and the write-check evaluator #20259, fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) #20260, feat(lint,metadata-protocol,cli)!: RLS read scopes are admitted by the engine judge when authored, at the save door and at os validate / build / lint #20265), 34 files, none of the PR's 11. feat(lint,metadata-protocol,cli)!: RLS read scopes are admitted by the engine judge when authored, at the save door and at os validate / build / lint #20265 touches packages/lint but adds no stack.packages reader: git grep on origin/main finds only the five recordsOf(stack.packages) sites this PR replaces.
  6. CI at head, complete at 18:32:12Z: 49 check-runs, 42 success, 7 skipped, 0 pending, nothing red. Lint & Repo Gates success (18:28:11Z); Type Check source, consumer, debt ledger and workspace all success; Build Core success; Check Changeset success three times, including the 18:32:10Z re-run the body edit triggered; Test Core 1/6 to 6/6 all success plus the rollup. The cli shard is 6/6: @objectstack/cli:test Test Files 141 passed (141), Tests 1747 passed (1747) (the round-3 red was 1 failed of 141 files / 1747 tests, the leg-1 os lint case), check-test-completeness: OK (12 of 12 scheduled package(s) … 7056 test(s) declared and all accounted for), shard attested green. So null-packages-follows-resolver.test.ts is green at this head with core refusing null (:215-227) before any lint rule runs.
  7. Trailers on a38a259df6: the generic co-author (bare name Claude with the harness address) plus Claude-Session: https://claude.ai/code/session_…. MODEL_ID_FORM (scripts/check-commit-card-trailers.mjs:320, /\bclaude-[a-z]+(?:[-.][a-z0-9]+)*\b/i, read over the value only, :314-319) matches neither value; display name equals DECLARED_COAUTHOR's bare name; session value starts with DECLARED_SESSION_PREFIX. Model-free.

② Semver level

Unchanged and correct. The delta to .changeset/ is one prose line; grades and markers byte-identical to 82dc0c9c01: @objectstack/lint: minor + Clause-②: no (narrowing) + adr-0087: not-required (no-migration-prescription); @objectstack/spec: minor + Clause-②: yes. Body Clause-②: yes (narrowing) reads declared/yes/narrowing (item 4). Check Changeset success at head, all three runs.

③ Boundary flags

  • packages/cli/src/utils/stack-collections.ts:172 (and :119), at head and on origin/main: "for null while the resolver still reads it as absent, answered []" / "until it does, the resolver still": the cli lane's comment, stale since fix(core,runtime,plugin-dev,plugin-security): refuse packages: null as malformed, never absent #20228; not this PR's file.
  • Body :126 names only 17bd318771 as main's movement; origin/main now carries three commits past the merge-base, none touching PR files; main-side drift, not a claim this PR must fix.
  • The 15:58:56Z figure in earlier records was the merge-queue build time of 443b2f4fdc; the landing time on main is 16:17:46Z, as the body now says.

Implemented-by: claude/issue-20206-lint-packages-non-array-refused
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 18:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 5f9d7d7 Sep 27, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20206-lint-packages-non-array-refused branch September 27, 2026 18:57
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…bjectstack-ai#20253)

Fixes objectstack-ai#20216
Clause-②: no (narrowing)

**Landing order: PR objectstack-ai#20228 → PR objectstack-ai#20229 → this PR.** objectstack-ai#20228 has merged.
objectstack-ai#20229 (the `packages[]` read in `artifactProvidedObjectNames`, still in
a rework round) edits the same file. This PR does not touch that hunk,
its import line, or `packages/lint/src/object-graph.ts`, and a local
`git merge-tree` of this branch against objectstack-ai#20229's head `a4b05d6` writes a
clean tree. The seat enqueues this PR after objectstack-ai#20229 merges, and this
branch merges `origin/main` then.

## CI fix round: `Test Core (6/6)`, and a file-surface amendment

- **What went red:**
`packages/cli/test/generate-scaffold-validates.test.ts`, "`os g 'view'`
writes a stack `os validate` accepts", got `object-reference-unknown at
views[0].object: view container object "probe_thing" …`.
- **Why:** the harness judged each scaffold in a host stack holding only
the collection under test. The view scaffold binds `probe_thing`, the
object `os g object probe_thing` writes, but no object was in the stack.
The new leg refused it correctly: the omission was the harness's, not
the template's. Reproduced red locally before the fix (1 failed, 17
passed).
- **Fix (test fixture only):** every `namesObject` generator other than
`object` itself (view, action, flow, app) is now judged beside the
object scaffold for the same name, materialized through the same
`bundle-require` loader. A new pin asserts the view's binding and the
seeded object's `name` are the same spelling, and that a non-binding
generator (`dashboard`) and `object` itself carry no seeded object. ⛔
The rule is not weakened, `probe_thing` is not special-cased, and no
test is skipped.
- **File-surface amendment:**
`packages/cli/test/generate-scaffold-validates.test.ts`, test fixture
only. No other CLI fixture needed a change (see the runs below).
- **Other generators:** action, flow and app also bind `probe_thing`.
They passed before the seeding and still pass with it.

## What changed

A view container's own `object` (`ViewSchema.object`) is the key the
runtime indexes views by (`getViewsByObject()` / `GET
/meta/view?object=`). Nothing resolved it at authoring time:
`defineStack`'s `validateCrossReferences` reads a container's
`list.data` / `form.data` bindings, never the container's own key.

- `packages/lint/src/validate-object-references.ts` gains a
view-container leg beside the relationship-target leg. It uses the same
`check` ladder and the same `resolvable` set: the stack's objects plus
what its `packages[]` provide.
- An unresolved unprefixed name is an **error**,
`object-reference-unknown` at `views[N].object`.
  - A known platform object passes.
- A platform-shaped name that nothing registers gets the existing
`object-reference-unregistered-platform` advisory.
- The refusal is located and carries a prescription:
  - it lists the objects the stack declares (`Defined objects: ...`);
- when the bound name is exactly a declared object minus the stack's
`manifest.namespace` prefix, the hint names that object: `write
"my_app_order_line", not "order_line"`.
- It gates like its sibling. It is the same member of the same
reference-integrity suite entry, so `os validate`, `os build` and `os
lint` all run it at the same tier.
- Not judged, on purpose:
- a container with no `object` (its binding falls back to
`list.data.object` / `form.data.object` / `name`, which is a different
reference);
- a runtime-authored container (objectstack-ai#13407 is out of scope). This member
does not run on a `view` write at the runtime publish gate, and the
runtime is untouched.
- ⛔ No second copy in `packages/spec/src/stack.zod.ts`.

## Measured at the public door (CLI built at this branch)

The scratch project has `manifest.namespace: 'my_app'`, an object
`my_app_order_line`, and a view container with `object: 'order_line'`.

| run | `os validate` | `os build` |
|:--|:--|:--|
| leg disabled (ablation, lint rebuilt, marker proven in `dist/`) | exit
0, "Validation passed", nothing about the view | not run |
| this branch, `object: 'order_line'` | exit 1,
`object-reference-unknown at views[0].object`, hint names
`my_app_order_line` | exit 1, same rule and path |
| this branch, `object: 'my_app_order_line'` (control) | exit 0 | exit 0
|

`os generate view order_line` in the same namespaced project (after PR
objectstack-ai#20214) writes `object: 'my_app_order_line'`, which passes.

## Census of producers (H2)

The instrument is one TypeScript-AST scan at `0d60f88760`. It reads
`examples/**`, `packages/**` (tests and fixtures included), `skills/**`,
and the ts/js code fences in `content/docs/**` md/mdx (generated
`references/` excluded). It covers 7242 files and counts object literals
that carry a view-container slot
(`list`/`form`/`listViews`/`formViews`). A name resolves when it is
declared by an object literal (`name` + `fields`) anywhere in the
corpus, or when it is a platform-provided object.

| tree | containers | carrying `object` | unresolved |
|:--|--:|--:|--:|
| examples | 10 | 0 | 0 |
| packages | 367 | 105 | 11 |
| skills | 3 | 0 | 0 |
| content/docs | 21 | 0 | 0 |

- **Control from the same instrument:** 94 of the 105 containers
carrying `object` resolve.
- **No example or platform package ships a dangling container.** No
example container carries `object` at all; they bind through
`list.data.object`.
- **The 11 unresolved:**
- 10 are non-literal `object` expressions in code, not stored views:
schema/`strictObject` definitions in `view.zod.ts`, walkers in
`validate-translation-references.ts` /
`validate-translatable-sections.ts` / `protocol.ts`, a helper in a rest
measurement test, and the parameterised helper in this PR's own test.
- 1 literal: `packages/cli/test/format-zod-union.test.ts`
(`union_probe_obj`). That specimen fails schema parse first, which that
file asserts as exactly one `invalid_union` issue. `os validate` exits
at the schema step, so author-time rules never run on it. No pin flips.
- **Pin sweep ①:** grepping `object-reference-unknown` and the rule's
message across the repo found no pin on a view container. The pins in
`packages/cli` (`artifact-packages.test.ts`,
`build-multi-package-artifact.e2e.test.ts`,
`union-fold-command-parity.test.ts`) have fixtures with no container
`object`, so none flips. **②:** nothing flipped, so no load-bearing
re-pin was owed.

## Tests (at `60808317dc` unless marked)

- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`:
**109 files / 4242 tests passed**.
- `pnpm --filter @objectstack/lint typecheck`: exit 0 (`tsc --noEmit`
plus `check:test-typecheck: OK`).
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2`: **227 files / 3219 tests passed**. This includes the
fixed `generate-scaffold-validates.test.ts` (19 of 19).
- CLI integration tier, the 21 files that reference views or scaffolds
(`--project integration`): **21 files / 196 tests passed**.
- `pnpm --filter @objectstack/cli typecheck`: exit 0
(`check:test-typecheck: OK`).
- Nightly tier (`OS_TEST_TIERS=nightly`), 7 e2e files on views or
scaffolds: 6 files passed. One test failed in
`generate-agent-retired.e2e.test.ts` ("`os g object … --dry-run` still
previews a typed object file"). It expects `import * as Data from
'@objectstack/spec/data'`, but the object template writes `import {
ObjectSchema }` since objectstack-ai#20195. That failure is independent of this PR:
neither file differs from the merge base (0 diff lines).
- New pins in `validate-object-references.test.ts` (two new `describe`
blocks, appended so they stay clear of objectstack-ai#20229's hunk):
- `object: 'order_line'` in a `my_app` stack is refused, naming
`my_app_order_line`; the prefixed container is the control;
  - the map form of `views` is read;
  - a non-prefix miss is refused without the namespace prescription;
- a container over a `packages[]` sibling's object passes, and the same
package alone is refused (control);
  - `sys_user` passes and `sys_approval_process` advises;
  - no views, `views: []`, and a container with no `object` stay silent;
- the finding reaches the gating tier of `runAuthoringRules` for
`validate`, `build` and `lint`.
- **Unit ablation** (`scripts/ablation-replace.mjs`, anchor `const bound
= strName(view.object);`, anchor count 1 to 0, blob `27699c9` to
`03f6f47`): **8 refusal pins red; 48 green, including both controls
(prefixed container, silence).** Restored with blob equal to HEAD and
`git diff HEAD` empty. Taken at `9fa1ff4c61`. Since then only comment
lines changed in `src`.
- **Door ablation:** marker planted, `@objectstack/lint` rebuilt,
`ablation-dist-preflight` found the marker in 4 built files, and `os
validate` exited 0. Then the restore leg: blob equal to HEAD, lint
rebuilt, preflight `--absent` confirmed the marker gone from all 14
built files with a clean tree, and `os validate` exited 1 again.

## Gates (derived by `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `60808317dc`)

- 60 families derived (one new: `check:cli-test-child-env`, green) and
all run at `60808317dc`. `--ran` reconciliation with exit codes: 59 run,
1 NOT MEASURED, 0 unrun.
- The ones this diff actually moves are green:
`check-adr-0087-registration` (disposition `not-required
(no-migration-prescription)` accepted), `check-changeset-no-major`,
`check-empty-changeset`, `check:doc-authoring`, `check:nul-bytes`,
`check-closing-keyword-parity`, `check:published-files`,
`check:type-check-coverage`, `check:test-source-alias`.
- `check:type-check-debt` now measures green (`none above its recorded
number`).
- NOT MEASURED: `check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT
MET: it needs every package's `dist/`, and this box built the lint and
CLI closures only). Declared to CI.
- Correction to the first round: I declared the `packages/cli` suites to
CI without running them, and the census missed `os g view` because its
container sits inside a template string the AST scan cannot see. That is
the red this round fixes. The CLI unit project now runs in full here.

## Changeset

`.changeset/20216-view-container-object-refused.md`:
- `@objectstack/lint: minor`, carrying a **BREAKING** banner and
`Clause-②: no (narrowing)`;
- a before/after accept-set table;
- ADR-0087 `not-required (no-migration-prescription)`: nothing
authorable moves in spec, and which object an author meant is a fact
about their stack, not a mechanical conversion.

The table is a behaviour table (door: FROM exit 0, TO exit 1). My first
draft headed it "FROM → TO", and the ADR-0087 gate read that heading as
a rewrite prescription and refused the exemption. The heading now says
"before and after", and the table carries no rewrite row.

## Acceptance notes (not filed)

- The namespace prescription is on this leg only. The other sites on the
same rule (a field `reference`, action params, dataset base object)
could give the same hint for the same missing-prefix miss. I noted it
and did not widen it here. Carrier: none.
- A container whose `object` and `list.data.object` name different
objects is not judged by any rule. It is out of this card's scope, and I
found no instance in the census.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

2 participants