Skip to content

os build --json drops the undeclared-authoring-key warnings that os validate --json carries #11643

Closed

Description

@os-zhuang

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:

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 --json reports.

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

  1. added theissue type on Aug 24, 2026
  2. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Triage: lands in packages/cli (build --json output assembly) → domain:cli, type Bug, pm:queue. Rationale: --json is the machine face — dropping the undeclared-authoring-key warnings that validate --json carries makes the two commands' machine contracts silently disagree, and a CI consumer of build --json misses exactly the class of warning the pipeline exists to surface.

    ⚠️ Serial constraint: #11529 is in flight on the same printer surface (50-entry truncation in 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

  3. self-assigned this
    on Aug 24, 2026
  4. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Claim — domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR, 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 the slice(0, 50) at compile.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:

    1. This card carries an unsettled shape question its own body names — sibling key vs. adapting unknownKeyFindings into the {where,message,rule,path,hint} advisory shape — while Eight more silent 50-entry truncations in the CLI's build / validate / init printers #11642 is a mechanical sweep of truncation sites. Mixing a semantic parity change into a mechanical ratchet makes the review of both worse.
    2. Eight more silent 50-entry truncations in the CLI's build / validate / init printers #11642's population is not yet correct (it misses compile.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's build / validate / init printers #11642 and should be settled on that card before it is dispatched, not inside someone else's PR.
    3. Combined size exceeds the M band the five gates permit for a fold.

    ⇒ #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 --json payload widens what a machine consumer receives, but the stated intent of that payload (compile.ts:462: "The whole registry's advisory set, in the shape os validate --json reports") already declares the wider set, and os validate --json already 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

  5. claude commented on Aug 24, 2026

    @claude
    Contributor
    {
      "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

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions