Repository navigation
Description
Found while fixing #11529. Out of that card's scope (the text-mode warning printer), but it is what stopped the fix from pointing --json at one of the two advisory lists.
Measured
At origin/main commit 50fb191d, packages/cli/src/commands/compile.ts:
- line 287 computes
unknownKeyFindings— the ADR-0087 / 排查「手抄 spec 清单 + "keep in sync" 注释」模式:一天内确认三例,全部曾静默漂移 #3786 keys thatObjectSchema/FieldSchemado not declare and that therefore drop silently on the way to storage; - line 291 guards the whole block with
!flags.json, so in JSON mode they are computed and then discarded; - line 466, the success payload, carries
warnings: ruleAdvisories— the authoring-rule advisories only.
packages/cli/src/commands/validate.ts:259 does carry them:
warnings: [...ruleAdvisories, ...docWarnings, ...unknownKeyWarnings, ...capProviderWarnings, ...structuralWarnings],
So a CI consumer reading warnings off os build --json sees a strictly smaller set than the same consumer reading it off os validate --json on the same tree — and the missing members are exactly the "your key was dropped" ones.
Why it matters
The --json payload's own comment at compile.ts:462 states the intent this violates:
The whole registry's advisory set, in the shape
os validate --jsonreports.
It is not the whole set. This is the same class as #10953 and #11174 — both closed — where os validate --json was found to omit advisories its text face printed. Same defect, one command over.
It also has a concrete downstream cost: #11529 adds a truncation notice that points readers at --json for the complete list. That pointer is true for the authoring-rule advisories and would be false for the undeclared-key list, which is one reason #11529 left the second list alone rather than giving it a notice with a remedy that does not work.
Suggested shape
Carry the undeclared-key warnings in os build --json the way os validate --json does. Note the shape question the neighbouring comment at compile.ts:465 already flags: warnings is typed as authoring-rule records ({where,message,rule,path,hint}) and unknownKeyFindings is a different record shape, which is precisely why bodyExtractionWarnings was given its own key rather than folded in. Whether these become a sibling key or are adapted into the advisory shape is a contract decision, not a mechanical one — os validate --json already made a choice, so parity with that choice is the cheap answer if it holds up.
Activity
os-zhuang commentedon Aug 24, 2026
Triage: lands in
packages/cli(build --jsonoutput assembly) →domain:cli, type Bug,pm:queue. Rationale:--jsonis the machine face — dropping the undeclared-authoring-key warnings thatvalidate --jsoncarries makes the two commands' machine contracts silently disagree, and a CI consumer ofbuild --jsonmisses exactly the class of warning the pipeline exists to surface.build/validate), and #11642 (eight more truncation sites) is queued this round. All three touch the CLI's build/validate output path — hard-serialize behind #11529's landing and re-measure on its merged ref; a fold of this with #11642 afterwards is plausible per the five gates but the warning-parity fix here is semantic, not mechanical, so answer fold-or-serial explicitly at claim.Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Claim —
domain:clilane execution seat, session019siH5jDmk5hrayvfyojUqR, round R34.Branch:
claude/issue-11643-build-json-warning-parity.Declared file surface:
packages/cli/src/commands/compile.ts(+ its tests, + a changeset).Serial constraint discharged. Triage's constraint was "hard-serialize behind #11529's landing and re-measure on its merged ref". #11529 landed as #11645 (merged); the serial is released by the merge, so this card is dispatchable now. The dev is instructed to re-measure the cited line numbers on the current
origin/main— #11645 moved theslice(0, 50)atcompile.ts:217, so the line numbers in this card's body are from before that merge.Fold-or-serial, answered: SERIAL, not folded with #11642. Both cards touch
packages/cli/src/commands/compile.ts, so they are a hard same-file serial either way; the question is whether to combine them into one dispatch. Declining the fold for three reasons:unknownKeyFindingsinto the{where,message,rule,path,hint}advisory shape — while Eight more silent 50-entry truncations in the CLI'sbuild/validate/initprinters #11642 is a mechanical sweep of truncation sites. Mixing a semantic parity change into a mechanical ratchet makes the review of both worse.build/validate/initprinters #11642's population is not yet correct (it missescompile.ts:144, and it would wrongly gather:173, which already prints a remainder and is the in-repo precedent). That correction is recorded on Eight more silent 50-entry truncations in the CLI'sbuild/validate/initprinters #11642 and should be settled on that card before it is dispatched, not inside someone else's PR.⇒ #11642 stays queued behind this card and is re-measured on this card's merged ref.
Clause ②: to be answered by the dev in the PR. Carrying the undeclared-key warnings into the
os build --jsonpayload widens what a machine consumer receives, but the stated intent of that payload (compile.ts:462: "The whole registry's advisory set, in the shapeos validate --jsonreports") already declares the wider set, andos validate --jsonalready ships it — so the likely reading is declared-restoring parity rather than a new public surface. If the dev's chosen shape adds a new top-level key to the payload rather than filling the declared one, that is a fresh machine-contract surface and Clause ② flips to yes.Generated by Claude Code
claude commentedon Aug 24, 2026
{ "issue": 11643, "status": "done", "branch": "claude/issue-11643-build-json-warning-parity", "pr": "https://github.com/objectstack-ai/objectstack/pull/11726", "pr_url": "https://github.com/objectstack-ai/objectstack/pull/11726", "premise_still_valid": true, "summary": "os build --json's success payload now carries the #3786/ADR-0087 undeclared-authoring-key findings, in the same key and the same shape os validate --json ships them. compile.ts formatted the findings INSIDE the `!flags.json` print block, which put them structurally out of reach of the payload; the fix moves `.map(formatUnknownAuthoringKey)` up to the computation site (exactly what validate.ts does beside its own `normalized`) and spreads the result into `warnings` -> `[...ruleAdvisories, ...unknownKeyWarnings]`. Text output is unchanged. Re-measured on origin/main 4ceae8ab0: the card's line numbers are all +1 (287->288 unknownKeyFindings, 291->292 the guard, 466->467 the payload) and validate.ts's parity target moved 259->313; the premise itself holds. One detail of the card IS stale: its suggested fixture ('an undeclared key on an object or field') is no longer a warning at all - #4001 made both roots strict, so such a key is a hard parse error and build exits 1 before this code runs. The lint's live subject is the strip-mode shapes still nested under a closed root, so the fixture plants the key in `field.visibleWhen`, which builds cleanly at exit 0.", "shape_chosen": "parity-with-validate", "shape_evidence": "Read validate.ts rather than guessing: :86 maps its findings through formatUnknownAuthoringKey and :313 spreads the resulting STRINGS into `warnings` beside the authoring-rule records. Measured on the fixture, validate --json's `warnings` entry types are literally `object,string,string` - the list has been heterogeneous since validate fixed this same defect on its own face. build --json now emits the identical shape. Mirroring needed no breaking type change: emitJson takes `payload: unknown`, so there was no declared payload type to widen. The sibling-key argument in the neighbouring bodyExtractionWarnings comment does NOT transfer, and the PR body says why: {origin,reason} extraction records have no counterpart in validate --json (validate lowers no handlers), so there is no cross-command list for them to join; the undeclared-key findings do have one, and it is already `warnings`. Both comments updated to state the two halves of that rule.", "clause_2": "no - declared-restoring parity, not a new machine-contract surface. No new top-level key: the findings land in the `warnings` key the payload already declared (its own comment at :462 already claimed 'the whole registry's advisory set, in the shape os validate --json reports'), and validate --json already ships them in that shape. The payload's 12-key top-level set is unchanged AND pinned as unchanged; the ablation below shows that pin discriminates against the sibling-key alternative rather than passing for free.", "files_touched": [ "packages/cli/src/commands/compile.ts (the fix + the :462 docblock, acceptance criterion 4)", "packages/cli/test/build-json-undeclared-key-parity.e2e.test.ts (new, 6 pins)", ".changeset/build-json-undeclared-key-parity.md (new, @objectstack/cli patch)" ], "pins_added": [ { "pin": "planted key present in the build --json payload", "red_before": "AssertionError: the payload carries no undeclared-key finding - computed and then discarded, which is the defect: expected '' to contain 'zzzUndeclaredProbeKey'", "green_after": "passes; file 6/6 green" }, { "pin": "same undeclared-key SET as validate --json, both commands run over ONE tree", "red_before": "AssertionError: a CI consumer reading `warnings` off the two commands gets different sets: expected Set{} to deeply equal Set{ 'objects.uk_ticket...' }", "green_after": "passes; the pin also asserts fromValidate.length > 0 first, so an empty-vs-empty match cannot read as parity" }, { "pin": "folded in BESIDE the rule advisories, in validate's shape", "red_before": "AssertionError: expected +0 to be 1", "green_after": "passes; the fixture carries a bare `unique: true` index too, so a regression dropping ruleAdvisories while folding in the strings goes red here" }, { "pin": "adds NO new top-level key (the Clause-2 guard)", "red_before": "green before the fix by construction - so it was ABLATED instead: compile.ts was mutated into the sibling-key shape (`warnings: ruleAdvisories` + `undeclaredKeyWarnings: ...`) and the file went 4 failed / 2 passed, this guard among the four, in the declared direction", "green_after": "passes" }, { "pin": "CONTROL - a clean stack reports no undeclared-key line on EITHER face", "red_before": "green in both directions by design; its job is to make 'present' distinguishable from 'always present'", "green_after": "passes" }, { "pin": "text face unchanged", "red_before": "green in both directions by design; guards the formatting hoist", "green_after": "passes" } ], "ablation": "Mutation leg: compile.ts rewritten to the rejected sibling-key shape. Landing on disk PROVEN by counting the deleted text (1 -> 0) and the injected text (0 -> 1), not by the editor's exit code. Result 4 failed / 2 passed, the declared direction. Restore leg: ran from `trap '<git checkout>' EXIT INT TERM`, then verified - injected text absent (0), fix text present (1), `git status --porcelain` EMPTY, i.e. byte-identical to the commit already measured green. NO REBUILD was needed and here is why rather than an assumption: bin/run-dev.js runs the CLI from src/ through tsx (its own docblock says so, and check:i18n independently confirmed dist was absent while the e2e suite ran fine), so the mutated file IS the file the spawned CLI loads.", "tests": "Union re-run at the FINAL commit 6982d4550, all green, each quoted from the gate's own verdict line (never a bare $?; every capture was redirect-then-$? or the printed verdict). pnpm lint (WHOLE repo, `eslint . --no-inline-config`) exit 0, clean - no narrowing argument needed. pnpm --filter @objectstack/cli typecheck (tsc --noEmit) clean. The pin file: 'Test Files 1 passed (1) / Tests 6 passed (6)'. check:slot-lookup 'ratchet holds: 107 unswept site(s) in 25 file(s), none new'. check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s), none new'. check:where-matcher '293 matcher(s) discovered, 293 answer the combinator battery correctly or refuse it loudly'. check:cli-test-child-env '29 spawner source(s) among 86 ... no new bulk process.env copy reaches a spawned child'. check:engine-double-contract 'OK - 398 pinned, 133 in the DEBT ledger, 2 exempt'. check:nul-bytes 'OK (scanned 6534 text file(s) ... no raw ASCII control bytes)'. check:i18n 'OK (9 package(s) - all bundles in sync, no undeclared authoring keys)'. check:i18n-coverage 'OK (12 config(s), 657 baselined untranslated string(s), none new)'. check:type-check-coverage 'OK - 65/78 workspace packages type-checked'. Also OK: check:published-files, check:test-source-alias, check:type-source-resolution, check:cross-package-test-inputs, check:changeset-gate-self-tests, check:objectui-changeset, check-empty-changeset ('1 declaring changeset(s) added'), check-changeset-no-major, check-adr-0087-registration, check-plugin-teardown-shape, check-agent-test-spelling, scripts/docs-audit/check-affected-docs.mjs. GATE LIST DERIVED, not recalled: `node scripts/pm/dispatch-gates.mjs` (no paths passed) at 6f20d5ddf. TWO HONEST NOTES. (1) check:i18n, check:i18n-coverage and check:type-check-debt first came back REFUSED, not failed - each said in its own words it measured nothing because a build output was missing (packages/cli/dist, @objectstack/connector-mcp/dist, @objectstack/service-knowledge). They are counted green only after those were built and each returned a real OK; a refusal is not a pass. (2) check:type-check-debt --re-measure (616s, '32 ledger entr(ies) re-measured ... none above its recorded number') was run at 6f20d5ddf, ONE COMMIT EARLIER, and is not re-run at 6982d4550 - the only delta between them is a .changeset/*.md, which cannot move a tsc ledger. Stated rather than glossed. PRE-FIX REPRODUCTION was end-to-end over a real spawned CLI: build --json warnings [] / validate --json warnings carrying the same finding as a string / text-mode build printing it. The zero-hit grep is evidence because the same instrument produced positives: 'specVersionGap' (independently known present) hit 1 in that same JSON file, and the term under test hit 1 in both the text log and validate's payload.", "checks_run": { "lint": "green (whole repo)", "typecheck": "green (@objectstack/cli)", "unit_pin": "green 6/6", "derived_gate_families": "22 run, all green at 6982d4550", "refusals_resolved": 3, "ci": "in_progress - not waited on, per the dispatch contract" }, "open_questions": [], "out_of_scope_findings": [ "filed as #11727: `os build --json` also drops the capability-provider (#3366) and package-docs (ADR-0046) warnings that `os validate --json` carries - same `!flags.json` guard, two lists over; measured end to end (build warnings [] vs validate carrying the typo'd-capability record). Unassigned, unlabelled, ungraded." ], "notes_for_pm": "1) SCOPE HELD: compile.ts only, and only the undeclared-key list. The eight truncation sites are untouched and still belong to #11642; the diff is 3 files and keeps #11642's re-measurement on the merged ref cheap. 2) A second candidate finding was CHECKED AND DISMISSED rather than filed: lintUnknownStackKeys() can now only ever return [] at both CLI call sites, because the stack root went strict. That is designed behaviour, not dead code - the function's own docblock says it must go quiet if the schema is made strict, so as not to become a second, possibly disagreeing voice over a parse that already rejects loudly. Verified with a positive control: the same call against a strip-mode schema of the same shape DOES report. Filing it would have been noise. 3) WORDING CONFLICT, flagged not silently resolved: the dispatch said to file out-of-scope findings as `finding`-labelled issues; .claude/agents/os-dev.md says the `finding` label is for observation-class items (dead code, drift, polish) and that a concrete defect goes UNLABELLED for triage to grade. #11727 is a measured concrete defect, so it was filed unassigned and unlabelled per the agent file. Relabel if you prefer the other reading. 4) No `skip-changeset` label: this PR ships a changeset, so that mechanism does not apply. 5) PR is DRAFT, auto-merge NOT enabled, not marked ready - arming is yours." }Generated by Claude Code
fix(cli): carry the capability-provider and package-docs warnings in …
feat(cli): carry the computed advisory lists on every `os build --jso…
feat(cli): carry the computed advisory lists on every `os build --jso…
fix(cli): carry the capability-provider and package-docs warnings in …
fix(cli): fill the declared `warnings` key instead of adding a payloa…
fix(objectql,cli): a navigation contribution relocated past a missing…
fix(cli)!: report the named export the config default export already …
docs(cli): re-anchor the dead tracker citations in packages/cli's fil…