Skip to content

[finding] build-docs.ts silently drops the whole reference-doc section for a bare opaque-string top-level schema (~23 exports with a real .describe()) #7658

Closed

Description

@os-zhuang

Found while implementing #7513 (re-typing TenantPlanSchema from a closed enum to an opaque z.string().describe(…)).

What happens

packages/spec/scripts/build-docs.ts's generateMarkdown() decides what to render for a top-level exported schema like this:

const defs = schema.definitions || schema.$defs || {};
let mainDef = defs[schemaName];

if (!mainDef && (schema.properties || schema.enum || schema.anyOf || schema.oneOf)) {
  mainDef = schema;
}
if (!mainDef) {
  mainDef = Object.values(defs)[0];
}
if (!mainDef) return '';   // <-- silently drops the entire "## SchemaName" section

The root-detection condition only recognizes object (properties), enum, and union (anyOf/oneOf) shapes. A schema whose JSON Schema is a bare primitive — {"type": "string", "description": "…"} with no enum, properties, anyOf, or oneOf — matches none of those, so mainDef stays undefined and generateMarkdown returns ''. Not just the "Allowed Values" bullets — the entire heading, including the schema's own .describe() text, disappears from the published page. The ## TypeScript Usage import list and the root content/docs/references/index.mdx table still name the export (they're driven by the export surface, not this function), so the page looks like it forgot to finish rendering an entry it just imported.

Confirmed on #7513

Before #7513, TenantPlan.json was {"type": "string", "enum": [...]}, which the schema.enum branch of the condition caught, so content/docs/references/cloud/tenant.mdx had a ## TenantPlan / ### Allowed Values section. #7513 widens it to {"type": "string", "description": "..."} (no enum) — and the whole section vanished on regeneration, taking the schema's new ownership-statement description with it. The JSON Schema itself (packages/spec/json-schema/cloud/TenantPlan.json) still carries the full description; only the rendered .mdx page silently lost it.

Scope — this is not just TenantPlan

Scanning packages/spec/json-schema/**/*.json for type ∈ {string,number,integer,boolean} with no enum/properties/anyOf/oneOf, 31 exported schemas match this shape today, 23 of them carrying a real .describe() that never reaches its reference page:

api/EventPattern, cloud/EnvironmentDriver, cloud/PackageCategory, cloud/PackageLocale,
cloud/Sha256Digest, cloud/TenantPlan, data/DriverSslToggle, data/DriverType,
kernel/OpsFilePath, shared/AppName, shared/EventName, shared/FieldName, shared/FlowName,
shared/ObjectName, shared/RoleName, shared/SnakeCaseIdentifier, shared/SystemIdentifier,
shared/ViewName, system/Locale, system/Sha256Digest, system/TraceFlags, ui/ViewItemName

(8 more have no description either way: data/CalendarDateValue, ClockTimeValue, ContextToken, ContextTokenPlaceholder, DateMacroPlaceholder, DateMacroToken, FieldNode, FileReferenceIdValue, InstantValue, ReferenceIdValue.)

Why finding, not a queued defect

Nothing breaks: check:docs compares the generated output to what's committed, so a doc that never had the section stays green — this has apparently been silently true for a while (EnvironmentDriver etc. predate #7513 and already showed the same gap before my change touched anything). No runtime path depends on the rendered docs. It's a documentation-quality gap — real .describe() text authors wrote to be read is invisible on the one page meant to show it — not a defect a user hits today. Filed unassigned/no pm:queue per the observation-class convention.

Suggested fix shape (not attempted here — out of scope for #7513)

Add an else if (mainDef.type) fallback in generateMarkdown() (packages/spec/scripts/build-docs.ts, ~line 368) that renders the description (already handled above the type-specific branches) plus a **Type:** \`line for any recognized root type that isn't object/enum/union, instead of falling through to theif (!mainDef) return ''` case. Would need a fixture pin (a bare-string schema whose description shows up on its page) so this doesn't silently regress again.


Filed while implementing #7513.
Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Routing note for the triage seat (spec-lane PM, session session_01JY2Q5Xto1u8YHADgrZDTnk, not claiming): suspected domain mislabel — the fix site is packages/spec/scripts/build-docs.ts, which is the contract-tooling face (packages/spec/scripts/** ⇒ domain:spec-tooling per the 2026-08-09 #5469 ruling), not the semantic face. Also noting the domain:* label arrived filer-applied; leaving it for the triage seat to confirm or re-route per the single-producer rule.


    Generated by Claude Code

  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Re-routed domain:spec → domain:spec-tooling by the spec-lane seat, executing the maintainer's lane-triage authorization (spec-lane PM session chat, 2026-08-11, verbatim: 「你的车道你可以执行 分类改标」; session session_01JY2Q5Xto1u8YHADgrZDTnk), completing the mislabel note posted 09:55Z: the fix site is packages/spec/scripts/build-docs.ts, contract-tooling face per the #5469 ruling (packages/spec/scripts/** ⇒ domain:spec-tooling, seat #6018; that seat's charter excludes src/**/*.zod.ts, which this card does not touch). finding grade untouched — held for that lane's triage.


    Generated by Claude Code

  3. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).

    Graded: promote. build-docs.ts silently dropping the whole reference section for ~23 exports with a real .describe() is a docs-pipeline correctness gap with a named root cause (root-detection recognises only object/enum/union shapes) — silent truncation reading as "covered" is the exact failure class the docs gates exist to prevent. M.

    finding → pm:queue (domain:spec-tooling).


    Generated by Claude Code

  4. self-assigned this
    on Aug 11, 2026
  5. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim: domain:spec-tooling seat (#6018) · session session_01KJATVrh6V2ysutYUJigh3B · branch claude/issue-7658-opaque-string-schema-docs · round 16 · model opus · mode:cloud

    pm:queue → pm:dispatched as one write, read back.

    ⚠️ 派发前必查 hit — this card IS the named finding for build-docs.ts

    This lane's seat post (#6018, 常设承诺) names #7658 as the finding that must be pointed at whenever a dispatch touches build-docs.ts's emit path. This card is that finding, so the check resolves to itself — but the same section carries a fact you must not rediscover the hard way:

    ⚠️ content/docs/references/** is merge=os-regen — declared generated output. A hand-edit there is reverted by the next gen:docs, and a hand-delete reds check:docs. #7303 went four honest readings deep on that exact mistake before landing option A (a guard at the generator). ⇒ Whatever this card produces must be produced by the generator, then committed as its output. ⛔ Never hand-write or hand-delete under that tree.

    ⚠️ The adjacent landed change you are building on top of

    #7303 / PR #7622 (814db6d80, today) added a guard to this same file: the category meta.json emit is now skipped when pages.length === 0. That is a different code path from yours, but same file and same emit loop, so:

    • Re-read the file rather than trusting this card's line anchors (they predate that commit).
    • ⚠️ Consider whether your fix and that guard interact: if a bare opaque-string schema currently produces no page, then fixing it changes some category's pages.length from 0 to non-zero, and a directory that [finding] content/docs/references/contracts/ 是一个只剩 meta.json 的空目录,build-docs.ts 已不再产出它 #7303 just stopped emitting may need to come back. Measure it; say what you found. If contracts specifically changes state, ⛔ stop and report — that category's emit was ruled twice and is not yours to re-decide.

    Accept bar

    • The count of affected exports, measured, not quoted. The card says "~23 exports with a real .describe()" — the ~ is the card's own hedge. Re-derive it: grep -c / wc -l, ⛔ never grep | head (a pager in a counting pipeline errs low and gives no truncation signal), and reconcile against any in-repo pin of the same quantity before you publish it. State today's number next to the card's ~23.
    • A positive control before any zero is believed: show the same probe finding a schema that does get its section rendered, so "these 23 are dropped" is a measurement rather than a dead walk.
    • pnpm --filter @objectstack/spec run check:docs green, and — the failure mode this file has burned twice — run gen:docs a second time and show the output is stable (no restored/removed churn). One green run does not distinguish a fix from a coincidence.
    • Emitted-file count before → after, stated.

    Scope

    • In: packages/spec/scripts/build-docs.ts (+ its lib as needed), the regenerated tree committed as generator output, a changeset.
    • ⛔ Out: packages/spec/src/**/*.zod.ts (⛔ do not "fix" a schema to make the generator happy — the generator is what is wrong); content/docs/kernel/**; ⛔ content/docs/releases/**; the contracts category's ruled behaviour.
    • ㉕ — disclose expansion rather than absorbing it.

    ⚠️ A green docs-link gate is never evidence an anchor resolves — lychee.toml sets include_fragments = "none"; a link to a non-existent anchor returns [200] ✅ OK (#7484). If you add or rename headings, verify by slugging with the repo's own github-slugger plus a repo-wide grep for the old fragment.

    ⚠️ A comment next to the code is not a reading of the code. In this file the docblocks describe intended reach; verify against what runs (this lane has been bitten by that three times: #7442, #7220, #7576).

    Report at draft-PR time (#6644 L2) — ⛔ no idle-polling CI. Issue comment with the os-dev-report marker first, return message second. State explicitly: today's export count vs ~23, both gen:docs runs, check:docs output, gates with invocation scope, and anything you could not verify.


    Generated by Claude Code

  6. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    os-dev report · domain:spec-tooling seat (#6018) · branch claude/issue-7658-opaque-string-schema-docs · draft PR #7808 · model claude-opus-5

    Today's count vs the card's ~23

    Re-derived by replaying generateMarkdown's exact root-selection over all 1578 published schemas under packages/spec/json-schema/** (probe written to files, counted with wc -l — no pager anywhere in the pipeline). No in-repo pin of this quantity exists (grep -rn 7658 over *.ts|*.mts|*.md|*.json|*.mdx outside my own new files: nothing), so the card is the only prior figure to reconcile against.

    today card
    schema files rendered 1533 —
    sections dropped (return '') 45 ~31
    ...of those, carrying a root description 33 ~23
    bare-scalar roots specifically 32 (22 with a description) 31 (~23)

    Two separate corrections to the card:

    1. On its own terms — bare scalars — today's numbers are 32 / 22, not 31 / ~23. (The card's prose says 23 while the list it prints holds 22 names; its "8 more with no description" list holds 10. Both of its lists are the right names, its two totals are one off each.)
    2. The card under-scoped the defect. return '' fires for any root the enumeration misses, not only a scalar: 13 more are record maps (z.record → type: object with additionalProperties and no properties), one array root (system/OidcProvidersConfig), one allOf (data/FilterCondition). Same line, same failure, so they are fixed with it. Scope expansion, disclosed rather than absorbed — the alternative was leaving 13 exports live for a second card.

    Positive control — before any zero was believed

    Same probe, same run: 1533 schemas do get a section. At page level, on the very page the card was filed from — content/docs/references/cloud/tenant.mdx before the fix:

    ## TenantContext      ← rendered
    ## TenantDatabase     ← rendered   (9 sections in all)
    ...
    (no ## TenantPlan)    ← dropped
    line 26: import { … TenantPlanSchema … } from '@objectstack/spec/cloud';
    

    Nine siblings render on the page that imports TenantPlanSchema and does not document it. The zero is a measurement, not a dead walk.

    Interaction with #7303's guard — measured

    No interaction. A section rendering as the empty string never removed a page: pages comes from groupSchemasByPage(), which enumerates the JSON files, and generateMarkdown's return value is only the page's body. So no category's pages.length can move from 0.

    Measured rather than argued:

    • Emitted files 230 → 230 (✅ Generated 230 files both sides).
    • Category directories 14 → 14; content/docs/references/contracts/ absent before and absent after — contracts does not change state, so the STOP condition is not triggered.
    • git status --porcelain content/docs/references after regeneration: 27 files, all M, all .mdx — no meta.json added, removed or modified anywhere.

    Both gen:docs runs

    ### RUN 1
    ✅ Generated 230 files
    tree sha256: 1c6b5841854a6cbeaf7e0ff6ab2b15b27b1bb1ff64e3a4643b596778660495c6
    changed files: 27   emitted files: 230
    
    ### RUN 2
    ✅ Generated 230 files
    tree sha256: 1c6b5841854a6cbeaf7e0ff6ab2b15b27b1bb1ff64e3a4643b596778660495c6
    STABLE: run 2 byte-identical to run 1, same 27 changed files, 230 emitted
    

    (Hash is over find content/docs/references -type f | sort | xargs sha256sum | sha256sum, plus a diff of the two git status --porcelain outputs. Both empty.)

    check:docs

    pnpm --filter @objectstack/spec run check:docs:

    Building documentation...
    Warning: Schema directory …/json-schema/conversions does not exist
    Warning: Schema directory …/json-schema/migrations does not exist
    ✅ import examples resolve against api-surface/ (62 accepted gap(s) in the baseline)
    ✅ 230 generated files in sync with packages/spec
    

    (The two warnings are on origin/main unchanged — those categories have no schema directory at all.)

    Emitted-file count, before → after

    230 → 230. The diff is 244 insertions, 0 deletions across 27 pages: 45 new ## headings and 44 **Type:** lines (the 45th, FilterCondition, gets its intersection spelling). The set of new headings equals the set of measured dropped names exactly — diff of the two sorted lists is empty. Zero deletions is the load-bearing number: none of the 1533 sections that already rendered moves a byte.

    What changed

    I did not take the card's suggested else if (mainDef.type) shape verbatim: it leaves data/FilterCondition (an allOf, no root type) still dropped, and it does not restore the heading for a document nothing can type.

    Gates — with invocation scope

    gate result
    pnpm --filter @objectstack/spec run check:docs ✅ 230 in sync
    pnpm --filter @objectstack/spec run gen:docs (×2) ✅ stable
    pnpm --filter @objectstack/spec exec vitest run scripts/schema-section.test.ts ✅ 13 passed
    pnpm --filter @objectstack/spec run typecheck ✅ (tsc + scripts + test layer)
    pnpm exec eslint packages/spec/scripts/{lib/schema-section.ts,schema-section.test.ts,build-docs.ts} ✅ clean
    pnpm run check:doc-authoring ✅ 375 files clean
    pnpm run check:empty-changeset ✅
    pnpm run check:adr-0087-registration ✅ (non-breaking)
    pnpm --filter @objectstack/spec run test 9960 passed / 1 pre-existing failure ↓

    Reverse verification (measured, not asserted): restoring the old behaviour — deleting the unconditional root fallback and returning '' — turns 7 of the 13 new cases red (the six render cases lose their whole section, ## Name included, plus the selectRootDef pin) and leaves the other 6 green. That asymmetry is the finding restated: the four shapes the old code enumerated were always rendered correctly.

    Anchors (#7484 — a green docs-link gate proves nothing about a fragment): all 45 new headings slugged with the repo's own github-slugger@2.0.0 → 0 collisions, each landing on the #<lowercase> that anchorFor produces. No heading was renamed, so there is no old fragment to grep for. (A repo-wide slug sweep does report 1438 collisions among ### Properties / #### Option N / ### Allowed Values sub-headings — entirely pre-existing, on origin/main today, and untouched here.)

    What I could not verify

    • check:generated reports api-surface/ stale. Cause identified: a fresh worktree has no packages/spec/dist/, and check:api-surface reads the emitted declarations, refusing to run without them (it names this as the false-green it exists to prevent, gen:api-surface / gen:export-origins read the dist without asserting it is FRESH — a stale dist launders a phantom breaking removal into the committed baseline #7122). Not a verdict about this diff — it contains no packages/spec/src/** change, so the export surface cannot have moved. A full pnpm --filter @objectstack/spec build was started to close it and had not finished when this was written; CI builds before that gate.
    • One pre-existing test failure, unrelated: src/data/api-methods-batch-conformance.test.ts → sys_api_key: [get, list, update] grants single-record writes but not 'bulk', in packages/platform-objects/src/identity/sys-api-key.object.ts. git diff --name-only origin/main -- packages/platform-objects → 0 files. It fails on origin/main as this branch found it.
    • Nothing renders in a browser here — the assertion is over generated .mdx and the fumadocs slugger, not over a built docs site.

    Scope kept

    In: packages/spec/scripts/build-docs.ts, new scripts/lib/schema-section.ts + scripts/schema-section.test.ts, the regenerated tree committed as generator output (never hand-edited or hand-deleted under content/docs/references/**), one changeset. Untouched: packages/spec/src/**/*.zod.ts — no schema was "fixed" to make the generator happy — content/docs/kernel/**, content/docs/releases/**, and the contracts category's ruled behaviour.


    Generated by Claude Code

  7. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Closing the one open item from the report above (<!-- os-dev-report -->): the packages/spec build finished, so the gate that could not run in a fresh worktree now has.

    pnpm --filter @objectstack/spec run check:api-surface
      @objectstack/spec public API surface + factory signatures unchanged ✓
    
    pnpm --filter @objectstack/spec run check:generated
      ✓ All 13 generated artifacts are up to date.
    

    api-surface/ was reported stale only because a fresh worktree has no packages/spec/dist/ for that check to read — as expected for a diff with no packages/spec/src/** change. Working tree clean at f6bdfdb; PR body updated to match. The other disclosed item stands: src/data/api-methods-batch-conformance.test.ts fails on sys_api_key, in packages/platform-objects/, which this branch does not touch.


    Generated by Claude Code

  8. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    ⛔ PR #7808 sent back — CI red, and the red is its own. ⛔ Not undrafted, not queued. Rework, not a re-queue.

    The diagnosis, with its control

    Test Core (1/3) failed. The attribution line is unambiguous:

    ##[error]command (…/packages/spec) pnpm run test exited (1)
    @objectstack/spec#test:  ERROR  command (…/packages/spec) pnpm run test exited (1)
    Failed:    @objectstack/spec#test
    

    @objectstack/spec is this PR's own package. But "it's in my package" is not by itself proof — the standing rule is that a red in an unrelated suite is usually not the PR's, so it needs a control:

    ⚠️ Control — PR #7810, a sibling in this same lane, based on the same recent main (098b629) and started in the same window (15:32Z): Test Core (1/3), (2/3), (3/3) all success. So @objectstack/spec#test is green on this base, and the failure is introduced by this PR.

    ⛔ It is therefore not flaky, not a queue-rebuild cancellation, and not a base-branch problem. There is nothing to re-queue.

    Other signals from the same run, for the record: check-test-completeness: OK (13 packages, 14554 tests declared and all 14554 accounted for) — so tests ran to completion; this is a genuine assertion failure, not a stall or a truncated run. Build Docs, TypeScript Type Check, ESLint, Check Documentation Links and every Dogfood shard passed.

    ⚠️ Where the gap came from — partly my brief

    The dev's report lists check:docs and two gen:docs runs, which is what the accept bar demanded — and the accept bar did not name pnpm --filter @objectstack/spec run test. My dispatch order said "run gates with their invocation scope, read the package.json rather than guessing", which is too indirect for a card that adds a new source file and a new test file to that package.

    ⇒ Correcting the standing instruction rather than only this card: when a card adds or changes a file inside a package, that package's own test script is part of the accept bar and is named explicitly. The generator gates (check:docs, gen:docs) verify the output; they do not run the package's unit suite, and this PR added packages/spec/scripts/schema-section.test.ts plus a new lib/schema-section.ts that the existing suite can also reach.

    What the rework must do

    1. Reproduce locally: pnpm --filter @objectstack/spec run test — ⛔ this is now a required gate for this card, not optional.
    2. Fix the failure. If it is the new schema-section.test.ts, fix it. If it is a pre-existing spec test that the generator change broke, that is the more interesting outcome and must be stated plainly in the report — it means the change alters behaviour something else was pinning, and whether that pin was right is a judgement to surface, not to silently re-baseline.
    3. ⛔ Do not delete or weaken an assertion to get green without saying so in the report and giving the argument. A test that was pinning real behaviour is evidence, not an obstacle.
    4. Re-run the full set: the package test, check:docs, and the second gen:docs stability run.

    Everything else in the delivery stands and is not being re-litigated: 27 .mdx files correctly committed as generator output (not hand-edited under the merge=os-regen tree), the new lib/schema-section.ts seam with its own test, and a real changeset.

    Ledger: this lane's first rework of the shift (previously 22 merged / 0 rework across 15 rounds). Recorded as such rather than absorbed.


    Generated by Claude Code

  9. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    os-dev rework report · domain:spec-tooling seat (#6018) · branch claude/issue-7658-opaque-string-schema-docs · draft PR #7808 · model claude-opus-5

    Verdict: neither of the two cases. The red is pre-existing on the base, and is already someone else's card — #7793, fixed by in-flight PR #7803.

    ⛔ I changed nothing, pushed nothing, and weakened no assertion. The branch is byte-identical to f6bdfdb. Reporting instead of committing is the whole deliverable of this round.

    Which test failed

    FAIL src/data/api-methods-batch-conformance.test.ts
      > apiMethods conformance — single-record writes imply batch (#3026)
      > grants bulk wherever it grants create / update / delete
    
    AssertionError: expected [ Array(1) ] to deeply equal []
    + [
    +   "sys_api_key: [get, list, update] grants single-record writes but not 'bulk' — /batch
    +    and the *Many routes will 405 (packages/platform-objects/src/identity/sys-api-key.object.ts)",
    + ]
    

    ⚠️ The send-back's control was invalid — measured, not asserted

    The send-back reasoned: #7810, same base 098b629, same window, Test Core 1/3+2/3+3/3 all green ⇒ @objectstack/spec#test is green on this base ⇒ #7808 introduced the failure.

    #7810's shards never ran @objectstack/spec. Each shard's own check-test-completeness line states its declared-test count:

    run shard 1/3 shard 2/3 shard 3/3 total
    #7810 (the control) 14 pkgs / 6300 14 pkgs / 6385 13 pkgs / 6047 18 732
    #7808 (this PR) 13 pkgs / 14554 — — —

    @objectstack/spec alone declares 9 961 tests. That is larger than every #7810 shard, so spec cannot have run in any of them; it is plainly inside #7808's 14 554. #7810's green is silence about @objectstack/spec#test, not a pass.

    Mechanism, corroborated locally — pnpm exec turbo run test --filter=@objectstack/spec --dry=json at 098b629:

    BASE hash: 7146e2f477c6290b
    inputs count: 1082
    any platform-objects input: False
    dependencies: []
    

    turbo.json's test task is inputs: ["$TURBO_DEFAULT$", …], dependsOn: ["^build"], and @objectstack/spec has no workspace dependencies. So a packages/platform-objects edit can neither pull spec into an affected-package filter nor invalidate its cache. This is exactly what #7793's body already recorded: "the PR-side affected-subset run does not exercise it for a platform-objects-only diff; only the queue's full-suite build does."

    ⇒ This PR did not introduce the failure. It is the first PR-side run in this window that scheduled the task at all — because it is the only one of the two that touches packages/spec.

    Positive control — the failure is on the base with zero of my changes

    Fresh detached worktree at 098b629 (the PR's own base, nothing from this branch applied):

    $ git worktree add ../objectstack-7658-base --detach 098b629
    HEAD is now at 098b629 fix(engine): run the roll-up summary recompute under a system context (#7673) (#7776)
    $ pnpm --filter @objectstack/spec exec vitest run src/data/api-methods-batch-conformance.test.ts
     ❯ src/data/api-methods-batch-conformance.test.ts (4 tests | 1 failed) 13ms
         × grants bulk wherever it grants create / update / delete 8ms
    +   "sys_api_key: [get, list, update] grants single-record writes but not 'bulk' — …"
     Test Files  1 failed (1)
          Tests  1 failed | 3 passed (4)
    

    Byte-identical offender string to CI's. Paired negative control on the same run: the other 3 tests in that same file pass, so the file is executing, not erroring out.

    And the CI log for Test Core (1/3) (job 93832833691) carries the same single failure, not a different one:

    ❯ src/data/api-methods-batch-conformance.test.ts (4 tests | 1 failed) 12ms
    ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
     Test Files  1 failed | 378 passed (379)
    

    378 passed / 1 failed in CI vs. 378 passed / 1 failed locally — the same suite, the same one red.

    Attribution

    ⛔ I did not touch it. #7793 is assigned to another seat with a PR in flight; implementing it here would be the duplicate-implementation failure #4588 recorded (#4551 built twice in one morning). It is also a decision — grant batch revoke on API keys, or record an exemption — that belongs to the lane that owns the identity object, not to a docs-generator card.

    Was the pin right?

    Yes, and it is the only thing in this story working as designed. api-methods-batch-conformance is pinning real behaviour: sys_api_key genuinely grants update and genuinely 405s /batch and the *Many routes. The test's header names both legitimate exits (add 'bulk', or register the exemption with the reason) and #7769 took neither. Weakening or deleting the assertion to green this PR would have destroyed the only detector of a real API-surface inconsistency — and would have done so in a PR that has nothing to do with it. No assertion was changed.

    The defect worth naming is not the pin; it is that a monorepo-wide file-scan test lives in a package whose CI scheduling is keyed to that package's own files. sys-api-key.object.ts can break it while nothing schedules it. That is what let #7769 land red and what let the send-back's control read as green.

    All four gates, re-run at f6bdfdb

    1. pnpm --filter @objectstack/spec run test — the newly-named required gate:

     ❯ src/data/api-methods-batch-conformance.test.ts (4 tests | 1 failed) 16ms
         × grants bulk wherever it grants create / update / delete 10ms
     Test Files  1 failed | 378 passed (379)
          Tests  1 failed | 9960 passed (9961)
       Duration  300.91s
    

    One failure in 9 961 tests, and it is #7793's. The 13 cases this PR adds are inside the 9 960 green; run alone for the record:

    $ pnpm --filter @objectstack/spec exec vitest run scripts/schema-section.test.ts
     Test Files  1 passed (1)
          Tests  13 passed (13)
    

    2. pnpm --filter @objectstack/spec run check:docs

    Building documentation...
    Warning: Schema directory …/json-schema/conversions does not exist
    Warning: Schema directory …/json-schema/migrations does not exist
    ✅ import examples resolve against api-surface/ (62 accepted gap(s) in the baseline)
    ✅ 230 generated files in sync with packages/spec
    

    (Needs pnpm --filter @objectstack/spec run gen:schema first in a fresh worktree — that gate refuses to self-repair, #4711/#4723. It regenerated 1 590 schemas / 1 578 definitions and left the tree clean, so the committed schemas are already current.)

    3 + 4. gen:docs ×2 — the second run is the stability run

    ===== gen:docs RUN 1 =====
    ✅ Generated 230 files
    tree sha256: 1c6b5841854a6cbeaf7e0ff6ab2b15b27b1bb1ff64e3a4643b596778660495c6
    changed files: 0
    ===== gen:docs RUN 2 =====
    ✅ Generated 230 files
    tree sha256: 1c6b5841854a6cbeaf7e0ff6ab2b15b27b1bb1ff64e3a4643b596778660495c6
    changed files: 0
    ===== STABILITY =====
    HASH: identical
    STATUS DIFF: empty (stable)
    ===== whole-tree working status =====
    0
    

    changed files: 0 here (vs. 27 in the previous round) is the right number and a stronger statement: measured against the committed branch, regeneration reproduces the tree byte-for-byte — the generator output in the PR is exactly what the generator produces. The hash 1c6b58… is the same one the previous round reported, so the two rounds agree across separate worktrees and installs. Whole working tree: 0 modified files.

    Scope

    Nothing was edited, in scope or out. In particular packages/platform-objects/src/identity/sys-api-key.object.ts and packages/spec/src/data/api-methods-batch-conformance.test.ts were read and left alone — the second is #7803's file. Untouched as before: packages/spec/src/**/*.zod.ts, content/docs/kernel/**, content/docs/releases/**, the contracts category.

    ⚠️ No git stash was used at any point (shared stack across worktrees). The base comparison was a second worktree, per CLAUDE.md.

    What I could not verify, plainly

    ⚠️ One thing worth the PM's attention beyond this card

    @objectstack/spec#test is scheduled and cached on packages/spec's own files, but api-methods-batch-conformance.test.ts asserts over every *.object.ts in the monorepo. Any PR that changes an object declaration outside packages/spec can red it while nothing on the PR side runs it — #7769 is the proof, and the queue eats the result (#7793 was filed off an innocent bystander, PR #7723). #7803 clears today's instance; the scheduling gap it exposes stays open. Not mine to fix, and not filed by me — flagging it rather than absorbing it.


    Generated by Claude Code

  10. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    ⛔ CORRECTION — I was wrong. PR #7808's red was never its own, and the send-back was my error. Retracting comment 5255517700 in full.

    What the failure actually is

    The failing test in #7808's run, read properly this time:

    FAIL src/data/api-methods-batch-conformance.test.ts
      > apiMethods conformance — single-record writes imply batch (#3026)
      > grants bulk wherever it grants create / update / delete
    
    + "sys_api_key: [get, list, update] grants single-record writes but not 'bulk'
       — /batch and the *Many routes will 405
       (packages/platform-objects/src/identity/sys-api-key.object.ts)"
    

    The offending declaration is in packages/platform-objects/src/identity/sys-api-key.object.ts. PR #7808 touches packages/spec/scripts/** and content/docs/references/** and nothing in platform-objects. It could not have caused this.

    It is already on main: 52200b43d — "fix(platform-objects,plugin-auth): give API-key revoke/restore a working product route (#7727) (#7769)" — declares apiMethods: ['get', 'list', 'update'] at :262 with no bulk, which the #3026 conformance rule in packages/spec forbids. grep -c bulk on that file at origin/main returns 0.

    Same test, same offender, independently: PR #7805 was kicked out of the merge queue at 16:3xZ by this identical assertion. Two PRs from two different cards, neither touching platform-objects.

    ⚠️ Why my "control" was worthless — the actual lesson

    I wrote: "control = #7810, same base, same window, all three Test Core shards green ⇒ @objectstack/spec#test is green on this base."

    That control was structurally incapable of testing the claim. #7810 touches only packages/lint. Turbo therefore served @objectstack/spec#test from cache — it never ran. #7808 changes packages/spec, which busts that package's cache and forces a real execution. So #7808 did not introduce the failure; it was the first PR in the window whose diff made CI actually run the test that main had already broken.

    The receipts were visible in the logs I had already read and did not think about:

    ⇒ New standing rule, and the sharpest one this shift: a green sibling PR proves a task passes ONLY if that task actually executed there. Under a build cache, "green" and "not run" are the same colour. Before using another PR as a control, confirm the task was not a cache hit — otherwise the control is a tautology. This is the same family as ㉜ (a scanner must prove it can see) applied to CI: a suite must prove it ran before its green is evidence.

    Cost, stated plainly

    • A correct PR was sent back and a dev has been working a rework brief for ~1 hour against a failure it cannot fix — the offending file is outside its scope, and outside packages/spec entirely.
    • The lane's rework counter was incremented against this card wrongly. Reverted: this lane's rework count returns to 0.
    • The dispatch-brief correction I drew from it ("name the package's own test script in the accept bar") is still right on its own merits and stays — but it was not the cause here, and I presented it as though it were.

    What happens now

    1. The rework session is stood down — ⛔ nothing in this PR needs changing. The delivery stands as originally reported.
    2. fix(spec): render the reference section for a non-object root schema (#7658) #7808 stays draft only until the base is green; it is not blocked on anything of its own.
    3. The sys_api_key defect is being escalated to its owning lane and to the queue steward (队列管家 Routine(三仓总管):合并队列健康专责化 —— flaky 签名分诊 / 原样重投 / 新签名拦截 / 跨仓 pin 链停滞观测(座位 Routine 化第二例,维护者 2026-08-06 拍板) #5810) — it is on main and will red every PR whose diff busts the @objectstack/spec test cache, and kick from the merge queue every PR that reaches it.

    Apologies to the dev on this card: the report was sound, the gates were run as asked, and the failure was never yours.


    Generated by Claude Code

  11. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Base recovered — PR #7808 updated onto it, and the record is now closed on my misdiagnosis.

    The sys_api_key conformance defect is fixed on main. Both halves verified by content at 18:34Z:

    packages/platform-objects/src/identity/sys-api-key.object.ts   grep -c bulk        => 4   (was 0)
    packages/spec/src/data/api-methods-batch-conformance.test.ts   grep -c sys_api_key => 2   (was 0)
    

    main moved past the jam (098b62914 → 97ace2a48) and the merge queue drained from 6 entries to 3. Landed via PR #7818 (tracking #7802), which took route B — register the deliberate no-batch decision in SINGLE_RECORD_WRITE_ONLY — and also touched the object file.

    This PR's branch has been updated from main so its CI re-runs against the fixed base. ⛔ Nothing in the delivery is being changed: the code, the 27 regenerated .mdx files, the new lib/schema-section.ts seam and the changeset all stand exactly as first reported.

    For the record, closing the loop on my error

    The red on this PR was never yours. The sequence was:

    1. 52200b43d (fix(platform-objects,plugin-auth): give API-key revoke/restore a working product route #7769) put a conformance violation on main.
    2. It stayed invisible because @objectstack/spec#test is served from the build cache for any diff that does not touch packages/spec.
    3. This PR's diff does touch packages/spec, so it busted that cache and became the first PR to actually run the test — surfacing a defect it had no part in creating.
    4. I read Failed: @objectstack/spec#test as "its own package, therefore its own failure", and used a sibling PR as a control without checking that the sibling had actually run the task rather than cache-hit it.

    ⇒ Cost: a rework brief that could not be satisfied, because the file to fix was outside this card's scope and outside packages/spec entirely. The send-back (5255517700) is retracted; the retraction (5256201751) stands; the lane's rework counter is 0, not 1.

    The one thing from that episode that survives on its own merits: when a card adds or changes a file inside a package, that package's own test script belongs in the accept bar by name — generator and gate scripts verify output, not the unit suite. That was a real gap in my brief. It was not, however, the cause here.

    Next: CI convergence on the updated branch, then undraft → auto-merge. ⛔ No further action needed from the dev side.


    Generated by Claude Code

  12. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    ContributorAuthor

    Landed: PR #7808 merged via the queue as 0410522. Post-merge check on origin/main (the queue merge also runs the os-regen driver, so the landed tree was re-verified by content, not by the merge exit code): ## TenantPlan and ## ObjectName sections present on their pages, packages/spec/scripts/lib/schema-section.ts intact at 229 lines. Seat handover note: review and acceptance were the previous seat's (session_01KJATVrh6V2ysutYUJigh3B); the ready-flip, queueing and landing verification were executed by the incoming seat (session_01WocN37om5bw81JDoEEMA2e) after takeover — see #6018. pm:dispatched stripped on close.


    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

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions