Skip to content

fix(spec): file-description skips doc blocks the lazify codemod detached from their symbols - #13336

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-13263-reexport-doc-block
Aug 30, 2026
Merged

fix(spec): file-description skips doc blocks the lazify codemod detached from their symbols#13336
os-zhuang merged 3 commits into
mainfrom
claude/issue-13263-reexport-doc-block

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#13263

⚠️Title correction, reported rather than routed around. The dispatch fixed this PR's title, and the measurement does not support it: a doc block written for a re-exported symbol is one instance (ui/view.zod.ts, already corrected at source by #13262 and gone from the tree), not the mechanism. The mechanism is an import injected by the lazify codemod between a doc block and the declaration it documents. Everything below reports what was measured; the title is the dispatch's and is the PM's to change.

The reading came first — the triage fence, honoured

Triage was explicit that the candidate list is not a verdict and that the route choice is not available until the candidates are read, one module at a time. So they were, before a line of the rule was touched.

Premise reproduced verbatim on origin/main at 3322527f, with the card's own one-level glob packages/spec/src/*/*.zod.ts:

bucketcardmeasured here
sources scanned193193
block selected, real declaration boundary105105
block selected, sits above an import / re-export6969
no qualifying block1919
of the 69, block starts after line 52929

What the 29 actually are — and the line-5 threshold is not the predicate

Reading them individually turned up something the card's framing did not predict. In 27 of the 29 the line directly under the block is not a re-export at all; it is import { lazySchema } from '…/shared/lazy-schema';, and the block is the doc of the declaration on the other side of that import:

/**
* Service Status Enum
* Describes the operational state of a service in the discovery response.
*/
import { lazySchema } from '../shared/lazy-schema';
export const ServiceStatus = z.enum([...]);

The cause is mechanical, not editorial. packages/spec/scripts/lazify-schemas.tsensureLazyImport inserts its import at the end of the file's leading run of comments, blank lines and imports, and the regex for that run counts a doc block among the comments (its comment alternative is slash-star, any run, star-slash). So a block written directly above a schema came out of the codemod with an import wedged in. Nothing about the block changed; only the detector's view of it did. That also means the attachment is broken for editors, not just for this generator — hovering PageRegionSchema today shows nothing, because the JSDoc is attached to the import.

The other 2 of the 29 are genuine module headers: shared/mapping.zod.ts ("This module provides the canonical field mapping schema used across ObjectStack") and system/cache.zod.ts ("Application Cache (system/cache.zod.ts) - This File").

And "starts after line 5" is an arbitrary cut. The predicate that actually separates the class is does a plumbing line precede the block — i.e. is the block inside the import block or above it. Measured, that is 39 of the 69, not 29; the extra 10 (api/events, data/datasource, data/document, data/seed, kernel/execution-context, shared/identifiers, system/auth-config, system/license, system/search-engine, ui/notification) sit at line 5 after a single import { z } from 'zod'; and every one of them was read as a symbol doc too. The card's 29 undercounts by construction.

The route chosen, and why the carve-out is measured rather than assumed

Direction 1 (tighten the rule), in the one form the reading supports. Condition 3 now sees plumbing the way condition 2 already does: a declaration reachable across nothing but blank lines and import / re-export lines is still the block's subject. Two limbs bound it, and dropping either was measured to cost real pages:

  • It applies only INSIDE the import block. The codemod injects after the last import, so a block preceding every import preceded them beforehand too. Without this limb, api/error-code-ledger, cloud/template-manifest and system/doc — three real headers whose imports happen to be followed straight by a declaration — go blank.
  • A comment of any kind on the far side still ends the preamble. A // === banner or the next schema's own JSDoc means the block was not against the declaration before the codemod ran either. api/analytics (banner) and system/cache (next schema's JSDoc) keep their headers through exactly this clause.

⛔ Neither the marker route nor a corpus pass is taken here: triage reserved the first as its own dispatch, and the second would edit packages/spec/src/**/*.zod.ts, which this dispatch fenced off.

Measured delta over the real corpus

origin/main's findModuleDocBlock versus this one, both run over the same tree (208 sources, recursive — the card's one-level glob misses kernel/events/*.zod.ts and src/stack.zod.ts):

scanned: 208
unchanged (same block still selected): 154
unchanged (still no description): 19
DEMOTED (had a description, now none): 35
CHANGED to a different block: 0

All 35 open with a schema's name. Zero legitimate module headers are lost — the three above-the-imports headers and both in-import-block headers are in the 154. 34 reference pages change (the 35th, src/stack.zod.ts, has no category and so no page).

The published skill indexes are where this stops being a docs-site matter. Each row now tells the truth instead of naming one schema:

- .../src/data/field.zod.ts — Field Type Enum
+ .../src/data/field.zod.ts — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, ...

Every added line in the regenerated products is one of those Exports: fallback rows — checked, not assumed: git diff -- content/docs/references skills | grep '^+[^+]' yields 28 lines and all 28 have that shape.

Reverse verification

Direction predicted before running: red, on the new cases only. The fix was committed first, so the restore leg has a real restore point.

Mutation (if (insideImportBlock && …) guarded to false) proven on disk by anchor count and blob hash, not by the editor's exit code: injected marker count 1, blob b16f90efcc4053c3. No dist leg exists — vitest imports ./lib/file-description from source, and tsx runs the generator from source.

× rejects a block the codemod separated from the declaration it documents
× rejects it across a blank line and several injected imports
× rejects it across a MULTI-LINE import — continuation lines are plumbing too
× never selects a block with a declaration on the far side of the imports (corpus)
× publishes no description for the modules whose opening was one schema's doc
Tests 5 failed | 73 passed (78)

The three keeps cases stayed green under the ablation, which is the asymmetry that says the pins measure the tightening and not the selector at large. Restored with git checkout HEAD -- ...: blob back to b16f90ef, git diff HEAD empty, git status --porcelain empty for the path.

One existing test asserted the opposite, and is corrected

keeps a header the lazify codemod separated from the imports pinned exactly this shape with the comment "It is still a header." Its cited real file is api/analytics.zod.ts — which carries a // === banner between the block and the declaration, and therefore is not in this class at all. The reduction had dropped the one line carrying the verdict. The case is rewritten to the file it names, banner included, and still asserts Analytics API Protocol; the bannerless shape it used to assert now sits directly below it asserting null, so the difference is visible in one screen.

Residue, filed rather than absorbed

10 modules still put a block inside their import list with a comment on the far side. 8 publish a schema's doc; 2 (shared/mapping, system/cache) are genuine headers. They are structurally identical — line number, imports-before count, block length and whether the next declaration has its own JSDoc all fail to separate them, each measured — so only the prose differs, and a first-sentence pattern check is the approach file-description.ts's own header rejects with the measurement behind the rejection. Recorded module by module in #13334, with the two composable routes and their real costs.

#13335 records a second finding from the merge: skills/*/references/_index.md is generator-owned and gated by check:skill-refs, but is in neither .gitattributes's merge=os-regen block nor regen-artifacts.mjs's NOT_DRIVER_MANAGED roster, so two branches that regenerate it text-merge. It conflicted for real on this merge.

Published-skills readings

Required whole-file and whole-package numbers, measured against merge base 56c093c4:

readingbeforeafterdelta
every changed references/_index.md, lines3903900
all skills/**/SKILL.md, lines (0 files touched)10515105150
all tracked skills/**, lines17858178580
all tracked skills/**, bytes752677754611+1934 (+0.26%)

No SKILL.md is touched and no row is added or removed — 8 index rows are rewritten in place. The byte growth is entirely the generator substituting an Exports: list for a description it no longer has; it is not authored expansion, and it is the price of the row being true.

⚠️skills/** is a governed surface (Prime Directive #14), so this PR stays draft and is the maintainer's to land. No AI seat merges, queues, or arms auto-merge on it.

Changeset

None, deliberately; the skip-changeset label declares it. This PR releases nothing from any package, checked on all three parts of the diff rather than on the first: packages/spec's files array is ["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]scripts is absent, so neither edited source ships; apps/docs is private: true, so content/docs/** ships nowhere; and no package manifest in the workspace names skills in its files (checked over every non-private tracked package.json). Precedents #13202 and #13199, both landed on the same argument.

Difference from those two, stated rather than glossed: they carried no regenerated products, this one does. The products are still unpublished by the test above, so the criterion the Check Changeset job states — such a PR releases nothing — holds the same way.

Verification

Union re-run after the final commit, at b48f9574.

Gate family derived mechanically with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no hand-fed paths — change set 44 path(s) vs merge base 56c093c4d, three-dot. Re-derived after the merge and byte-identical to the pre-merge derivation.

All exit codes captured before any pipe, all 0:

generated exit=0 docs-single-h1 exit=0 doc-anchors exit=0 doc-authoring exit=0
doc-frontmatter exit=0 doc-route-spell exit=0 docs-sect-name exit=0 sect-landing exit=0
docs-redirects exit=0 quick-ref exit=0 agent-test-sp exit=0 skill-compat exit=0
skill-frame exit=0 published-files exit=0 nul-bytes exit=0 docs-audit-sc exit=0
keyed-text exit=0 spec-typecheck exit=0 merge-driver exit=0 gov-merges exit=0

Verdict lines quoted from the gates themselves, not from $?:

  • ✓ All 14 generated artifacts are up to date.check:generated, which covers both regenerated artifacts, so the hand-resolved merge conflict in skills/objectstack-ui/references/_index.md is proven generator-exact rather than hand-merged into place
  • check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json

pnpm --filter @objectstack/spec testTest Files 443 passed (443), Tests 11784 passed (11784).

pnpm --filter '@objectstack/spec^...' build matched no projects. That zero match is real and not a mistyped filter: @objectstack/spec declares no workspace dependencies, so there is nothing to build ahead of it.

Declared narrowing. Repo-wide pnpm lint was not run: the container caps a foreground command at ~10 minutes and killed one union batch at that boundary already (exit 143), with os-verify-lock queue waits of 2–6 minutes on top. CI runs the farm regardless, and the derived family above is what this diff can move. check:type-check-debt --re-measure, check:dev-prereqs and check:dual-build-cjs-loads are NOT MEASURED here for the reason their own text gives — each refuses on an unbuilt workspace closure rather than failing — and CI builds first and runs all three.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 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 3f7f8f59099cb0f4c3d8a1ed127540ee68c7f411packageMentionDocs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
@os-project-manageros-project-manager changed the title fix(spec): file-description skips doc blocks written for re-exported symbolsfix(spec): file-description skips doc blocks the lazify codemod detached from their symbolsAug 30, 2026
@claude
claudeBot requested review from hotlong and os-zhuangAugust 30, 2026 03:56
@os-zhuang
os-zhuang marked this pull request as ready for review August 30, 2026 10:43
@os-zhuang
os-zhuang enabled auto-merge August 30, 2026 10:43
@os-zhuang
os-zhuang added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 3bfd4e3Aug 30, 2026
41 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-13263-reexport-doc-block branch August 30, 2026 11:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gateteststooling

Projects

None yet

2 participants

@os-zhuang@claude