Skip to content

fix(spec): JobSchema's own @example no longer opens with the retired id key - #19296

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-19184-job-example-retired-id
Sep 20, 2026
Merged

os-steve merged 2 commits into
mainfrom
claude/issue-19184-job-example-retired-id

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19184

Clause-②: no

JobSchema's TSDoc @example opened with id, the key removed in 17.0.0 (#4667, ADR-0049) and tombstoned a dozen lines below the block that wrote it. The repository's own example was therefore refused by the schema it documents on a verbatim copy. One line is deleted; the schema, the tombstone and its prescription are untouched.

The two readings the card marked NOT measured by the filer

Both were attributed rather than adopted on the card, and both were re-run here, first-hand, on this branch.

1. Parse side — confirmed. The example was extracted from the source by anchor (not retyped) and parsed:

JobSchema.safeParse(the block as written)
  success: false
  code: unrecognized_keys · keys: ["id"]
  message: Unrecognized key(s) on this job: `id`.
           • `job.id` was removed in @objectstack/spec 17.0.0 (ADR-0049) — nothing ever read it …
             `name` IS the job's identity everywhere … Delete the key; rename the job via `name` …

Control leg, same probe, same run: the identical object with id deleted answers success: true. So id was the only defect in that block — the cron expression string, timezone: "UTC", the handler string and both retryPolicy keys all parse — and deleting the line is the entire fix.

2. Blast radius — confirmed, three ways. The block is source TSDoc and reaches no generated page:

  • content/docs/references/system/job.mdx carries the .describe() text of this very file verbatim (positive control: the timezone description, matched byte for byte) and carries no part of the @example. Grep for Metadata Sync Job (Cron) and sync_metadata_nightly over content/docs/: zero hits, against a firing control (timeoutMs matches five reference pages).
  • pnpm --filter @objectstack/spec check:generated reports all 16 generated artifacts up to date after the edit — check:docs among them — so the change moves no generated byte.
  • It does not reach dist either: the distinctive phrase of the block has zero hits under packages/spec/dist/ after a full build, while the sibling defineJob example (a fenced ts block) is present in the emitted .d.ts. The block travels to consumers through the tarball's source file, not the declarations.

Why one block and not a family — re-derived here

The file's second @example, on defineJob, was extracted and tested by the same probe: it carries no id, no timeout and no retryDelayMs. It is untouched, as is JOB_ID_RETIRED and every other retirement mechanism. The package-wide @example question is a separate card and is deliberately not started here.

Worth recording, because it is why this survived: check:skill-examples type-checks prose examples across three surfaces, including spec source TSDoc — but its population is fenced code blocks (ts / tsx / typescript), 10 of them in spec sources. This @example is a bare object literal with no fence, so no instrument in the repo reads it. The fix is the example; the gap is noted, not widened.

The pin

JobSchema's own @example (#19184) in packages/spec/src/system/job.test.ts reads the block out of the source rather than restating it — a restated copy is a second declaration and rots green. It carries an extraction assertion (an unmatched anchor fails loudly instead of parsing an empty string) and a firing control: re-adding id must still be refused with unrecognized_keys naming id and a message carrying the version and the replacement identity. Without that leg a pin that accepts anything would read as green.

Changeset — measured, not assumed

patch on @objectstack/spec, and skip-changeset was measured wrong rather than skipped by convention:

  • packages/spec is public at 17.4.0 and its files list includes src/**/*.zod.ts.
  • npm pack --dry-run --json lists src/system/job.zod.ts among the 2042 files of the tarball. The corrected block is shipped content, so the diff publishes.
  • Clause-②: no is a measurement too: a full pnpm --filter @objectstack/spec build leaves every tracked generated artifact byte-identical (git status clean), and check:authorable-surface is green — the accept set does not move, because only a comment changed.

Verification

Run on cf3e8107b1, the final commit of this branch.

  • pnpm --filter @objectstack/spec exec vitest run --project local src/system/job.test.ts — 59 passed (55 before, 4 new).
  • pnpm --filter @objectstack/spec typecheck — exit 0; check:test-typecheck OK with the debt ledger unmoved (54 files / 259 errors / 144 pinned signatures, shrink-only).
  • pnpm --filter @objectstack/spec check:generated — 16 of 16 artifacts up to date.
  • pnpm --filter @objectstack/spec check:skill-examples — 258 prose examples type-check across 3 surfaces (first run refused for an unbuilt client-react/dist; re-run after building that package, which is what the refusal asked for).
  • Green, each exit code captured before any pipe: check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:spec-docblock-symbol-anchors, check:comment-mask-adoption, check:comment-mask-corpus, check:keyed-text-bounds, check:changeset-gate-self-tests, check-changeset-no-major --base origin/main, check-adr-0087-registration --base origin/main, check:published-files, check:doc-authoring, check:objectui-changeset, check:pm-changeset-deadline-census, and the spec source audits check:duration-unit-keys, check:yaml-examples, check:empty-state, check:exported-any, check:variant-docs, check:dual-source-exports, check:entry-nameability, check:browser-reachable-entries, check:llms-txt, check:objectui-pin-citations.
  • eslint over the two touched TypeScript files: 2 files judged, 0 errors, 0 warnings. The narrowing is declared: this config runs no type-aware rules (no parserOptions.project, stated in the config itself), so this diff cannot move the verdict on a file it does not touch; the repo-wide sweep stays CI's.

Declared narrowings, so the reviewer reads coverage rather than infers it: scripts/pm/dispatch-gates.mjs --ran reconciles 28 of 81 derived families as run here; the remaining 53 are repo-wide ratchets and whole-tree sweeps that CI owns. The clause-② level axis of check-changeset-no-major reports NOT APPLICABLE locally — it reads a pull_request payload, so this PR's own run is its first real reading. The branch is two commits behind main at the time of writing, and dispatch-gates flags its own derivation stale for ci.yml on that account.

Acceptance notes

Noted while measuring, not fixed here, not filed by this PR:

  • The same example's handler: "services/syncStatus.ts:syncAll" parses (the key is a plain string) but names something the runtime cannot resolve: AppPlugin resolves a job handler as a straight key lookup into bundle.functions, and a miss logs job handler not found in bundle.functions — skipping at warn and schedules nothing. The working spelling is a bare function key — handler: 'sweepProjectHealth' in examples/app-showcase, and the sibling defineJob example in this very file. Same block, different failure mode from the card's (accepted at parse, inert at boot), so it is reported rather than ridden in on this PR.
  • The handler line also carries a trailing space. Style only; left alone.

🤖 Generated with Claude Code


Generated by Claude Code

`JobSchema`'s TSDoc `@example` opened with `id`, removed in 17.0.0 and
tombstoned a dozen lines below the block that wrote it, so the example the
schema publishes was refused by that schema on a verbatim copy — measured:
`safeParse` answers `success: false` with `unrecognized_keys: ["id"]` and the
`JOB_ID_RETIRED` prescription as the message. Deleting the line is the whole
fix: the same object with `id` removed parses.

The pin reads the block out of the source instead of restating it, and carries
a firing control (re-adding `id` must still be refused, with the prescription)
so it cannot pass vacuously.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
Measured rather than assumed: `packages/spec` publishes `src/**/*.zod.ts` in
its `files`, and `npm pack --dry-run` lists `src/system/job.zod.ts` in the
tarball, so the corrected block is shipped content. The generated artifacts do
not move — `pnpm --filter @objectstack/spec build` leaves the tracked
authorable-surface and JSON-Schema output byte-identical — so the accept set is
unchanged and the declaration is `Clause-②: no`.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

What this run could not see

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 4efb988a941056d6099b680924fa41807292115d → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 4efb988a941056d6099b680924fa41807292115d

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 20, 2026

os-steve commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 80/80 CONTRACT_REVIEW_TIER
Head-sha: cf3e8107b12b8e4a34a5c635546be756135d165a

Isolated at-tier review of PR #19296 (card #19184), read off the PR body, diff, commits and check runs, the card body, the os-dev-report comment 5749227974, and a detached worktree at the head above. The dispatching seat's conclusions were not read as inputs. Served-tier reading: this reviewer's transcript (subagents/agent-a6a1e20fc62a4e478.jsonl) carries 80 assistant requests, 80 of them with the harness per-request model stamp equal to the value CONTRACT_REVIEW_TIER binds at scripts/pm/dispatch-gates.mjs:11903; the identifier itself is deliberately not spelled in this comment.

① Derived judgments

Diff vs merge-base e6a03e6491: 3 files, +108/−1 — packages/spec/src/system/job.zod.ts (−1, one TSDoc line), packages/spec/src/system/job.test.ts (+77), .changeset/job-example-retired-id.md (+31).

  1. Accept set: unchanged — correct. The deleted line is inside a comment; JobSchema (job.zod.ts:181-209) is untouched, still strictObject with guidance: { id: JOB_ID_RETIRED }. Re-measured at the head: JobSchema.safeParse of the block as now written ⇒ accepted; the same object with id: "job_sync_meta" re-added ⇒ unrecognized_keys naming id with the 17.0.0 prescription (pin test 4, and mutation M1 below). check-widening-tells --declaration no over the real diff: 1 contract file judged, no tell; 2 NOT MEASURED by design (a test and a changeset).

  2. Public surface: unchanged — correct. No export moves. The block never reaches dist/*.d.ts, and the reason is structural, not incidental: the "Job Schema" docblock (:130-148) is immediately followed by a second docblock (:149-158) for the non-exported const JOB_ID_RETIRED, so TypeScript attaches the example to that const, and export const JobSchema carries no TSDoc at all. Verified in the published 17.4.0 tarball: grep -rl "Metadata Sync Job" package/dist ⇒ zero files, while the fenced defineJob example IS present in dist/translation.zod-*.d.ts (positive control). Pre-existing and outside this card — carried under ③.

  3. Generated artifacts and docs: nothing derives from this block — correct. packages/spec/scripts/build-docs.ts has no @example handling; lib/file-description.ts reads module-header docblocks only (this is not the file's first docblock); check-skill-examples.ts populates from FENCED ts/tsx blocks (this block is a bare literal, no fence); check-yaml-examples.ts declares TSDoc @example roots out of scope. content/docs/**, packages/spec/json-schema/**, api-surface*/**, liveness/**, prompts/**: zero hits for Metadata Sync Job, sync_metadata_nightly, job_sync_meta. CI on this head: TypeScript Type Check (the required job carrying check:generated and check:docs) ⇒ success.

  4. Clause-②: no — stands. Deleting a key from an example cannot widen, and no derivation exists that a comment could widen through (item 3). This is not a deletion of a rejection, so no Contract-text: is owed; for completeness, the published contract text the example is brought into line with is the shipped tombstone (src/system/job.zod.ts in the 17.4.0 tarball): 「job.id was removed in @objectstack/spec 17.0.0 (ADR-0049) — nothing ever read it … Delete the key; rename the job via name」.

  5. Scope — re-derived; the card holds. job.zod.ts at the head carries exactly two @example tags (:134, :218). Only the first carried id; the defineJob block (:218-225) is name/schedule/handler only, no retired key. Across packages/spec/src (203 .zod.ts files, 358 @example blocks) 11 other blocks carry an id: line on other schemas — not judged here: that is [finding] odata.zod.ts 的两个 @example 块随 tarball 与参考页发运,却在 check:skill-examples 的可达范围之外 —— packages/spec/src 下 134 个写 @example 的文件里,只有 6 个带标记 #19065's sweep, and this PR correctly does not start it.

  6. The pin reads the source and cannot pass vacuously — verified by mutation. job.test.ts reads ./job.zod.ts via readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), './job.zod.ts')) — same package, so no cross-package input; vitest.repo-tests.json does not list it, so it runs in the local project CI executes. At the head: 59/59. Seven mutations of the source in the worktree, each restored to a clean tree afterwards:

    • M1 re-add id: "job_sync_meta" ⇒ 2 fail (parses as written, with the unrecognized_keys issue list as the message; opens with name).
    • M2 change the anchor caption ⇒ 4 fail, the first on toContain(ANCHOR) — no empty-string pass.
    • M3 append prose inside the docblock after the block ⇒ 4 fail, SyntaxError: Unexpected identifier 'See' — loud.
    • M4 shrink to the invalid { name: "x" } ⇒ 1 fail (invalid_type on the required keys).
    • M5 break the closing */ gutter ⇒ 4 fail on extraction.
    • M6 replace with a minimal VALID job ⇒ passes. This is the pin's floor, stated so nobody reads it as a hole: the pin holds "the block parses as a job and opens with name", not "the block is this text" — the right invariant for this card.
    • M7 make JobSchema non-strict (drop strictObject and the guidance map) ⇒ the firing control fails (expected true to be false): the instrument detects its own dulling.

    pnpm --filter @objectstack/spec typecheck at the head ⇒ exit 0. new Function over source text is lint-clean here (no no-new-func / no-implied-eval in eslint.config.mjs) and confined to a test.

② Semver level

patch on @objectstack/spec — correct. skip-changeset would be wrong; minor and major would be wrong. Re-derived from the published artifact, not the manifest: the 17.4.0 tarball (spec-17.4.0.tgz, 2072 files) contains package/src/system/job.zod.ts, and that shipped copy carries the defective line (:96 id: "job_sync_meta"). So the corrected file publishes, and a false TSDoc in a shipped file is a defect in a released package (review-checklist: 「spec 发运 src/**/*.zod.ts,假 TSDoc/.describe() 即缺陷」) ⇒ a bug fix ⇒ patch, never skip-changeset (that label is for a diff that publishes nothing from any released package). Nothing is added (not minor); the accept set does not move (not major; check-changeset-no-major ✓, check-empty-changeset ✓, Check Changeset ✓ on this head). Changeset frontmatter "@objectstack/spec": patch is consistent with the PR body's Clause-②: no (a no carries no minimum level). The changeset body carries no model identifier and the diff touches no release page or CHANGELOG.

③ Boundary flags

open_questions: []. Three out_of_scope_findings flagged by the dev, plus one of mine.

  1. handler: "services/syncStatus.ts:syncAll" — reported, deliberately not fixed. Leaving it was right; the residual is real and is owed a card. Re-measured: JobSchema.handler is z.string() described as "Handler function name (must match a key in defineStack({ functions }))" (job.zod.ts:193); AppPlugin resolves fnMap[job.handler] (app-plugin.ts:1220) over collectBundleFunctions (:2292), a plain name→fn map keyed by bundle.functions names; the only colon-splits in runtime, cli and core are a host:port split (cli/src/commands/serve.ts:5488) and a hook-category split (core/src/security/plugin-permission-enforcer.ts:345), so no file:export spelling is lowered anywhere; a miss is warn + skip (:1222, pinned at app-plugin.jobs.test.ts:275-289). So the block copied verbatim still schedules nothing after this PR — it fails one layer later, and the card's own boundary sentence 「示例照抄即失败是 (a)」 covers it. The exemption (.claude/agents/os-dev.md, four conditions, all must hold): ② mechanical with a pinned shape — yes (handler: 'syncAll'; the sibling defineJob form and examples/app-showcase are the shape); ③ no other claim on the file — yes (No other open PR may claim the same single-writer path ✓); ④ same gate family — satisfiable by a spelling pin in the same test file; ① same defect class — not met, by the board's own calibration on this very card: the CronScheduleSchema.timezone in packages/spec is an unvalidated z.string() — the authoring tier for a job cron zone does not use the membership predicate its own package exports #16292 implementer found the id defect while narrowing timezone and did not ride it in, and the filer endorsed that as a different defect class; by that yardstick a parse-time refusal carrying a tombstone prescription and a boot-time silent skip carrying none are two classes even inside one block. One condition down ⇒ the rule's default (list, don't touch) is exactly what happened, with dedupe words in the report. Escalation, to the seat: the dev has no filing budget, so that card does not exist yet — file it (class (a), naming the published surface src/system/job.zod.ts in the tarball; a sub-issue of [finding] odata.zod.ts 的两个 @example 块随 tarball 与参考页发运,却在 check:skill-examples 的可达范围之外 —— packages/spec/src 下 134 个写 @example 的文件里,只有 6 个带标记 #19065 or standalone) at or before landing. Fixes #19184 is correct for the card as written — its defect statement is the parse refusal and it names nothing else — but the card's rationale is only fully discharged once the sibling card exists.
  2. check:skill-examples is blind to unfenced @example literals — confirmed (its population is fenced ts/tsx/typescript blocks, 10 in spec sources; this block has no fence). Carrier [finding] odata.zod.ts 的两个 @example 块随 tarball 与参考页发运,却在 check:skill-examples 的可达范围之外 —— packages/spec/src 下 134 个写 @example 的文件里,只有 6 个带标记 #19065. Agree: noted, not widened here.
  3. Trailing space on the handler line — pre-existing, comment-internal, read by no gate. Agree.
  4. Mine — the orphaned docblock (① item 2). The "Job Schema" TSDoc, example included, is attached to const JOB_ID_RETIRED, so JobSchema has no hover text and the example is invisible to every declaration-based reader. Same block, same family as flag 1 — belongs on that card or on [finding] odata.zod.ts 的两个 @example 块随 tarball 与参考页发运,却在 check:skill-examples 的可达范围之外 —— packages/spec/src 下 134 个写 @example 的文件里,只有 6 个带标记 #19065; not this PR's scope and not a reason to hold it.

Independence and landing inputs: the dev was a mode:subagent build (branch identity below); this record is an isolated subagent's and names no session of its own. Check runs on this head, read at 2026-09-20T10:56Z (last completion 2026-09-20T10:47:48Z): 35/35 completed, none non-green; the seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core — 6 shards and the rollup, Dogfood Regression Gate — 3 shards and the rollup, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) all success. No governed surface in the file list.

Implemented-by: claude/issue-19184-job-example-retired-id
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2

VERDICT: PASS


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 20, 2026 13:03
@os-steve
os-steve added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit c86d351 Sep 20, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-19184-job-example-retired-id branch September 20, 2026 13:46
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 protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] JobSchema's own TSDoc @example opens with the key id, retired in 17.0.0 — the example is refused when copied verbatim

2 participants