feat(scripts): add type-bearing JSDoc hygiene gate for .ts source (SD-673)#3511
Conversation
…-673) Adds a new check:public:superdoc stage (jsdoc-hygiene-ts) that enforces TypeScript-as-single-source on the .ts public contract surface. Companion to the existing jsdoc-ratchet stage, which covers .js files; together they enforce that each language's source files use the right type system (TS syntax in .ts, JSDoc + @ts-check in .js) without one polluting the other. Scanner (check-jsdoc-hygiene-ts.cjs): - Walks .ts source under packages/superdoc/src and packages/super-editor/src (excludes test/.d.ts files). - Positive detection on tag name. ALWAYS_FLAG_TAGS: @type, @typedef, @callback, @template, @implements, @extends, @Augments, @enum. FLAG_WHEN_TYPED_TAGS: @param, @returns, @return, @this (only flagged when tag.typeExpression is set, so prose-only @param name description passes). Every other tag is ignored, including @deprecated, @example, @throws, @see, @typeparam. - AST-based via ts.getJSDocTags + tag.typeExpression presence check; no regex on raw comment text. De-dupes per (file, pos, end, tag) so tags surfaced through multiple parent nodes count once. - Classifies each violation as declaration-doc-type, inline-fake-cast, or typedef-style so the future cleanup PR can group fixes by type. Baseline (jsdoc-hygiene-ts-baseline.json) records the 85 existing violations grandfathered at PR time. The ratchet fails on net-new entries or stale baseline entries (file cleaned up, snapshot still records it). Refresh with --write after intentional cleanup. Goal is to drain to zero, then flip to 'zero allowed' in a separate PR. Scanner tests (check-jsdoc-hygiene-ts.test.cjs) protect against two failure modes: silent false-positives (flagging legitimate doc tags) and silent false-negatives (scanner produces nothing and looks like it's working). 13 in-memory fixtures including a negative control (prose-only JSDoc + TS types -> zero hits) and a mixed-tag block (@deprecated + typed @param + prose @returns -> exactly one hit). Policy doc at packages/superdoc/scripts/type-hygiene.md explains the rule, the fix patterns for each violation class, and the scope. The gate's failure message links to this doc so contributors hit a clear path forward rather than a cryptic CI error. Verified: scanner tests -> 13 passed, 0 failed; baseline scan -> 85 grandfathered, no net-new; canary fixture (added typed @param) -> correctly fails with the new violation surfaced; pnpm check:public:superdoc --skip-build -> PASS (10 ran, 1 skipped, 139.4s). Sibling pnpm check:jsdoc continues to enforce the .js side unchanged.
…pdate wrapper docs Three follow-ups to the initial scanner commit, all addressing review feedback: Baseline key was file:line:tag, which churned under unrelated edits. Inserting 3 blank lines at the top of SuperDoc.ts (verified empirically) would have produced one stale + one new baseline entry per grandfathered violation in that file - ~40 false-positive ratchet failures from a no-op edit. New key shape: file::enclosingSymbol::tagName::class::occurrenceIndex Line stays in the violation record for human-readable display in failure messages, but is NOT part of the identity tuple. Verified that inserting 3 blank lines at the top of SuperDoc.ts now produces zero stale/new churn (85 still grandfathered). enclosingSymbol is the nearest named-declaration ancestor (function, method, class, interface, type alias, variable). occurrenceIndex disambiguates multiple same-name tags on the same symbol (e.g. two @param tags on the same method). Wrapper docs updated to reflect the 11-stage check:public:superdoc: - AGENTS.md (CLAUDE.md symlinks to it): stage list + count. - scripts/check-public-contract.mjs header: inserted stage 4 (jsdoc-hygiene-ts), renumbered 5-11. @typedef test fixture comment corrected: only @typedef fires (ts.getJSDocTags does not surface @Property as a top-level tag); the parent @typedef is the violation and the cleanup fix takes its @Property lines with it. Verified: 13/13 scanner tests pass; check:public:superdoc --skip-build PASS (10 ran with --skip-build, 1 skipped, 126.6s); line-shift canary on SuperDoc.ts produces no baseline churn.
…EADME Three follow-ups addressing review feedback on the scanner PR: enclosingSymbolName() only recognized Identifier names, so private methods like `#privateMethod` collapsed onto their enclosing class's key. Verified empirically: the previous baseline had 12 SuperDoc:: entries (6 distinct `SuperDoc::param` with occurrenceIndex 0-5), but the SuperDoc class itself only carries @extends and @implements JSDoc. The other 10 were private methods (#applyDocumentMode, #abortUpgrade, etc.) all keyed as SuperDoc, which would have churned unrelated indexes when any one of them was edited. Fix: - renderName() helper recognizes ts.isPrivateIdentifier (returns #name via nameNode.text so the leading # is preserved without depending on escapedText escape rules across TS versions) and string/numeric literal property names alongside identifiers. - enclosingSymbolName() uses renderName() for direct .name, variable declarations, and variable statements. Regenerated baseline. SuperDoc::SuperDoc:: entries dropped from 12 to 2 (the legit class-level @extends and @implements); 13 private-method entries now anchor to their actual #name. Total count unchanged at 85 — same violations, more honest keys. packages/superdoc/scripts/README.md updated with a row for check-jsdoc-hygiene-ts.cjs alongside check-jsdoc.cjs, and the wrapper-stages paragraph now lists jsdoc-hygiene-ts alongside the other policy gates. Scanner failure-message scope wording aligned with reality: was "public contract surface," now "packages/superdoc/src or packages/super-editor/src," matching the actual scan scope. Verified: 13/13 scanner tests pass; check:public:superdoc --skip-build PASS (10 ran, 1 skipped, 138.5s); single-# rendering on private identifiers confirmed in baseline output.
…ublic summary
Two doc tweaks following review:
README.md wrapper-stages paragraph was correct in its count but the
prose ('Of these, six run as wrapper stages ... after [parenthetical
including public-method-coverage] and build: [list of five post-build
stages]') could be parsed as a mismatch. Rewrote to break public-
method-coverage out as 'runs alongside the cheap policy gates' so the
'five post-build' list reads cleanly as a subset rather than as the
total.
AGENTS.md check:public summary bullet listed every other gate but
omitted the new ts-jsdoc hygiene one; added it alongside 'jsdoc
ratchet'. The detailed check:public:superdoc bullet was already
correct.
Verified: file diffs only, no script changes; baseline + scanner
tests unchanged.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 89e75fbde5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…s vitest discovery on #3511) CI failure on this PR: the unit-tests (other-packages) job runs vitest across all non-superdoc packages, and the @superdoc package's vitest config picked up packages/superdoc/scripts/check-jsdoc-hygiene-ts.test.cjs via the *.test.* glob. The file is a standalone Node runner (not a vitest suite), so vitest fails with: Error: No test suite found in file packages/superdoc/scripts/check-jsdoc-hygiene-ts.test.cjs Test Files 1 failed | 284 passed (285) Local check:public:superdoc didn't catch this because it doesn't run the broader vitest sweep. Two-part fix: - Rename check-jsdoc-hygiene-ts.test.cjs -> check-jsdoc-hygiene-ts-tests.cjs. The new name doesn't match vitest's *.test.* glob. - Wire the self-tests in as wrapper stage 4 (jsdoc-hygiene-ts-test), running immediately before jsdoc-hygiene-ts. Self-tests had been manual-only; AST drift would have surfaced as a silent zero-result downstream. The stage runs the standalone Node script and asserts 13 in-memory fixtures pass. File header updated to reflect the rename and the stage wiring. AGENTS.md / CLAUDE.md (symlinked) and scripts/check-public-contract.mjs header now show 12 stages (was 11) with stage 4 = jsdoc-hygiene-ts-test. packages/superdoc/scripts/README.md row updated for the rename plus the wrapper-stages paragraph now includes the new stage. Verified: - pnpm check:types -> PASS - node check-jsdoc-hygiene-ts-tests.cjs -> 13/13 pass - pnpm check:public:superdoc --skip-build -> PASS (11 ran, 1 skipped, 131.1s; was 10 before this commit added the test stage) - pnpm --filter superdoc test --run -> PASS (1054/1054, 77 files; was 1 file failing before the rename)
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…(SD-673) Drains the jsdoc-hygiene-ts baseline from 85 entries to 0. Companion to the scanner PR (#3511) that grandfathered these entries; this is the bulk mechanical cleanup. The zero-baseline flip lands in a third PR after this. declaration-doc-type (78 entries): - @param {T} name description -> @param name description (drop braces, keep prose). - @returns {T} description -> @returns description (drop braces, keep prose). - @template T - description -> @typeparam T - description. TSDoc-canonical replacement; prose preserved (16 entries across Mark / Node / OxmlNode / defineMark / defineNode / Extension / EventEmitter / helpers). - @extends {Base} / @implements {Iface} -> deleted (TS has native extends / implements syntax on the declaration). - Empty @returns tags removed (10 dead documentation lines across both EventEmitters and PresentationEditor where the brace strip left a tag carrying no information). inline-fake-cast (6 entries: 5 from original baseline + 1 new on Editor.ts:219 that landed post-#3511): - #abortUpgrade, readyEditors, pendingCollaborationSaves: the @type was redundant next to the TS field annotation. Dropped the tag, kept the prose. - /** @type {RuntimeDocument} */ ... and /** @type {string} */ doc.id in SuperDoc.ts: real-intent casts that did nothing in .ts. Converted to 'as RuntimeDocument' and 'as string'. - /** @type {YMapEvent<unknown>} */ on a Yjs observe callback param in SuperDoc.ts: converted to a TS param annotation (event: Y.YMapEvent<unknown>). - /** @type {Mark[]} */ on insertionMarks in Editor.ts:219 (new in main since the scanner PR): converted to a TS local var annotation (PmMark[] is already imported). - The InternalConfig doc example in types/index.ts was teaching the /** @type */ cast pattern explicitly. Rewrote to teach '(value as Type)' instead. typedef-style (2 entries): rewrites. The @typedef tags lived inside @example blocks in defineMark.ts / defineNode.ts teaching JS consumers how to type-hint. Replaced literal /** @typedef */ syntax with prose describing the pattern; preserves teaching value without triggering the scanner. Verified: - node check-jsdoc-hygiene-ts.cjs -> OK, 0 violations - node check-jsdoc-hygiene-ts-tests.cjs -> 13/13 pass - pnpm check:types -> PASS - pnpm check:public:superdoc --skip-build -> PASS (11 ran, 1 skipped, 135.3s) - pnpm --filter superdoc test --run -> PASS (1064/1064, 79 files)
…jsdoc-hygiene-ts Two pre-review trims on the strict-zero scanner: - occurrenceIndex / occurrenceCounter were the stable-baseline-key mechanism. With strict-zero, there's no key — failure messages only need (file, line, tag, class, symbol) for display. Dropped the counter, the per-finding occurrenceIndex field, and the sort comparator that used it. Sort now keys on (file, line, tag) which is the natural display order for a scanner failure. - Removed '#3513'/'#3511' from source/doc prose. PR numbers belong in PR bodies, not long-lived files. The scanner header and type-hygiene.md now use timeless wording ('no baseline, grandfathering, or --write mode'). No behavior change. Scanner output, exit codes, and the canary case all match the previous commit.
### Bug Fixes - copy-pasted text in suggestion mode (#3576) - center virtualized matches after mount in find nav (SD-3315) - stop find navigation jumping to the reverted caret (SD-3315) - honor the focus() contract + fix dangling docs reference (SD-3312) - don't re-center visible matches on find navigation (SD-3315) - report 'zoom' not 'mixed' for a zoom repaint (SD-3311) - wire pointer-source tracking on all init paths; update export snapshot - reset state on unload, dedupe + export payload types, add core tests - skip empty block SDT content selection - type modules.contentControls exactly (no pass-through index) - allow block SDT wrapper deletion to follow lock rules - promote image-bearing inline SDT wrappers to inline-block - keep block sdt fill behind content - hide block sdt fills in output modes - paint block SDT background on chrome layer - use logical inset for inline SDT label position - anchor inline SDT label to start of chrome - route smartTag through export and preserve smartTagPr in SDT flatten (SD-2647) - include inline SDT chrome width in block SDT bounds - keep block SDT chrome at full fragment width - render and round-trip w:smartTag content (SD-2647) - decouple base64 image helper imports - preserve block ids during metadata updates - ignore covered sdt label clicks - sync block sdt label selection updates - show empty SDT placeholder text in viewing and print modes - skip empty sdt scan on arrow right - preserve permission-only sdt placeholders - preserve comment-only sdt placeholders - preserve empty sdt bookmark placeholders - collapse hidden sdt placeholder text - keep sdt placeholder pm range atomic - trust empty sdt paragraph conversion - keep vanished sdt paragraph side effects - preserve vanished block sdt paragraphs - suppress hidden block sdt chrome - keep remeasured sdt placeholder atomic - transform sdt placeholder measure - remeasure sdt placeholders - hide sdt placeholders in print - hide sdt placeholders in viewing - ignore collapsed inline sdt cut - expose block sdt appearance - hide empty block sdt placeholder - align empty block sdt caret - use measured width for empty SDT placeholders - size SDT block labels to content width - collapse selection on sdtContentLocked delete - allow history transactions through sdt lock - drop unreachable move fallback - target marker-only textblock end - ignore empty block sdt key targets - cap block sdt label width - handle sdt marker gaps and block atoms - skip hidden field annotations in sdt navigation - skip hidden metadata sdt markers - skip hidden block sdt markers - handle marker-only sdt paragraphs - keep visible atoms in sdt navigation - skip hidden sdt navigation markers - target nearest sdt cursor position - respect inline atoms in sdt navigation - handle empty block sdt navigation - avoid restoring dragged block to its source position - exclude sdt chrome labels from caret position lookup - keep text-align enabled in locked SDT paragraphs - block disabled toolbar execution - guard unlisted locked toolbar commands - block locked sdt toolbar execution - reject malformed base64 image data URIs - reuse colliding data uri media targets - enforce upload byte cap for data uris - avoid non-image data uri extensions - reject raw raster data uri dimensions - validate oversized async svg images - register preset raster data uris in place - narrow sdt metadata overrides - normalize image data uri extensions - centralize image data url policy - reuse target image relationships - validate in-place svg payloads - validate field annotation data uri exports - reject malformed data uri files - avoid duplicate image rids - block raw raster data uri exports - reject malformed svg data uri payloads - warn on skipped image exports - reject separatorless data uri files - block non-image data uri exports - read svg data uri dimensions - share data uri media parsing - skip invalid data uri image targets - normalize svg data uri filenames - validate in-place svg image data - guard non-base64 data uri exports - export field annotation svgs as svg - allow non-base64 SVG data URLs in image rendering - mirror in-place image media to parent - decode non-base64 data URI exports - extract shared hash helpers - reuse data URI image exports - support non-base64 data URI images in registration - scope inline SDT placeholder to structuredContent metadata - register sized SVG data URI images without canvas processing - persist data URI images set via setPresetContent - share SDT lock predicates - version inline image metadata - align RTL SDT chrome to text - size SDT chrome for justified lines - honor ancestor image SDT locks - dirty inline image SDT changes - align SDT chrome within paragraph width - preserve SDT chrome continuation offsets - offset block SDT chrome for indents - suppress SDT pseudo hover in viewing mode - allow top-aligned inline images - fit block SDT chrome to actual content width - bottom-align text on lines with inline images - keep block SDT chrome and inline images out of paragraph geometry - disable image resize inside content-locked SDTs - detect inline image run changes in paragraph diff - mark block SDT selected when contained image is selected - select inline SDT content as text on Delete - intercept beforeinput insertText at inline SDT boundaries - delete contentLocked SDT wrapper in one step - bump sdBlockRev on ancestors of inline edits - select inline SDT content as text on Backspace - select inline SDT on Backspace at start of following run (SD-3165) - share structured content chrome label set - resolve block labels at node boundary - use contract label selectors - clear label gesture state on cancel - scope label clicks to owning editor - defer label selection to mouseup so native drag still fires - share structured content label classes - avoid deferred block label retry - select labels with active editor - focus editor after label selection - correct cursor placement and label interactions for structured content - handle cell-level SDT in vMerge column lookup (SD-3289) - preserve cell-level SDT wrapping table cells (SD-3289) - preserve recipient identity attrs on replay (SD-3279) - accept legacy fingerprints in compare/apply (SD-3279) - drop misleading pnpm run type-check hint from audit - structural-fail on missing dist + sync check-jsdoc header - strip session-local sdBlockId from diff fingerprint (SD-3279) - autoFit table width overflow from cell preferences (#3522) - flip public-method-coverage to strict-zero gate (SD-673) - flip jsdoc-hygiene-ts to strict-zero gate (SD-673) - rename jsdoc-hygiene-ts self-tests + wire into CI (fixes vitest discovery on #3511) - jsdoc-hygiene-ts handles private-identifier symbols + README - more bugs - add ui for overlapping delete, other fixes - replacement pair - remaining collab bugs - coalesce tracked inserts across run gaps - restore tracked change comment interactions - expose tracked mark predicate option - more cases - tc fixes - collab mode bug - floating comments fixes - make jsdoc-hygiene-ts baseline key line-independent + update wrapper docs - preserve tab underline via runProperties fallback in collab ### Changes - Merge branch 'main' into caio/sd-3315-find-replace-scroll - Merge pull request #3509 from superdoc-dev/artem/SD-3232 - Merge pull request #3555 from superdoc-dev/luccas/delete-image-content-locked-sdt - Merge branch 'main' into artem/SD-3159 - Merge pull request #3550 from superdoc-dev/luccas/sd-3302-bug-sdt-in-template-builder-shows-grey-background-highlight - Merge branch 'main' into artem/SD-3159 - SD-2676 - fix: table selection not providing a feedback (#3508) - Merge pull request #3549 from superdoc-dev/luccas/left-align-inline-sdt-label - Merge pull request #3546 from superdoc-dev/caio/sd-2647-bug-render-and-round-trip-content-wrapped-in-wsmarttag - Merge remote-tracking branch 'origin/stable' into sync/stable-to-main-20260527-230540 - Merge pull request #3539 from superdoc-dev/caio-pizzol/sd-3289-preserve-cell-level-sdt - Merge pull request #3527 from superdoc-dev/caio-pizzol/sd-3279-strip-sdblockid-from-diff-fingerprint - Merge branch 'main' into caio-pizzol/SD-js-contract-owner-audit - Merge pull request #3531 from superdoc-dev/caio-pizzol/SD-docs-snippet-typecheck-2 - Merge pull request #3526 from superdoc-dev/caio-pizzol/SD-runtime-payload-tests - Merge pull request #3521 from superdoc-dev/caio-pizzol/SD-public-method-coverage-strict-zero - Merge branch 'stable' - Merge pull request #3485 from superdoc-dev/artem/SD-3200 - Merge pull request #3517 from superdoc-dev/caio-pizzol/SD-jsdoc-hygiene-zero-flip - Merge pull request #3513 from superdoc-dev/caio-pizzol/SD-jsdoc-hygiene-cleanup - Merge branch 'stable' - Merge pull request #3511 from superdoc-dev/caio-pizzol/SD-jsdoc-hygiene-scanner - Merge pull request #3435 from superdoc-dev/nick/sd-3220-overlapping-suggestion-contract - Merge pull request #3429 from superdoc-dev/artem/underlined-tab-collab ### Documentation - note content controls in entityAt hit types (SD-3313) - clarify data uri buffer conversion - document image data uri helpers - clarify image registration comments - sync README exit semantics for js contract-owner audit - clarify wrapper-stages prose + add ts-jsdoc to check:public summary ### Features - add ui.contentControls.focus to place the caret in a control (SD-3312) - add ui.viewport.observe geometry-invalidation signal (SD-3311) - add ui.contentControls.scrollIntoView (SD-3310) - add activePath (full active stack) to content-control:active-change - expose public sdt events - select adjacent block SDT content at textblock boundaries - inherit run styles in empty block SDT placeholders - render placeholder text for empty SDTs - move caret into following block sdt on delete - move caret into preceding block sdt on backspace - disable mutation toolbar controls inside content-locked SDTs - render empty inline SDTs as a visible placeholder - add modules.contentControls.chrome - js contract-owner audit, report-only (SD-673) - add snippet typecheck for editor/superdoc/** + fix stale examples (SD-673) - add non-hover field color for sdt (#3506) - comments and tc on small screen (#3446) - overlapping tracked changes - add type-bearing JSDoc hygiene gate for .ts source (SD-673) - add anchored metadata orphan status ### Tests - resolve handleBase64 source path from package or repo root - cover locked block SDT Delete selection - cover nested block SDT boundary selection - update SDT keymap chain coverage - cover contentControlsChrome plumbing; clarify chrome-none comment - assert chrome-none hover suppression and cascade order - add v2 bridge unit + round-trip behavior coverage (SD-2647) - assert smartTag child text survives export round-trip (SD-2647) - cover nested block sdt navigation - roundtrip mixed image block sdts - cover image data uri length boundary - repaint saved sdt images through painter - cover structured content image edges - cover inline image diff fields - cover locked inline SDT beforeinput - cover inline SDT Cmd+X selection - cover inline SDT content Delete flow - clarify inline SDT boundary lock comment - cover two-step inline SDT Backspace - cover inline SDT selection meta escape - use real production payload for list-definitions-change bridge (SD-673) - tighten list-definitions-change bridge assertion (SD-673) - pin list-definitions-change bridge and DELETED comments shape (SD-673) - pin runtime event payload shapes (SD-673) ### Refactoring - consolidate type imports in dom painter - share block sdt navigation helpers - share box model between block and inline sdt labels - share structured content predicates - wrap shared tryDecodeDataUriText re-export - reuse shared data uri export policy - share image relationship export lookup - centralize image data uri parsing - share data uri text decoding - trim data uri metadata fields - extract CHECKED_FILES to shared module + drop audit from wrapper - drop list-item fragment renderer (SD-2851) (#3269) - drain 85 type-bearing JSDoc entries from .ts source (SD-673) ### Performance - avoid scanning data uri media ### Chores - 1.37.0 [skip ci] - fix import breaking delinstrtext orphans (#3535) - fix tests (#3533) - fixes - more fixes - run generate:all and fixes - more fixes - soec fixes - type fixes - ui and more - fix regression - type fixes - more fixes - more fixes - ci fixes - type fixes - add dispatch test for collab bug - review fix, type fix - add tests for metadata issue - tests for review issues
|
🎉 This PR is included in superdoc-cli v0.15.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.14.0 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.10.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.38.0 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.9.0 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.10.0 |
Adds a new
check:public:superdocstage (jsdoc-hygiene-ts) that enforces TypeScript-as-single-source on.tssource underpackages/superdoc/src/andpackages/super-editor/src/. Companion to the existingjsdoc-ratchetstage which covers.jsfiles; together they enforce that each language's source files use the right type system without one polluting the other.Scope is TS-source hygiene in the public packages, not a reachability-filtered public-surface scan. Every
.tsfile under those roots is scanned (excluding*.d.ts,*.test.ts,*.spec.ts, anddev/,__mocks__/,__fixtures__/directories). The rule is "TS source uses TS syntax for shape, prose-only JSDoc for documentation" - it applies uniformly, not only to declarations that reach the emitted facade.This is the scanner + baseline PR. Cleanup of the 85 baseline entries comes next in a separate PR; the zero-baseline flip is a third PR after that. Same ratchet pattern as
jsdoc-debt-snapshot.json.Detection model (positive, on tag name):
ALWAYS_FLAG_TAGS:@type,@typedef,@callback,@template,@implements,@extends,@augments,@enum. Each has a native TS equivalent.FLAG_WHEN_TYPED_TAGS:@param,@returns,@return,@this. Only flagged whentag.typeExpressionis set (the{Type}brace). Prose-only@param name descriptionpasses.@deprecated,@example,@throws,@see,@typeParam, etc.AST-based, not regex. Uses
ts.getJSDocTags+tag.typeExpressionso prose containing the literal text@type {Foo}(e.g. docstring examples that escape the closing comment with*\/) doesn't false-positive. De-duped per(file, pos, end, tag)so tags surfaced via multiple parent walks count once.Stable baseline key:
file::enclosingSymbol::tagName::class::occurrenceIndex. Line numbers are not part of the key - the baseline survives unrelated edits that shift line numbers. Verified empirically: inserting 3 blank lines at the top ofSuperDoc.ts(shifting ~40 grandfathered violations down) produces zero stale/new churn.enclosingSymbolhandles private identifiers, named declarations, string/numeric property keys. Private methods like#applyDocumentModeanchor to#applyDocumentMode, not to their enclosing class. The identifier-only resolver previously collapsed 10 private-method violations ontoSuperDoc; the fix correctly distributes them.Classification. Each violation is tagged as
declaration-doc-type,inline-fake-cast, ortypedef-styleso the cleanup PR can group fixes by type.Baseline.
jsdoc-hygiene-ts-baseline.jsonrecords the 85 existing violations grandfathered at PR time. Tag distribution: 31@template, 23@param, 22@returns, 5@type(inline fake casts), 2@typedef, 1@extends, 1@implements. Concentrated inSuperDoc.ts,EventEmitter.ts,Mark.ts,Node.ts,OxmlNode.ts.Scanner tests (
check-jsdoc-hygiene-ts.test.cjs) protect against silent false-positives and false-negatives. 13 in-memory fixtures including a negative control (prose-only JSDoc + TS types -> zero hits) and a mixed-tag block (@deprecated+ typed@param+ prose@returns-> exactly one hit).Policy doc at
packages/superdoc/scripts/type-hygiene.mdexplains the rule, the fix pattern for each violation class, and the scope. The gate's failure message links to it.Wrapper docs updated for the 11-stage chain:
AGENTS.md(CLAUDE.md symlinks to it),scripts/check-public-contract.mjsheader,packages/superdoc/scripts/README.md(new row forcheck-jsdoc-hygiene-ts.cjs+ updated wrapper-stages paragraph).Verified: scanner tests -> 13 passed; baseline scan -> 85 grandfathered; canary fixture (
@param {string}added) -> correctly fails; line-shift canary (3 blank lines prepended toSuperDoc.ts) -> zero baseline churn; private-identifier rendering verified single-#;pnpm check:public:superdoc --skip-build-> PASS (10 ran with--skip-build, 1 skipped, 138.5s). The siblingpnpm check:jsdoccontinues to enforce the.jsside unchanged.