LearnNewsExamplesServices
Frontmatter
id13426
titleCI lint for unparseable JSDoc type expressions + fix the docs-build breakers
stateClosed
labels
bugdocumentationairegressionbuild
assigneesneo-opus-vega
createdAtJun 16, 2026, 12:21 PM
updatedAtJun 16, 2026, 1:39 PM
githubUrlhttps://github.com/neomjs/neo/issues/13426
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 16, 2026, 1:39 PM

CI lint for unparseable JSDoc type expressions + fix the docs-build breakers

Closed v13.1.0/archive-v13-1-0-chunk-3 bugdocumentationairegressionbuild
neo-opus-vega
neo-opus-vega commented on Jun 16, 2026, 12:21 PM

Context

The v13 JSDoc cleanup (#12899) made the docs build fail loudly on unparseable type expressions — buildScripts/docs/jsdocx.mjs now exits non-zero when any jsdoc-x batch throws. #12899 explicitly deferred the prevention arm as a post-v13 follow-up, out of scope: "a lint/CI gate running the parser in check-mode." That follow-up was never filed, so the failure mode recurred: new TS-like JSDoc type expressions have landed in src/dashboard/ and again break npm run generate-docs-json (the last step of build all) and the docs-app content. This ticket is that deferred #12899 follow-up plus the cleanup of the current breakers. Operator-flagged as the top-priority next item.

Release classification: release-blocker candidate — breaks the build all docs step + docs-app content; board on the active release project if release-gating.

The Problem

jsdoc-x parses type expressions with catharsis (the Closure/JSDoc type grammar — not TypeScript). A bare union inside a record-type value breaks it, and — empirically (V-B-A below) — the precise trigger is a bare union with no space after the colon:

type expression catharsis.parse(expr, {jsdoc:true})
{layout:Object|null, errors:String[]} (no space) FAILExpected … but "|" found
{layout: Object|null, errors: String[]} (space) OK
{layout:(Object|null), errors:String[]} (parenthesized) OK

The build aborts on the first failing batch (runner.mjs Promise.all rejects → jsdocx.mjs .catch sets exitCode=1) without enumerating the rest — which is why the raw log reads like "50+ errors" (one batch's errors, doubled across the error + stack re-print). The true unparseable set across the full build scope (src, ai, docs/app, apps) is 5 record-unions, all in the dashboard docking subsystem.

The Architectural Reality

  • Parser chain: buildScripts/docs/jsdocx.mjsbuildScripts/docs/jsdoc-x/{index,runner}.mjsjsdoc-apicatharsis (dictionaries: ['jsdoc','closure']). Scope (jsdocx.mjs:18): src/**, ai/**, docs/app/**, configured apps/**; excludes underscore-prefixed paths + the machine-local config overlays.
  • The fail-loud-but-abort-on-first-batch behavior is by design (#12899) — correct for the build, but it surfaces late (last build all step) and never enumerates. A fast pre-merge lint that runs the same parser and enumerates all offenders is the missing prevention layer.
  • The 5 current breakers (all @returns record-unions, no space after colon):
    • src/dashboard/DockZoneModel.mjs:655 {{layout:Object|null, errors:String[]}}
    • src/dashboard/DockZoneModel.mjs:734 {{document:Object|null, errors:String[]}}
    • src/dashboard/DockZoneModel.mjs:891 {{collection:Object|null, errors:String[]}}
    • src/dashboard/DockZoneModel.mjs:1056 {{document:Object|null, errors:String[]}}
    • src/dashboard/DockSplitter.mjs:215 {{document:Object|null, errors:String[]}}
  • The rest of the codebase (e.g. src/ai/WriteGuard.mjs:64, src/ai/admitWrite.mjs:31, much of ai/**) already uses the spaced or parenthesized form → catharsis-valid → must NOT be touched.

The Fix

  1. Clean the 5 breakers — parenthesize the union: Object|null(Object|null). Matches the codebase's established in-record-union pattern (admitWrite.mjs, DockZoneModel.mjs:384 slot:(Number|String)), is robust regardless of spacing, preserves author intent, minimal diff.
  2. Add buildScripts/util/check-jsdoc-types.mjs — a catharsis-scan lint (sibling of check-shorthand.mjs / check-aiconfig-test-mutation.mjs): extract type expressions from /**-block type tags (@param/@returns/@type/@property/@typedef/@yields/@throws/…) and run catharsis.parse(expr, {jsdoc:true}), failing on parse errors. Runs the same parser the build uses, catching exactly what breaks build all — including future TS-isms a regex couldn't anticipate. CLI: positional file-args (lint-staged) OR no-args full-build-scope scan (CI). Exported core for a unit spec. /**-only (jsdoc ignores /* and //).
  3. Wire it: lint-staged *.mjs entry (fast local pre-commit) + .github/workflows/jsdoc-type-lint.yml (the firm gate; npm ci --ignore-scripts for catharsis, skipping the heavy postinstall).
  4. Unit spec locking the behavior + the spacing quirk.

Contract Ledger

Not applicable — no consumed API / MCP tool / error-code / config-key surface changes. The lint is a new build/CI gate that enforces the docs-build's already-existing parse requirement earlier; it introduces no new contract, only surfaces an existing one pre-merge. (Ledger gate run per discipline → negative result.)

Decision Record impact

none — build tooling + JSDoc-comment hygiene; no ADR governs the docs-build type grammar.

Acceptance Criteria

  • The 5 dashboard/ record-unions are parenthesized → catharsis-parseable.
  • npm run generate-docs-json passes after the fix (ground-truth build gate).
  • buildScripts/util/check-jsdoc-types.mjs: scans /**-block type tags over the build scope via catharsis; exit 1 + lists offenders on failure, exit 0 on clean; supports file-args (lint-staged) + no-args (CI full scan) modes.
  • Wired: lint-staged *.mjs + .github/workflows/jsdoc-type-lint.yml (workflow green on the branch).
  • Unit spec: no-space union FAILs, spaced/parenthesized/plain pass, /*-block ignored, malformed-bracket FAILs — proving catch-correctness with no false-positives/negatives.

Out of Scope

  • src/sitemap/Component.mjs:30 @member {String[} (malformed-bracket typo) — it lives in a /* single-star comment jsdoc ignores, so it does NOT break the build; the /**-only lint correctly won't flag it. (Separate latent issue: those sitemap member docs use /* and aren't parsed at all.)
  • Re-styling the ~30 valid spaced/parenthesized record-unions — they parse fine; pure churn.
  • Forking/patching catharsis's no-space quirk — we conform to the parser, not patch it.

Avoided Traps

  • Regex linter — the trigger is a catharsis tokenizer quirk (spacing-sensitive); a regex would false-positive on valid spaced unions or miss other TS-isms. Running the real parser is the only correct design (the spacing table above is the falsifying evidence).
  • Heavy CI install — the other lint workflows are dependency-free; mine needs catharsis, so npm ci --ignore-scripts keeps it lean.

Related

  • #12899 (origin — this is its deferred post-v13 lint follow-up; the fail-loud arm shipped there)
  • Dashboard docking subsystem: #13158 (epic), #13247 — the breakers landed here.

Handoff Retrieval Hints

  • Semantic: "catharsis no-space union record JSDoc type lint docs build"
  • Anchors: buildScripts/docs/jsdocx.mjs:18, buildScripts/docs/jsdoc-x/runner.mjs:104-116, src/dashboard/DockZoneModel.mjs:{655,734,891,1056}, src/dashboard/DockSplitter.mjs:215.

Authored by Claude Opus 4.8 (Claude Code), @neo-opus-vega (Vega).

tobiu referenced in commit b1a6910 - "fix(docs): CI lint for unparseable JSDoc types + fix the docs-build breakers (#13428) on Jun 16, 2026, 1:39 PM
tobiu closed this issue on Jun 16, 2026, 1:39 PM