Skip to content

feat(mds)!: basePath fix (#180), browser lint exports (#215), option-type second pass (#213) - #302

Merged
dean0x merged 26 commits into
wave/v0.4.0-wave1from
ticket/pr3-ts-public-surface
Aug 16, 2026
Merged

feat(mds)!: basePath fix (#180), browser lint exports (#215), option-type second pass (#213)#302
dean0x merged 26 commits into
wave/v0.4.0-wave1from
ticket/pr3-ts-public-surface

Conversation

@dean0x

@dean0x dean0x commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Expose basePath on CompileOptions to match LintOptions #180 (live bug — basePath silently dropped): basePath was accepted by the unknown-option validator but discarded before reaching the backend. The forwarding builders (compileOpt/varsOpt) never included it. Templates using @import/@extends with compile(source, { basePath }) resolved imports against the wrong directory or failed silently. Fixed by adding basePath to CompileOptions/CheckOptions and propagating through four new typed builders (compileSrcOpt, checkSrcOpt, fileCompileOpt, fileCheckOpt). The WASM backend now rejects non-null basePath on compile/check/lint with mds::invalid_options (OD-1: errors instead of silently ignoring).
  • Add lint/lintFile/lintVirtual exports to browser.ts (pre-existing gap) #215 (browser lint exports): lint() and lintVirtual() added to browser.ts. All seven lint types and LINT_RULE_NAMES are now exported from both Node.js and browser entries.
  • options.ts second pass: value-type validation, indexed-access safety, and varsOpt type narrowing #213 (options.ts second pass): Deleted _CompileBackendOpts/_CheckBackendOpts stubs; replaced compileOpt/varsOpt with four typed per-surface builders; updated METHOD_KEYS witnesses; added BASEPATH_PASSTHROUGH set for compileFile/checkFile.

Changes

  • packages/mds/src/types.tsCheckOptions gains basePath?; FileOptions broken from extends CompileOptions (D-TS-02); new CheckFileOptions type; MdsNodeBackend.checkFile param changed.
  • packages/mds/src/util/options.ts — Four typed builders (compileSrcOpt, checkSrcOpt, fileCompileOpt, fileCheckOpt); BASEPATH_PASSTHROUGH set; updated METHOD_KEYS.
  • packages/mds/src/backend/native.ts — All four new builders wired up.
  • packages/mds/src/backend/wasm.ts — OD-1 guards on compile/check/lint; _WasmCompileInput extends Omit<CompileOptions,'basePath'> (D-TS-06).
  • packages/mds/src/node.tsfileBasePathError() helper; public compileFile/checkFile use Promise.reject(fileBasePathError()) for basePath (preserves sync-throw contract for unknown keys and pre-init).
  • packages/mds/src/browser.tslint() and lintVirtual() exported; all lint types re-exported.
  • packages/mds/__test__/ — 25 new test cases (U-OV-21–31, U-WB22–25, U-BR14–21); type-level matrix in __test__/types/.
  • packages/mds/tsconfig.types.json — New; runs type-level tests via tsc --noEmit as part of npm test.

Breaking Changes

  • FileOptions no longer extends CompileOptionsFileOptions is now a standalone interface; basePath is NOT present.
  • checkFile parameter changed from CheckOptions to CheckFileOptionsCheckFileOptions has only vars; basePath and source-map options are rejected.
  • WASM backend rejects non-null basePath on compile/check/lint — previously silently ignored; now throws mds::invalid_options.

Packaging note

src/index.ts is NOT in the package exports map and has no main/types fallback — dist/index.js is unreachable to consumers. This barrel serves internal repo tooling only. All public types and values are exported from dist/node.d.ts (Node) and dist/browser.d.ts (browser) via the "." exports-map entry in package.json (AC-P3-21).

Reviewer Focus Areas

  • src/node.ts: fileBasePathError()Promise.reject(fileBasePathError()) in public compileFile/checkFile; the async wrapWithFileOps guard also calls the same helper for the error path.
  • src/util/options.ts: BASEPATH_PASSTHROUGHassertKnownKeys skips basePath for compileFile/checkFile so the purpose-built error fires instead of the generic "unknown option key".
  • __test__/types/: @ts-expect-error guards are positive controls — an unused directive fails the build if basePath is ever added to a file-surface type (ADR-010 pattern).
  • __test__/options-validation.spec.mjs: U-OV-27 asserts byte-identical messages across native and WASM paths (avoids PF-007); U-OV-29 subprocess includes await init() for WASM path.

dean0x and others added 26 commits August 16, 2026 19:44
… option-type second pass (#213)

**#180 (live bug)** basePath was accepted by the unknown-option validator but
silently discarded before reaching the backend: the old forwarding builders
(compileOpt/varsOpt) never included it. Templates using @import/@extends with a
string-source call and a basePath option would resolve against the wrong directory
or fail silently.

Fix: add basePath to CompileOptions and CheckOptions; propagate through four new
typed builders (compileSrcOpt, checkSrcOpt, fileCompileOpt, fileCheckOpt).
The WASM backend now rejects non-null basePath on compile/check/lint with
mds::invalid_options (OD-1 — errors instead of silently ignoring). The file-surface
guard (compileFile/checkFile) returns Promise.reject(fileBasePathError()) so callers
using assert.rejects()/.catch() receive a proper rejection; unknown-key and pre-init
errors still throw synchronously (existing contract preserved per U-OV-12/U-B11).

**#215** lint() and lintVirtual() exported from the browser entry point.
All seven lint types (LintDiagnostic, LintFileOptions, LintFileReport, LintOptions,
LintResult, LintRuleName, LintSpan, RuleSeverity) and LINT_RULE_NAMES are now
available from both the Node.js and browser entries.

**#213** options.ts second pass: delete _CompileBackendOpts/_CheckBackendOpts stubs;
replace compileOpt/varsOpt with four typed per-surface builders; update METHOD_KEYS
witnesses to the real public types; add BASEPATH_PASSTHROUGH set for compileFile/
checkFile so assertKnownKeys lets basePath through to the purpose-built guard.

BREAKING: FileOptions no longer extends CompileOptions (D-TS-02); checkFile parameter
changed from CheckOptions to CheckFileOptions; WASM backend rejects non-null basePath
on string-surface methods instead of silently ignoring.

New: tsconfig.types.json + __test__/types/ type-level matrix test (consumer-node.ts,
consumer-browser.ts) with @ts-expect-error guards as positive controls (ADR-010).

Closes #180 #213 #215
…rror wrapper

- options.ts: extract `pickDefined<T>` helper to replace 24 lines of
  identical null-check + build + empty-guard boilerplate shared across
  compileSrcOpt, checkSrcOpt, and fileCompileOpt. Each builder is now a
  one-liner; fileCheckOpt is unchanged (already a one-liner with a
  distinct return type).

- node.ts: remove `throwFileBasePathError()` — a 4-line wrapper that
  existed only to call `throw fileBasePathError()`. Replace both call
  sites with the direct throw. TypeScript's control-flow analysis handles
  throw in async functions correctly; the never-typed helper was adding
  indirection with no benefit.

- browser.ts: expand single-line lintVirtual JSDoc to a multi-line block
  consistent with lint and the surrounding exported functions.

All 295 tests pass. TypeScript build and type-fixture (tsconfig.types.json)
both exit 0. Source hygiene gate exits 0. Rust scope unchanged.
P0 — Windows CI breakage (options-validation.spec.mjs)
Three new sites used `new URL(..., import.meta.url).pathname` to derive
filesystem paths. On win32 that yields "/C:/..." (leading slash, drive
letter) and it percent-encodes spaces on every platform, so path.join and
execFileSync's cwd receive unusable paths. windows-latest is in the `js`
CI matrix, and every other spec in this package already uses
fileURLToPath. Switched to fileURLToPath (TEST_DIR / PKG_DIR).

P1 — U-OV-27 was tautological
The test claimed to compare the compileFile/checkFile basePath rejection
message across the native and WASM backends, but node.ts's public file
methods short-circuit basePath BEFORE assertReady(), so neither side ever
reached a backend. Verified empirically: the subprocess reports
"BACKEND NEVER INITIALIZED". Both operands came from the same
fileBasePathError(), i.e. the test asserted X === X and could not detect
drift. Rewritten with the authoritative leg — wrapper vs the RAW napi
addon, which owns the canonical string — plus the backend-independence
leg and per-leg non-empty positive controls. Confirmed the new assertion
fails when the wrapper message is mutated.

P1 — U-BR17 assertion could not fail
`assert.ok(fileNames.length >= 0)` holds for an empty array. Replaced
with the assertions the plan specified: the entry key is present in the
report and carries an unused-variable diagnostic.

P1 — AC-P3-08 coverage gap
U-OV-29 covered only compile/check. Extended to compileFile/checkFile,
the interesting half: napi gates on property PRESENCE, so a builder that
forwarded `basePath: undefined` verbatim would throw on native but not on
WASM. Confirmed these legs reach the real WASM backend.

P2 — accuracy and reachability
- native.ts NapiAddon JSDoc claimed lintFile/lintVirtual accept basePath;
  napi rejects it on both.
- Recorded the OD-5 residual on BASEPATH_PASSTHROUGH: lintFile/lintVirtual
  deliberately emit the generic message and diverge from napi's.
- Corrected the consumer-node.ts comment that claimed invalid rule names
  are rejected (D-224-1 makes them a runtime warning) and added the real
  negative control: an invalid SEVERITY must be a type error.
- Exported SourceMapV3 from node.ts and browser.ts. MarkdownResult.sourceMap
  is typed as it, but it was only re-exported from the unreachable index.ts
  barrel, so consumers could hold the value without naming its type.
  Additive; covered by both type fixtures.
- Clarified the fileBasePathError JSDoc: napi never emits this message for a
  public call, so U-OV-27 is the only thing keeping the strings in lockstep.

Rust untouched (git diff --name-only ...crates/ Cargo.toml Cargo.lock is
empty, with packages/mds/ as positive control), so the WASM binary and the
850,000-byte guard are unaffected.
Amendment 4 of the Cross-PR Conflict Audit required comparing the browser
`lint` result against the Node `lint` result at RUNTIME for the same
fixture instead of asserting browser-local golden values. PF-007: per-surface
goldens each lock in their OWN value and cannot prove cross-surface parity.

U-BR-PARITY runs both surfaces on the same fixture with deepStrictEqual,
closing the fifth-surface parity gap. Also imports `nodeInit` so the
node backend is ready before the post-init describe block executes.

PR body simultaneously updated (via gh pr edit) to:
- Remove stale `throwFileBasePathError` references (removed in 0330597)
- Add Packaging note documenting index.ts unreachability (AC-P3-21)
`_WasmCompileInput` (Omit<CompileOptions,'basePath'> & { filename?,
modules? }) is already structurally assignable to FileOptions | undefined
because it carries the same vars/sourceMap/sourcesContent fields with
identical types and the extra optional properties are permitted on
non-fresh values.

The `options as Parameters<typeof fileCompileOpt>[0]` cast was therefore
redundant and obscured the signature of fileCompileOpt behind an
indirection that would have hidden any future incompatibility.

Co-Authored-By: Claude <noreply@anthropic.com>
All seven NapiAddon members now use a named option type:
  NapiCompileOpts, NapiCheckOpts, NapiFileCompileOpts, NapiFileCheckOpts,
  NapiLintOpts, NapiLintFileOpts (shared by lintFile/lintVirtual).

Previously checkFile used an inline { vars?: Record<string, unknown> }
which was inconsistent with the other six members and obscured the
file-surface / string-surface split. The new NapiFileCheckOpts type
mirrors CheckFileOptions (the public equivalent) and makes the split
visible in one place.

Co-Authored-By: Claude <noreply@anthropic.com>
…ile methods

- Move compileFile/checkFile basePath rejection from a **Note:** under Fixed
  into its own BREAKING subsection; the silent-success -> Promise.reject
  transition is a runtime breaking change on a published API (AC-P3-24).
- Restate the FileOptions/CompileOptions type reshape as source-compatible:
  before this PR CompileOptions had no basePath, so the old extends resolved
  to the same shape as the new standalone interface. The false migration step
  ('now needs an explicit mapping') is removed.
- Restate the checkFile parameter narrowing as source-compatible: CheckOptions
  was structurally equivalent to CheckFileOptions before this PR. The real
  break is the runtime rejection documented in the new subsection, which tsc
  does not flag for variable-typed callers.
- Update the BREAKING section header to lead with the file-method rejection.

Co-Authored-By: Claude <noreply@anthropic.com>
compile.spec.mjs U-C7 comment cited 'varsOpt' which was removed and
replaced by compileSrcOpt/fileCompileOpt (via pickDefined). Update the
comment to name the actual functions responsible for the != null filter.

source-map.spec.mjs U-SM6 block comment cited the adapter's compileOpt()
as the key-filtering mechanism. That function was replaced by assertKnownKeys(),
which throws on unknown keys. Update the comment to match.

Co-Authored-By: Claude <noreply@anthropic.com>
Browser usage section (#215):
- Line 38: "compile/check" → "compile/check/lint" (matches browser.ts:98)
- Browser import example: add lint and lintVirtual; show minimal usage
- Node-only callout: add lintFile to the list of unavailable methods

Unknown-option rejection paragraph (AC-P3-25/PF-015):
- Drop hard-coded "any of the seven public methods" count that drifts
- Qualify the format guarantee: basePath on compileFile/checkFile
  surfaces as a rejected promise with a purpose-built message, not a
  synchronous throw with an accepted-keys list (avoids PF-015)

Also removes stale (OD-1 — ) design-ID annotation from LintOptions
inline comment (stale-comment cleanup, same spirit as 7ef8369).

Co-Authored-By: Claude <noreply@anthropic.com>
The AC-P3-20 comment block contained two contradictions:

1. "basePath is NOT accepted on CompileOptions or CheckOptions from the
   browser entry — it IS in those types (D-TS-01)" — the two halves
   assert opposite things; the code at lines 28-30 (no @ts-expect-error)
   correctly shows basePath IS accepted at the type level, which is what
   AC-P3-20 mandates.

2. "Same @ts-expect-error guards as consumer-node.ts for FileOptions etc."
   — false; this fixture has no guard for FileOptions or CheckFileOptions
   because neither is exported from the browser entry. The only file-surface
   negative case present is LintFileOptions.

Replaced with a precise statement:
- basePath IS accepted at the TYPE level (same d.ts on both entries, D-TS-01)
- the WASM backend rejects a non-null basePath at RUNTIME
- FileOptions/CheckFileOptions have no guard because they are not exported
  from the browser entry — the only file-surface negative case is LintFileOptions

Also commit the pre-existing (uncommitted) variable-passing case that
verifies LintOptions is not assignable to LintFileOptions via a typed
variable (not only a fresh object literal) — correct and tsc-verified.

Co-Authored-By: Claude <noreply@anthropic.com>
- basePath propagation claim (#180 Fixed): was "all four per-surface option
  builders (compileSrcOpt, checkSrcOpt, fileCompileOpt, fileCheckOpt)".
  fileCompileOpt/fileCheckOpt deliberately NEVER forward basePath (D-TS-02/
  D-TS-05), and the BREAKING subsection 30 lines later says the opposite.
  Correct to: propagates to the backend for the string-source methods
  (compile, check); compileFile/checkFile deliberately exclude basePath.

- Lint-types count (#215 Added): "All seven lint types" followed by a list
  of eight. The list is correct; fix the prose count to "eight".

- Lint-types export scope (#215 Added): "exported from both the Node.js and
  browser entry points" overstates what changed — all eight were already
  exported from node.ts at the wave base. Only the browser entry is newly
  gaining them. Correct to "from the browser entry point as well as the
  Node.js entry point".

- Drop stale lintVirtual references in basePath migration guidance; native
  backend is the correct migration path.

Co-Authored-By: Claude <noreply@anthropic.com>
…ption forwarding

- Add `basePath?: never` to FileOptions, CheckFileOptions, and LintFileOptions
  so structural assignment from a string-surface type (basePath?: string) fails
  at compile time. This satisfies the AC-P3-20 variable-passing negative cases
  and aligns with `@ts-expect-error` guards in consumer-node.ts.
- Replace per-surface option builder functions (compileSrcOpt, checkSrcOpt,
  fileCompileOpt, fileCheckOpt, lintOpt, lintFileOpt) with a single forwardOpts
  helper keyed on METHOD_KEYS. Single authoritative source for which keys are
  forwarded per method name.
- Add makeFileBasePathError factory; change BASEPATH_PASSTHROUGH from ReadonlySet
  to ReadonlyMap<MethodName, factory> so adding a new file-surface method without
  an error factory is a TypeScript error at the Map literal.
- Convert U-OV-25/U-OV-26 from async assert.rejects to synchronous assert.throws:
  the basePath guard now throws before any I/O (per U-OV-32), and assert.rejects
  in Node.js v22 does not intercept synchronous throws.
- Fix compileFile/checkFile basePath cast: (options as unknown as { basePath?: string })
  is required because FileOptions.basePath?: never is structurally incompatible with
  CompileOptions for a direct cast.
…n node.ts

The forwardOpts import was added but never called directly in node.ts — all
option forwarding in node.ts is handled by the backend adapters (native.ts /
wasm.ts). Remove to keep the zero-warnings policy.

Also update a comment in the WASM-path checkFile that still referenced the
deleted fileCompileOpt helper; now names fileOpts and forwardOpts correctly.
…OV-34

The prior commit replaced per-surface option builders (compileSrcOpt,
fileCompileOpt) with a single forwardOpts helper, but the explanatory
comment in the U-OV-34 test block still referenced the old names. Update
the comment to reference forwardOpts to match the implementation.
Commit 225d5a6 removed the `forwardOpts` import from node.ts, but
`wrapWithFileOps.lintFile` calls `forwardOpts(options, 'lintFile')` to
forward vars/rules through METHOD_KEYS rather than accessing them
directly. Restores the import so the build compiles cleanly.
The prior commit (50b37c9) removed the entire explanatory comment above
`export { isMdsError, LINT_RULE_NAMES }` when it should have only dropped
the final sentence ("The browser entry gains it with the browser lint
surface; today it has no lint API to configure.") which became false once
browser.ts gained lint/lintVirtual/LINT_RULE_NAMES exports.

The first three lines — explaining why LINT_RULE_NAMES must be re-exported
from node.ts/browser.ts rather than index.ts (the exports map never routes
to dist/index.js, so index.ts is unreachable for consumers) — remain
accurate and are restored here.

Co-Authored-By: Claude <noreply@anthropic.com>
…omments

The MAINTENANCE NOTE and positive-control description in the forwarding-parity
test block still referred to the deleted per-surface helpers (pickDefined,
lintOpt, lintFileOpt). Update both comments to reference forwardOpts, which
is now the single forwarding path keyed on METHOD_KEYS.
…r METHOD_KEYS consistency

wasm.ts checkOpts still accessed options?.vars directly instead of going through
forwardOpts, making it inconsistent with the sibling compileOpts (which uses
forwardOpts('compileFile')). If CheckOptions gained a new key in METHOD_KEYS.checkFile
the drift would be silent.

node.ts lintFile (WASM path) called forwardOpts then manually copied individual
keys (forwarded?.vars, forwarded?.rules) rather than spreading forwarded. A new
key added to METHOD_KEYS.lintFile would be silently dropped by the manual copying.

Fix:
- checkOpts now uses forwardOpts(options, 'checkFile'), matching the compileOpts
  pattern, so METHOD_KEYS.checkFile is the single source of truth (avoids PF-004)
- lintFile WASM path spreads ...forwarded directly, matching lint/lintVirtual

No observable behaviour change: forwardOpts already filters to the same key set
that the manual code extracted. Identical results on all existing test inputs.

300/300 tests pass; tsc exits 0; source-hygiene gate clean.

Co-Authored-By: Claude <noreply@anthropic.com>
…kFile

checkOpts(options?: CheckOptions) was calling forwardOpts(options, 'checkFile'),
which uses METHOD_KEYS.checkFile — derived from CheckFileOptions, a different
interface from the function's own parameter type. This recreated the PF-004 /#180
silent-drop topology on the WASM check path: a future key added to CheckOptions
would update METHOD_KEYS.check (via the keysOf witness) but forwardOpts would
still select METHOD_KEYS.checkFile, silently dropping the new key while the
native backend honoured it.

Fix: call forwardOpts(options, 'check') so the key list is derived from
CheckOptions — the same interface as the parameter. METHOD_KEYS.check includes
basePath, but forwardOpts only includes keys whose value is != null; the call
site already throws mds::invalid_options before reaching checkOpts whenever
basePath is non-null, so basePath is never included in the forwarded object.

Applies PF-004. Verified by U-WB23 (basePath rejection) and U-WB25
(basePath: undefined accepted) — both continue to pass.

Co-Authored-By: Claude <noreply@anthropic.com>
…aims

Five confirmed review findings, all rooted in a sequencing issue: commit
891db04 wrote CHANGELOG and README before commit 3a9e2be added
`basePath?: never` to FileOptions, CheckFileOptions, and LintFileOptions —
leaving the docs contradicting the shipped types.

Changes:
- CHANGELOG.md: "return a rejected promise" → "throw synchronously" for
  compileFile/checkFile basePath rejection (node.ts:280,299 are sync throws).
- CHANGELOG.md migration paragraph: remove "runtime-only break" and
  "TypeScript does not flag it"; add sync-throw / try-catch guidance and
  pointer to compile-time compatibility notes below.
- CHANGELOG.md FileOptions compatibility: replace "source-compatible" with
  "compile-time break"; add shared-variable migration with destructuring
  example. basePath?: never means CompileOptions is not assignable to
  FileOptions (verified by consumer-node.ts:72-73 @ts-expect-error, ADR-009).
- CHANGELOG.md CheckFileOptions compatibility: same correction. CheckOptions
  is not assignable to CheckFileOptions for the same reason.
- README.md: "surfaces as a rejected promise" → "throws synchronously"; add
  try/catch guidance consistent with node.ts JSDoc.
- types.ts: clarify CheckFileOptions JSDoc to explain the compile-time break
  that `basePath?: never` intentionally introduces.

The LintFileOptions section already correctly describes this as a
compile-time break and is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
…eted symbols

compileSrcOpt, fileCompileOpt, and pickDefined were removed by commit 3a9e2be
and replaced with forwardOpts. The U-C7 comment was rewritten in that same
commit to reference those symbols, which were then immediately absent. Sibling
cleanup commits 7ef8369 and cfd84d5 fixed this staleness class in
options-validation.spec.mjs but missed this site.

Rewrite the comment to name forwardOpts, which filters via != null over
METHOD_KEYS — the current and only forwarding path for all seven public methods.

Co-Authored-By: Claude <noreply@anthropic.com>
…r, comment hygiene, and drop backend interfaces from public surface

Five confirmed findings from PR review:

[medium] Remove MdsBackend/MdsBaseBackend/MdsNodeBackend from node.ts export
type block and MdsBaseBackend from browser.ts. No public method signature
accepts or returns these types; AC-P3-21 scopes the requirement to option
types a consumer must name to call a public method. Publishing the backend
seam would make any future backend-method addition a semver-major event for
`implements MdsBaseBackend` callers. Update consumer-node.ts/consumer-browser.ts
to remove the now-removed imports and corresponding type tests.

[low] Add defense-in-depth basePath guard in wrapWithFileOps.compileFile and
wrapWithFileOps.checkFile (PF-004). The public wrappers already throw
synchronously, but a future internal caller bypassing the public layer would
previously get the silent-drop semantics that caused #180. The inner async
guard rejects as a promise rejection (consistent with their async contract).

[low] Fix lintOpts spread order in wrapWithFileOps.lintFile: put ...forwarded
first so filename and modules are always last. A future key in METHOD_KEYS.lintFile
named filename or modules could silently clobber WASM-internal values with the
old order (avoids PF-004).

[low] Rewrite stale LINT_RULE_NAMES comment that referenced deleted src/index.ts
in the present tense. Per the project leave-the-end-state rule, the comment
now states where the value is exported rather than explaining the history of why.

Also committed from pre-existing working-tree changes:
- forwardOpts<T>: generic T parameter so callers receive Partial<T> and are
  cast-free (options.ts). Removes all explicit casts in native.ts and wasm.ts.
- U-BR18: browser entry compile/check/lint reject basePath via WASM guard
  with PF-013 positive control (browser.spec.mjs).

Co-Authored-By: Claude <noreply@anthropic.com>
U-OV-27 (AC-P3-06 / findings 1+2): Add MDS_BACKEND=wasm subprocess leg.
The guard fires in node.ts before assertReady() so the WASM path produces
the same message today — but the byte-equality assertion is the PF-004
regression barrier: if the guard is ever refactored into the backend layer
a WASM-specific divergence is caught here before reaching production.
Test-plan item 5 mandated assert.strictEqual(nativeMsg, wasmMsg); this leg
was deleted in 3a9e2be alongside the wrapWithFileOps guards.

U-OV-24 (finding 3): Harden the self-referential fullOpts oracle.
Add an assert.ok(KEY_VALUES[k] !== undefined) guard inside fullOpts() so
a key added to METHOD_KEYS but absent from KEY_VALUES fails loudly at
lookup time rather than producing a confusing deepStrictEqual mismatch.
Add an independent hand-typed literal oracle for the lint surface so that
a systematic corruption of KEY_VALUES is caught by something other than
the self-referential loop (avoids PF-013).

U-OV-34 Part 2 (finding 4): Strengthen DEFAULT_COMPILE_OPTS identity check.
The identity assertion (compileOpts(undefined) === compileOpts({})) proves
a singleton is reused but not WHICH singleton. The new deepStrictEqual
against { filename: 'input.mds', modules: {} } ensures the fast path
reuses the documented default, not an incidental frozen object (AC-P3-22).
Commit 3a9e2be stripped D-TS-01, D-TS-02, and OD-1 from src/types.ts
but left the same class of identifier in other shipped files. Apply the
same treatment uniformly:

- node.ts: exported compileFile JSDoc had U-OV-12, U-OV-32, U-B11;
  checkFile JSDoc and inline guard comments had U-OV-33. The behavioral
  prose (sync-throw contract description) is preserved exactly — only
  the bare IDs are dropped.
- backend/wasm.ts: _WasmCompileInput JSDoc, compileOpts/checkOpts inline
  comments, and throwWasmBasePathError JSDoc cited D-TS-06, OD-1,
  AC-P3-10, and U-WB22. The three OD-1 callsites in createWasmBackend
  are also cleaned up for consistency.
- util/options.ts: METHOD_KEYS JSDoc cited U-OV-14; makeFileBasePathError
  JSDoc cited U-OV-27; BASEPATH_REJECTORS comment cited OD-5, U-OV-7,
  U-OV-13; forwardOpts JSDoc cited D-TS-03/D-TS-05.

No logic changes. Zero TypeScript errors; source-hygiene gate clean.

Co-Authored-By: Claude <noreply@anthropic.com>
…omment

options.ts: "Nothing enforces the two strings staying in sync except\nKeep
the two in lockstep" was a sentence fragment — 'except' had no object and
the claim was factually wrong (U-OV-27 does enforce parity). Replaced with
a direct imperative that names the test.

wasm.ts checkOpts: the 8-line comment restated the identical rationale already
given for compileOpts directly above it. Reduced to three lines that reference
compileOpts and state only the check-specific detail (basePath exclusion).

No behavior change; TypeScript build and source-hygiene gate both pass.
Two test-side gaps found in the PR3 9-pillar self-review.

1. Four basePath assertions were backend-dependent (plan section 0, OD-1).
   U-OV-8, U-OV-9, U-OV-15 and U-OV-16 asserted a bare
   `assert.doesNotThrow(() => compile('Hello\n', { basePath: '.' }))`.
   Under OD-1 the WASM backend now rejects a non-null basePath, so on any
   machine that falls back to WASM (node.ts:225) these failed with a
   misleading "wrapper reconciliation" message — misattributing correct
   WASM behaviour to a wrapper regression. Verified: under MDS_BACKEND=wasm
   both calls throw mds::invalid_options today.

   Replaced with assertWrapperAcceptsBasePath(), which asserts the property
   the tests actually exist for — that assertKnownKeys no longer intercepts
   basePath on the string surface (#180) — on BOTH backends. The WASM branch
   is not a free pass: it requires the error to be the WASM-backend rejection
   and explicitly NOT the generic `unknown option key` form (avoids PF-013).
   Confirmed passing under both the default (native) and MDS_BACKEND=wasm runs.

2. New U-OV-35 pins the checkFile WASM forwarding invariant.
   On the WASM backend, checkFile routes through prepareFileArgs -> fileOpts,
   which forwards via METHOD_KEYS.compileFile rather than METHOD_KEYS.checkFile.
   That is correct only while checkFile's keys are a subset of compileFile's.
   Should CheckFileOptions ever gain a key FileOptions lacks, assertKnownKeys
   would accept it while the WASM path silently dropped it — the exact #180
   validated-then-discarded bug class, on one backend only, invisible to a
   native-only run (avoids PF-004). Carries its own positive controls so the
   subset assertion cannot pass vacuously.

Also refreshed the stale file docblock (claimed "U-OV-1 through U-OV-20";
the file now runs through U-OV-35) and recorded that basePath behaviour is
NOT backend-agnostic.

No production code changed.
@dean0x
dean0x marked this pull request as ready for review August 16, 2026 19:45
@dean0x
dean0x merged commit 4c13ecb into wave/v0.4.0-wave1 Aug 16, 2026
16 checks passed
@dean0x
dean0x deleted the ticket/pr3-ts-public-surface branch August 16, 2026 19:53
dean0x added a commit that referenced this pull request Aug 16, 2026
…89 [#209]

AC-209-04 contained an absolute prohibition on `allow(deprecated)` in
crates/*/src/ that was already violated on the wave base: config.rs lines
287 and 289 carry #[allow(deprecated)] inside the compiled doctest for
LintConfig::from_rules (itself deprecated since #302, before this branch).

The audit grep (`allow(deprecated)|expect(deprecated)` over crates/*/src/)
therefore returns two hits the original criterion did not account for. A
reviewer running the audit could not distinguish a genuine suppression leak
from a legacy carve-out.

Fix (applies PF-015 — absolute completeness claim is a liability):
- Restate AC-209-04 with an enumerated whitelist of exactly three permitted
  locations: fix.rs >1006, crates/mds-core/tests/, and config.rs:287,289.
- Add a provenance note: (c) is pre-existing from #302, confirmed via
  `git show wave/v0.4.0-wave1:crates/mds-core/src/lint/config.rs`.
- Update the test plan section 4 expected outcome to match, including the
  confirmation command reviewers should run to verify the carve-out.

No source code changed. Only the plan document is updated.
Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant