Skip to content

Latest commit

 

History

History
644 lines (472 loc) · 79 KB

File metadata and controls

644 lines (472 loc) · 79 KB
title m-stdlib — discoverability & tooling implementation tracker
status live (2026-05-08; Wave A not yet started; all rows = not-started)
authority this file is the canonical "what's done / in flight / queued" view for the discoverability & tooling plan. Any commit that touches a task below MUST update the row's Status and append to the task's narrative section in the same commit.
plan docs/plans/discoverability-and-tooling-plan.md (the design doc; this tracker drives the work items)
sibling docs/tracking/module-tracker.md (the module-level tracker; deferred decisions D1/D2/D3 there originated in the same plan)
audience anyone executing or proposing work against the discoverability plan, including AI agents asked to "complete phase X"
created 2026-05-08
last_modified 2026-05-10
revisions 22
doc_type
STATUS

m-stdlib — discoverability & tooling implementation tracker

This tracker drives the work in docs/plans/discoverability-and-tooling-plan.md. The plan is the why and what; this file is the who/when/where.

Tasks are grouped into four waves (Wave A → D) matching the plan's § 8 phasing. A wave's downstream consumers (m-cli, VS Code, AI skills) depend on its upstream artefact (the manifest from Wave A), so the order matters; within a wave, most tasks can run in parallel unless a Deps cell says otherwise.


How to use this tracker

When asked "based on the tracking table, complete phase X" (e.g. "complete Wave A"), follow this procedure:

  1. Read the plan section matching the phase. Each Wave maps to a plan § — e.g. Wave A is plan § 3 + § 8 Wave A. The task rows below include the precise plan-§ pointers.
  2. Walk the tracker rows for the requested wave. For each row whose Status is not-started or in-progress:
    • Verify every Dep listed is done. If a Dep is not done, work that Dep first (or block on it).
    • Read the narrative section for that Task ID further down this file. The narrative carries any prior progress notes, blockers, and design choices already pinned.
    • Read the file(s) listed in the Paths column. The plan § pointer in Notes gives the design rationale.
  3. Execute the task following project conventions (TDD-first, make check, m fmt, m lint --error-on=error, per-module acceptance gate from module-tracker.md § "Per-module acceptance gate"). For tasks landing in m-cli rather than m-stdlib (Wave B), the work lands in ~/projects/m-cli/ and m-stdlib remains untouched.
  4. Update this tracker in the same commit:
    • Flip the row's Status (not-startedin-progressdone).
    • Append a dated bullet to the task's Progress log in the narrative section, summarising what landed, the commit SHA(s), and any follow-ups.
    • If the task is done, set the row's Status, fill in the Tag column with the m-stdlib release that carries it (or "n/a" for m-cli/VS Code/skill artefacts), and check the Acceptance condition is genuinely met.
  5. Cross-tracker hygiene. If the work also lands a module-level change (e.g. backfilling tags inside src/STDxxx.m is also a change to that module), add a one-line note in module-tracker.md Table 1. The discoverability tracker stays focused on the cross-cutting metadata/tooling work; per-module behaviour stays in the module tracker.

Cross-references

Everything below is the canonical pointer set an agent needs to do the work without further orientation.

Within m-stdlib (this repo)

Concern Path
Plan / design doc docs/plans/discoverability-and-tooling-plan.md
Module tracker (sibling) docs/tracking/module-tracker.md
Toolchain findings docs/tracking/TOOLCHAIN-FINDINGS.md
Released-module catalogue docs/modules/index.md
Per-module long-form docs docs/modules/*.md
User guide docs/guides/users-guide.md
TDD guide docs/guides/m-tdd-guide.md
M routines (the source of truth for the manifest) src/STD*.m
Tests tests/STD*TST.m
Build / generator scripts (target dir for new tools/) tools/
Manifest output (will not exist until WA4 lands) dist/stdlib-manifest.json
Errors output (will not exist until WA7 lands) dist/errors.json
Release notes changelog.md
Project conventions CLAUDE.md

Outside m-stdlib (downstream consumers)

Concern Path
m-cli source (Wave B target) ~/projects/m-cli/
m-cli command source dir ~/projects/m-cli/src/cmd/ (existing pattern; new doc.m, search.m, manifest.m, examples.m, errors.m land here)
m-cli lint rules dir (WA3 target) ~/projects/m-cli/ (find lint subdir; rule M-DOC-001 lands alongside other M-DOC-* rules if any, or as a new family)
tree-sitter-m parser (informs WA4 design; do NOT bind to it for Wave A) ~/projects/tree-sitter-m/
AI skill output dir (Wave D) ~/claude/skills/m-stdlib/
AI memory home ~/claude/memory/
VS Code extension scaffold (does not yet exist; Wave C creates it) ~/projects/m-stdlib-vscode/ (recommended new repo) or ~/projects/m-cli/vscode/ (if shared) — decide in WC1

External references

Concern Pointer
JSDoc tag spec (informs WA1 grammar) https://jsdoc.app/
rustdoc reference (informs WA1, search-index design) https://doc.rust-lang.org/rustdoc/
godoc (informs WB1 m doc UX) https://go.dev/doc/comment

Summary table

Status legend. not-started · in-progress · blocked · done · deferred (tracked elsewhere; left in the table only as a pointer).

Phase legend. WA = Wave A (m-stdlib: grammar + manifest). WB = Wave B (m-cli: m doc family). WC = Wave C (VS Code extension). WD = Wave D (AI surface).

Repo legend. stdlib = ~/projects/m-stdlib/ · m-cli = ~/projects/m-cli/ · vscode = VS Code extension repo (TBD in WC1) · skill = ~/claude/skills/m-stdlib/.

Task ID Status Phase Seq Repo Title Deps Effort Acceptance Plan § Paths
WA1 done WA 1 stdlib Specify M-doc tag grammar (@param / @returns / @raises / @example / @since / @stable / @see / @deprecated / @internal) and synopsis-line rule 0.5d New docs/guides/m-doc-grammar.md reviewed; sample worked example shown using STDJSON.parse § 3.1 docs/guides/m-doc-grammar.md (landed 2026-05-08)
WA2 done WA 2 stdlib Backfill structured doc tags across all public labels in src/STD*.m (incl. @stable tier annotation per § 3.6) WA1 3–4d Every public label in src/ has @params matching the formal-list, @returns for extrinsics, @raises for any $ECODE it sets, ≥1 @example, @since, @stable. Manifest generator (WA4) parses src/ without diagnostics. § 3.1, § 3.6 src/STD*.m (all 32 routines); ALL DONE (32/32)
WA3 done WA 3 m-cli Add lint rule M-DOC-001 warning on public labels missing required tags (warn-only at first) WA1 0.5–1d m lint raises M-DOC-001 (severity warn) on a public label whose ; doc: block is missing @returns (extrinsic) or has a @param not in the formal-list. No errors emitted yet. § 3.1 (acceptance gate) ~/projects/m-cli/src/m_cli/lint/_doc.py (new), ~/projects/m-cli/tests/test_lint_doc.py (new), m-cli commit f97b849 pushed
WA4 done WA 4 stdlib Implement tools/gen-manifest.py — hand-rolled parser — that walks src/STD*.m and emits dist/stdlib-manifest.json + dist/errors.json per the § 3.2 schema WA1 1–2d Running make manifest produces dist/stdlib-manifest.json; all public labels present with signature, source.file/line, plus tag-derived fields (params, returns, raises, examples, since, stable, see_also) populated when WA2 backfill provides tags. § 3.2, § 11.4 tools/gen-manifest.py (new, ~370 LoC), dist/stdlib-manifest.json (new, generated), dist/errors.json (new, generated), Makefile (manifest + manifest-check targets)
WA5 done WA 5 stdlib CI gate: regenerate manifest in CI, fail on diff against committed dist/stdlib-manifest.json WA4 0.5d CI workflow runs make manifest-check; passes today, fails if a label changes without manifest re-gen. § 3.2 (acceptance gate) .github/workflows/ci.yml (step add), Makefile (already had manifest-check)
WA6 done WA 6 stdlib Add YAML frontmatter (module / tag / phase / stable / since / synopsis / errors / labels / conformance / see_also) to every docs/modules/stdXXX.md 0.5d Every per-module markdown has frontmatter parsing cleanly as YAML. Field values agree with the manifest where overlapping (verified by a small check script, not gated yet). § 3.3 docs/modules/std*.md, tools/write-module-frontmatter.py (new), Makefile (frontmatter target)
WA7 done WA 7 stdlib Generate dist/errors.json (inverted index of U-STD* codes → producing module + labels) as a derivative of the manifest WA4 0.25d make manifest also writes dist/errors.json containing every U-STD* code with its origin module + labels. § 3.5 tools/gen-manifest.py (covers WA7 too), dist/errors.json (generated)
WA8 done WA 8 stdlib Cut a release tag carrying Wave A; update docs/modules/index.md to link the manifest + errors registry WA4, WA5, WA6, WA7 0.5d A new tag (≥ v0.5.0 or per CHANGELOG decision) ships with manifest + errors.json + frontmatter; docs/modules/index.md carries a "Machine-readable surface" subsection linking both. § 8 Wave A gate CHANGELOG.md ([v0.5.0] heading), docs/modules/index.md (Machine-readable surface section), git tag v0.5.0 (local; not pushed)
WB1 done WB 1 m-cli m doc <symbol> — module overview, single-label, fuzzy lookup; reads dist/stdlib-manifest.json from the resolved m-stdlib install at runtime WA4 1–2d m doc STDJSON, m doc STDJSON.parse, and m doc parse all return correct godoc-style output within ~100ms cold. § 4.1 ~/projects/m-cli/src/m_cli/doc/{cli,lookup,format}.py (new+rewrite), ~/projects/m-cli/tests/test_cli_doc_lookup.py (new), m-cli commit 0024a72 pushed
WB2 done WB 2 m-cli m doc --json and m doc --short flags WB1 0.5d --json emits the raw manifest entry; --short emits one-line synopsis. Both stable for scripting. § 4.1 landed in the same commit as WB1 (0024a72) — format.py carries format_*_short and format_*_json formatters; cli.py wires the flags
WB3 done WB 3 m-cli m search <query> — full-text fuzzy search over manifest synopsis + description + example WA4 0.5–1d m search "json parse" returns ranked module.label — synopsis lines; matches against case-insensitive substrings in synopsis, description, examples. § 4.2 ~/projects/m-cli/src/m_cli/doc/search.py (new), m-cli commit 6542e73 pushed
WB4 done WB 4 m-cli m manifest, m examples <module>, m errors thin wrappers WA4, WA7 0.5d All three commands emit useful output piped from the manifest / errors registry. § 4.3, § 4.4, § 4.5 ~/projects/m-cli/src/m_cli/doc/{manifest,examples,errors}.py (all new), m-cli commit 6542e73 pushed
WC1 done WC 1 vscode VS Code extension v0.1: hover, goto-def, completion driven by dist/stdlib-manifest.json (no LSP) WA4 2–3d Extension activates on .m files; hover on ^STDJSON and parse^STDJSON shows synopsis + signature; goto-def jumps to src/STDJSON.m:L; completion suggests ^STD* modules and labels. § 5.1 new repo rafael5/m-stdlib-vscode initial commit 1c15a05 pushed
WC2 done WC 2 vscode Snippet pack for canonical patterns: STDASSERT suite skeleton, STDFIX with wrapper, STDLOG kv line, STDJSON parse-then-walk WC1 0.5d Typing stdassert-suite (etc.) in a .m file expands to the canonical idiom. § 5.1 rafael5/m-stdlib-vscode snippets/m.json + package.json contributes.snippets; commit 7d0723b pushed
WD1 done WD 1 skill AI skill at ~/claude/skills/m-stdlib/SKILL.md + manifest-index.md + patterns.md + error-codes.md, generated from the manifest WA4 1d Skill files exist; tools/gen-skill.py regenerates them deterministically; make skill-check gates drift. § 6.1 tools/gen-skill.py (new), tools/skill-patterns.md (new static input), dist/skill/*.md (generated, committed), Makefile (skill / skill-check / skill-install targets), ~/claude/skills/m-stdlib/*.md (installed live), ~/claude/CLAUDE.global.md (skill registry entry)
WD2 done WD 2 stdlib Doctest generator tools/gen-doctests.py — emits tests/STDxxxDOCTST.m from self-contained Pattern-A @example lines so doc examples must execute WA1, WA2 1d 15 generated suites, 51/51 doctests pass under m test; make doctest-check gates drift; full make test (47 suites, 2562 assertions) green. § 3.4 tools/gen-doctests.py (new), tests/STD*DOCTST.m (15 files, generated + committed), Makefile (doctest / doctest-check / doctest-run targets)

Deferred / out of scope (linked from module-tracker.md)

Tracker ref Subject Status
module-tracker.md D1 HTML / GitHub Pages docs site (plan § 5.2) deferred — revisit when a non-maintainer adopts m-stdlib or a second contributor lands a module
module-tracker.md D2 @stable SemVer CI gate (manifest-diff at HEAD vs last tag) annotation ships in WA2; gate deferred — revisit when v1.0 is being planned or a non-maintainer consumer adopts m-stdlib
module-tracker.md D3 Replace hand parser in tools/gen-manifest.m with tree-sitter-m hand parser ships in WA4; revisit when tree-sitter-m hits stable v1 or generator needs language features beyond labels + doc comments

Per-task narratives

Each Task ID has a section below. Keep narratives short during planning; expand them as work happens. The format is:

### <Task ID> — <short title>

**Status.** <not-started | in-progress | blocked | done>
**Goal.** One paragraph from the plan section.
**Approach.** Bullets — how to attack this.
**Acceptance.** Same string as the table cell, expanded if useful.
**Out of scope.** Anything to deliberately not do here.
**Progress log.**
- <YYYY-MM-DD> — <what landed; commit SHA; any follow-ups>

Wave A — m-stdlib grammar + manifest

WA1 — Specify M-doc tag grammar

Status. done (2026-05-08).

Goal. Write docs/guides/m-doc-grammar.md defining the structured-tag extension to the existing ; doc: convention. Tags must be optional (existing prose-only blocks remain valid); the tag set is small (@param, @returns, @raises, @example, @since, @stable, @see, @deprecated, @internal); the first tag-free ; doc: line is the synopsis (consumed by m doc --short).

Approach.

  • Borrow JSDoc verbatim where possible — the tag names above are all standard JSDoc.
  • Show one fully-tagged worked example for parse^STDJSON exactly as in plan § 3.1.
  • Define @stable values: experimental / stable / deprecated.
  • Note the synopsis-line rule (godoc convention: first sentence is the summary).
  • Flag the parsing contract for downstream tools (manifest generator + lint rule must accept the same grammar).

Acceptance. New docs/guides/m-doc-grammar.md exists; reviewed for clarity; the worked example matches what WA4's generator will be expected to extract.

Out of scope. Not changing the existing ; doc: prose convention; not specifying a Markdown subset for tag bodies (keep them plain text for v1).

Progress log.

  • 2026-05-08docs/guides/m-doc-grammar.md landed (10 sections, ~250 lines). Defines the nine tags, the synopsis line rule (godoc-style; inline comment on label line preferred, first prose sentence as fallback), the lexical model (continuation lines append; empty ; doc: lines terminate a tag's body), and a worked parse^STDJSON example showing both the tagged source form and the derived manifest entry. § 7 pins the parsing contract three downstream tools must agree on (tools/gen-manifest.m/WA4, M-DOC-001 lint/WA3, all Wave B/C/D consumers via the manifest). § 8 tabulates edge-case behaviour (unknown tags, duplicate @param, etc.). Key calibrations vs the plan §3.1 sketch: (a) extrinsic-vs-procedure distinction is inferred statically from quit <expression> rather than from formal-list parens; (b) @example blocks collapse consecutive lines into one snippet (more idiomatic for STDASSERT-style multi-line examples); (c) @internal excludes a label from the manifest entirely rather than merely tagging it; (d) free-form description block can sit either before or after the tag block — recommended placement is tags-first, prose-second. No source files (src/STD*.m) touched — that is WA2's scope. WA3 (M-DOC-001 lint rule) and WA4 (tools/gen-manifest.m) now have a normative spec to implement against and are unblocked.

WA2 — Backfill structured doc tags

Status. done (32/32 modules — all src/STD*.m backfilled as of 2026-05-08).

Goal. Every public label across all 30+ src/STD*.m routines gets @params, @returns, @raises, ≥1 @example, @since, @stable. The existing prose ; doc: lines remain as continuation.

Approach.

  • One module per session; treat the v0.4.0 module-tracker order as a natural sequence.
  • Cross-check @param names against the formal-list; cross-check @raises against the routine-header's documented $ECODE values.
  • For @since, read from CHANGELOG.md (per-module first-shipped tag).
  • For @stable, default to stable for any label that's been shipped in a tagged release; experimental for anything still in flight; deprecated for nothing yet.
  • Run make check per module before moving on.

Acceptance. Every public label tagged. Once WA4 ships, the manifest generator parses src/ without warnings.

Out of scope. Not changing label signatures or behaviour. Not deprecating anything in this pass — @stable=deprecated is reserved for a future intentional deprecation.

Progress log.

  • 2026-05-08 — first 3 modules done (STDB64, STDHEX, STDFMT). Pattern validated end-to-end: grammar guide → tagged source → manifest extraction → manifest-driven frontmatter → errors registry. The 3 modules were chosen as the smallest/simplest of the 32 (5 + 4 + 2 public extrinsics) and as a representative cross-section: STDB64 and STDHEX are pure data-transform modules with no $ECODE raises (so @raises is empty); STDFMT is the first real test of @raises plumbing because it sets four distinct ,U-STDFMT-*, codes via a raise helper. Manifest globals: 519 → 502 public labels (the 13 internal helpers across these 3 modules — alpha, urlAlpha, encodeImpl, decodeImpl in STDB64; alpha, alphaU, encodeImpl in STDHEX; render, expand, lookup, apply, parseSpec, convert, toBase, pad, repeat, raise in STDFMT — all correctly excluded after @internal). dist/errors.json populated for the first time with the four STDFMT codes correctly tied to f and fn. Frontmatter regenerated with --force to pick up the trimmed labels lists and the populated errors. Style/lint impact: zero. make fmt-check clean (65/65 unchanged); make lint --error-on=error reports 0 errors (8480 findings total, all S/W/I, pre-existing — none from these edits). Per-label tags applied:
    • STDB64: encode/decode/urlencode/urldecode/valid@param data|text (string, byte-string semantics noted), @returns string|bool, @example (carried over from the prose Example: lines), @since v0.0.2, @stable stable, @see cross-refs to siblings. No @raises (module never sets $ECODE).
    • STDHEX: same shape — encode/encodeu/decode/valid with @since v0.0.2. The two encode entries point at each other via @see.
    • STDFMT: f/fn get @param for every formal-list arg (9 + 2 respectively), @returns string, all four @raises U-STDFMT-* codes (since the raise() helper is reachable transitively from both extrinsics), @example, @since v0.0.3, @stable stable, mutual @see.
  • Style observations carried into future modules: (1) the existing inline-Example: prose lines are removed from the description block once @example carries the same content (avoids duplication; matches the grammar guide's recommended style). (2) Internal helpers' "Internal — " prose prefix is dropped when @internal is added (the tag carries the same signal). (3) Tag-bodies are aligned at column position 24 within the ; doc: body for readability; not enforced by the grammar but a nice convention. (4) For modules with 9-position positionals (STDFMT.f), all nine @param lines are emitted even though the trailing ones may be optional in practice — explicit > implicit.
  • What's left for WA2: 29 of 32 modules. Recommended ordering (by simplicity): STDARGS, STDUUID, STDLOG, STDDATE, STDCSV, STDOS, STDSEMVER, STDSTR, STDTOML, STDCACHE, STDPROF, STDSNAP, STDENV, STDSEED, STDFIX, STDMOCK, STDFS, STDCSPRNG, STDCOLL, STDURL, STDMATH, STDXFRM, STDCRYPTO, STDCOMPRESS, STDHTTP, STDASSERT, STDREGEX, STDXML, STDJSON. The last 4 (STDASSERT, STDREGEX, STDXML, STDJSON) carry the most labels and most-complex error surfaces; do them last when the workflow is fully grooved.
  • 2026-05-08 — WA2 complete (batches 5–11 land remaining 20 modules). Final 20 modules backfilled in five workflow-batches plus a bulk-transform pass for the largest three:
    • Batch 5 (STDCACHE, STDPROF, STDSNAP) — STDSNAP gains @raises U-STDFS-OPEN-FAIL for the transitive STDFS dependency.
    • Batch 6 (STDENV, STDSEED, STDFIX) — STDSEED's 6 distinct error codes correctly tied to load(), validate(), loadJson() per the transitive raise() chain. STDFIX raises U-STDFIX-EMPTY-TAG / U-STDFIX-UNREGISTERED-TAG.
    • Batch 7 (STDMOCK, STDFS, STDCSPRNG) — STDFS captures the full byte-faithful I/O error matrix: NOT-WIRED + OPEN-FAIL + READ-TRUNCATED + REMOVE-FAIL across writeBytes/appendBytes/readBytes plus the v0.3.0 text-I/O paths.
    • Batch 8 (STDCOLL, STDURL, STDMATH) — STDCOLL is the largest module backfilled (48 public labels: Set/Map/Stack/Queue/Deque/Heap/OrderedDict). All 13 STDURL internal helpers excluded via @internal.
    • Batch 9 (STDXFRM, STDCRYPTO, STDCOMPRESS) — Phase 3 callout modules with rich @raises surfaces (CALLOUT-MISSING / DIGEST-FAIL / HMAC-FAIL / BAD-LEVEL / LIBZ-FAIL / LIBZSTD-FAIL).
    • Batch 10 (STDHTTP, STDASSERT) — STDASSERT's silent correctly marked @internal after the helper's own ; doc: block already labelled itself "Internal helper".
    • Final batch (STDREGEX, STDXML, STDJSON) — these three carry ~140 internal helpers that needed @internal tagging. Bulk transform via Python sed-like one-liner (re.sub(r'(\\s+); doc: Internal — (.)([^\n]*)', r'\\1; doc: @internal\\1; doc: \\u(\\2)\\3', text)) handled the prose-prefix cases in one shot; ~8 remaining internal helpers without "Internal —" phrasing got individual edits. Then 31 public labels backfilled with full tag set.
  • Aggregate at WA2 close: 519 → 284 public labels in the manifest (235 internal helpers correctly excluded). errors.json: 0 → 43 distinct error codes across all 32 modules. Module-level error counts validate: STDARGS 5, STDSEED 6, STDFS 4, STDCRYPTO 3 + 3 (digest/hmac shared), STDCOMPRESS 4, STDFMT 4, STDDATE 3, STDLOG 3, STDCSPRNG 3, STDREGEX 3, STDFIX 2, STDJSON 2, STDCSV 1, STDENV 1, STDSNAP 1 (transitive U-STDFS-OPEN-FAIL).
  • Style/lint impact: fmt-check clean (65/65 unchanged). Final lint: 0E 567W 7677S 272I (vs starting 0E 567W 7638S 275I). +39 S findings, -3 I — these are M-MOD-001 line-length warnings on a few @returns / @see lines that exceed the 100-byte limit. All S-severity, non-gating, comparable to the pre-existing 7638 such findings project-wide. Zero new errors introduced.
  • Frontmatter regenerated (make frontmatter --force) so all 32 docs/modules/std*.md files have up-to-date labels and errors lists.
  • Wave A status now ALL DONE in this repo (WA1 + WA2 + WA4 + WA5 + WA6 + WA7 + WA8 = 8/8). Cross-repo follow-ons remain: WA3 (m-cli M-DOC-001 lint rule), Wave B (m-cli m doc family), Wave C (VS Code extension), Wave D (AI skill).
  • 2026-05-08 — fourth batch (STDSEMVER, STDSTR, STDTOML). Three "predicate-style" P4-wave modules — no $ECODE raises anywhere; failures surface as "" or 0 returns instead. Public-label deltas: STDSEMVER 18→9 (10 internal: validTriple/validPrerelease/validBuild/validNumericId/validPreId/validBuildId/comparePrerelease/comparePreId/matchesOne/matchesCaret); STDSTR 13→13 (no internal helpers — STDSTR is a flat utility module); STDTOML 13→4 (9 internal: parseTable/parsePair/decodeValue/decodeString/decodeInteger/decodeFloat/trimWs/stripTrailingComment/validBareKey). Aggregate: 466→449 public labels. dist/errors.json unchanged at 16 codes — these three modules don't raise. Style observation: STDSTR is the first all-public module backfilled; it has no @internal tags at all. Net lint delta: +1S, -1I, essentially zero. fmt-check clean (65/65 unchanged).
  • 2026-05-08 — third batch (STDLOG, STDCSV, STDOS). Same workflow. Public-label deltas: STDLOG 17→8 (9 internal: collect/emitLine/emitJson/writeLine/kvVal/needsQuote/escape/replace/levelNum); STDCSV 6→4 (2 internal: emit/dq); STDOS 12→11 (1 internal: replaceDouble). Aggregate: 478→466 public labels. dist/errors.json now carries 16 codes (was 12): +3 STDLOG (INVALID-LEVEL → LEVEL; INVALID-SINK → SINK; INVALID-FORMAT → FORMAT — each tied to its single direct setter, since these don't go through a raise() thunk) plus +1 STDCSV (OPEN-FAIL → parseFile + writeFile). Net lint delta: ZERO0E 567W 7652S 274I unchanged from the prior batch. The pattern is now grooved: tag bodies stay under the 100-byte M-MOD-001 limit, prose continuation handles the longer descriptions. Style observation carried forward — STDLOG's 5 level-emitter stubs (DEBUG/INFO/WARN/ERROR/FATAL) all share the same 11-formal positional pattern, so DEBUG carries the full @param list and the other four emitter docs reference it via ; doc: @param k1 string key 1 (optional; same shape through k5/v5) to avoid 50 lines of redundant tag boilerplate. The manifest still records the formal-list correctly (extracted from the label-line itself, not the @param count); future m doc UX can render the cross-ref or expand it as desired.
  • 2026-05-08 — second batch (STDUUID, STDARGS, STDDATE). Same workflow as the first batch. Public-label deltas: STDUUID 8→5 (randomHex/toHex/unixMs excluded); STDARGS 15→7 (8 internal helpers excluded — initDefaults/checkPositionals/walk/tokenize/handleLong/handleShort/assignPositional/raise); STDDATE 20→7 (13 internal helpers excluded — the Howard-Hinnant calendar primitives, the strftime/strptime tz formatters, and padL). Aggregate: 502→478 public labels across the manifest. dist/errors.json now carries 12 codes (was 4): the 4 STDFMT codes from batch 1, plus all 5 STDARGS codes (UNKNOWN-ACTIONaddflag; UNKNOWN-FLAG / UNKNOWN-SUBCOMMAND / MISSING-VALUE / MISSING-POSITIONALparse), plus all 3 STDDATE codes (BAD-HOROLOGfromh/strftime/add; BAD-ISOtoh/strptime; BAD-DURadd). The transitive @raises discipline validated: parse^STDARGS correctly captures all four codes that the helper chain (walk/handleLong/handleShort/checkPositionals) raises through the shared raise() thunk, even though parse itself only calls walk directly. Style notes carried forward: a few new @returns and @see lines exceed the 100-byte M-MOD-001 limit — kept as-is because (a) the warnings are S-severity, not gating; (b) splitting them across continuation lines reduces readability; (c) the trend is project-wide (7652 such findings pre-existing). Net lint delta this batch: 0E unchanged; +14S (style); -1I (info). make fmt-check clean (65/65 unchanged).

WA3 — Lint rule M-DOC-001

Status. done (2026-05-08; m-cli commit f97b849 pushed to origin).

Goal. Warn on public labels missing required tags. Lives in m-cli (where lint rules live), not m-stdlib.

Approach.

  • Locate the existing m-cli lint rule directory; follow the pattern of an existing M-* rule.
  • Severity = warn (not error) at first; promote to error in a later release once WA2 backfill is complete and stays clean.
  • Required tags by label form: extrinsic (label() ; ...) needs @returns; procedure (label(args) ; ...) needs @param matching every formal arg; both need ≥1 @example if @stable=stable.

Acceptance. m lint against m-stdlib emits zero M-DOC-001 errors after WA2; emits warnings only on labels not yet backfilled.

Out of scope. Stricter rules (M-DOC-002+) — defer to a later pass. The acceptance-gate threshold change to --error-on=warning for M-DOC-* is also out of scope until backfill is complete.

Progress log.

  • 2026-05-08 (m-cli commit f97b849) — Landed as the first member of the M-DOC-NN family. New file src/m_cli/lint/_doc.py (~290 LoC) implements the rule following the same _label_body_extents + tag-block parser pattern that tools/gen-manifest.py in m-stdlib uses, so the lint rule and the manifest generator agree byte-for-byte on which ; doc: blocks belong to which label. New tests file tests/test_lint_doc.py (17 cases) covers each diagnostic shape (missing @param / extra @param / missing @returns / undeclared @raises / missing @since / missing @stable / unknown @stable level / @stable deprecated without @deprecated) plus the rule's profile/tag membership (modern + doc tags). New side-effect import in lint/__init__.py registers the family.
  • Severity = Severity.WARNING per the grammar guide §7.2 v1 spec. Tags = ("modern", "doc") so the rule lands in the default, modern, and pythonic profiles automatically; --rules=doc selects just the M-DOC family explicitly. Category = Category.DOCUMENTATION.
  • Two implementation calibrations during the work: (1) @raises detection scans only code lines (skips ; doc: lines) — caught a false positive while validating against m-stdlib's STDCSPRNG.available(), where the @example body itself contains a set $ecode=,U-MYAPP-NO-CSPRNG, to demonstrate caller code; that's documentation, not a code-side raise. Pinned by a regression test. (2) Transitive @raises (codes raised by helpers the label calls but doesn't catch) is intentionally NOT inferred — would need a workspace call graph the m-cli lint engine doesn't yet have (cross-routine pass is the same gap M-XINDX-007/008 face). v1 surfaces direct raises only. The grammar guide §5.3 acknowledges this as an under-reporting trade-off; the manifest's raised_in_body informational field captures the same direct-raise set for consumers wanting it.
  • Validation against m-stdlib post-WA2: m lint --rules=M-DOC-001 src/ reports zero findings (every public label in src/ carries the required tag set). Project-wide make lint jumps from 567W → 600W (+33), all M-DOC-001 findings against tests/STD*TST.m files — test routines are NOT in WA2's scope, but M-DOC-001 correctly flags them as candidates for a follow-on backfill pass. 0E unchanged (M-DOC-001 is WARNING, doesn't gate --error-on=error).
  • STDLOG side-fix (m-stdlib commit pending alongside this row's update): the four level-emitter labels INFO/WARN/ERROR/FATAL each have 11 formals (event,k1,v1,...,k5,v5) but I'd taken a shortcut during WA2 and only annotated @param k1 (with note "same shape through k5/v5"). M-DOC-001 correctly flagged the 36 missing @param tags. Backfilled to add the 9 missing per-label @param tags each — DEBUG already had the full set; INFO/WARN/ERROR/FATAL now match. After the fix: 0 M-DOC-001 findings against src/. Manifest regenerated.
  • Acceptance met. Closing WA3.

WA4 — Manifest generator

Status. done (2026-05-08).

Goal. tools/gen-manifest.m walks src/STD*.m and writes dist/stdlib-manifest.json per the schema in plan § 3.2.

Approach.

  • Hand-rolled M parser (~150 LoC). Decision recorded in plan § 11.4 / tracker D3 — do NOT bind to tree-sitter-m for v1.
  • Recognised tokens: routine line (column 1, all-caps name); label line (column 1, lowercase or camel name, optional formal-list in parens); comment line (;); ; doc: block (special).
  • Emit JSON via $$encode^STDJSON — eat our own dog food.
  • Provide a Makefile target make manifest that runs the generator.
  • Source location: each label entry carries source: {file, line} so VS Code goto-def works without a separate index.

Acceptance. make manifest produces dist/stdlib-manifest.json containing every public label with the full schema. Manual spot-check against STDJSON, STDDATE, STDREGEX confirms accuracy.

Out of scope. Markdown rendering; HTML generation; per-module slim manifests (deferred per plan § 3.2 "until a consumer asks").

Progress log.

  • 2026-05-08 — Generator landed as tools/gen-manifest.py (~370 LoC of Python), pivoting away from the planned .m implementation. Reason for the pivot (open redirect point if user prefers M): (a) there is no clean home for a non-stdlib M routine — src/STDMAN.m would pollute the consumer-facing stdlib promise, and tools/*.m has no precedent or routine-path wiring in m-cli's m test discovery; (b) Python's json stdlib emits canonical output cheaply where bash + jq would have been awkward (jq is not installed on host); (c) Python is already a project dependency through m-cli; (d) the plan § 3.2 explicitly allows alternative implementations ("or .sh wrapping..."). The eat-own-dog-food argument for using STDJSON loses out to shipping speed for v1; can be revisited if a real consumer needs it. Also pivoted artefact path: tools/gen-manifest.mtools/gen-manifest.py (tracker row updated). What landed:
    • tools/gen-manifest.py — module-file walker (parse_module_file), label-line parser (parse_label_line), doc-block tag extractor (parse_doc_block), label entry builder (build_label_entry), manifest assembler (build_manifest), and an inline --self-test mode that parses a synthetic STDFOO fixture and asserts structural correctness (passes; runs in <100ms). Reads CHANGELOG.md to populate stdlib_version (currently v0.4.0). Emits dist/stdlib-manifest.json and dist/errors.json (the inverted error-code index — empty until WA2 adds @raises tags; this also covers WA7's deliverable).
    • Schema as implemented: {stdlib_version, generated_at, modules: {NAME: {synopsis, description, errors, labels: {NAME: {form, signature, synopsis, params, returns, raises, raised_in_body, examples, since, stable, see_also, deprecated, description, source: {file, line}}}, source: {file, line}}}, errors: {CODE: {module, labels}}}. The raised_in_body field is informational (lists ,U-STDxxx-, codes detected via set $ecode= static scan) — it powers the WA3 lint rule's cross-check between actual raise sites and declared @raises. Calibration vs the plan: added raised_in_body (not in plan § 3.2 schema) for the WA3 hand-off; added module-level description (the routine-header prose under the synopsis) since it's free signal. Module-level tag / phase / stable / storage from plan § 3.2 are not yet populated — those come from docs/modules/*.md frontmatter once WA6 lands; the generator can merge them in then.
    • Doc-comment grammar parser: implements docs/guides/m-doc-grammar.md § 3 strictly. The key call: indent-based continuation — a non-tag line whose body starts with whitespace extends the prior tag; a non-indented non-tag line flushes the current tag and joins the description block. This made the parse^STDJSON worked example in §6 of the grammar guide work as written, without requiring an empty ; doc: separator between the tag block and the post-tag prose.
    • Makefile: new manifest (regenerate) and manifest-check (regenerate + git diff --exit-code) targets. The manifest-check target is the local equivalent of WA5; the actual CI workflow change is still WA5's scope.
  • Real-world output. make manifest against current src/ (no WA2 backfill) reports 32 modules, 519 public labels in dist/stdlib-manifest.json (415 KB, 10,326 lines). Spot-checks confirm: (1) STDJSON.parse correctly classified extrinsic with signature: "$$parse^STDJSON(text, root)" and source: {file: "src/STDJSON.m", line: 39}; synopsis + description extracted faithfully. (2) Internal labels with no ; doc: block (parseFail, encodeFail, parseFileEof) are correctly excluded. (3) Labels that have architecture-explainer ; doc: blocks but aren't part of the public surface (e.g. parseArray, encodeValue, STDASSERT's recordFail) currently leak into the manifest — these need @internal tags during WA2 backfill, by design (the grammar's design call: better to ship leak-on-the-side-of-overinclusion than hide labels with documented contracts). (4) dist/errors.json is {} because no @raises tags exist yet — this is correct; backfill repopulates.
  • Two known sub-issues, deferred: (a) The signature field reflects the formal-list verbatim (text, root), losing the by-reference convention (text, .root at call sites) — needs either a @param-level by-ref marker (grammar refinement) or static analysis (e.g. kill <name> / merge <name>= inside the body implies by-ref). Not v1 critical. (b) Module-level phase / tag / stable / storage fields from plan § 3.2 schema are absent at module level — they originate in docs/modules/*.md frontmatter (WA6) and will merge in once that lands.
  • Unblocks. WA5 (CI gate — wire make manifest-check into .github/workflows/ci.yml); WA7 already covered (errors.json ships in the same generator); all of Wave B (m doc family reads dist/stdlib-manifest.json); WC1 (VS Code extension reads the same manifest); WD1 (AI skill regenerates from manifest).

WA5 — Manifest CI gate

Status. done (2026-05-08).

Goal. CI fails when dist/stdlib-manifest.json drifts from what tools/gen-manifest.m would produce.

Approach.

  • Add a CI step: make manifest && git diff --exit-code dist/stdlib-manifest.json.
  • Mirrors the existing m fmt --check pattern.
  • The committed manifest is the authoritative artefact; any commit that touches a ; doc: block must include the regenerated manifest in the same commit.

Acceptance. CI green when manifest matches; CI red when a tag is added/removed in src/ without re-running make manifest.

Out of scope. Auto-regenerating in CI and committing back — keep the regeneration as a developer responsibility (matches the m fmt model).

Progress log.

  • 2026-05-08 — CI gate landed in .github/workflows/ci.yml as a new "Manifest drift check" step on the m-stdlib job, placed after the m-cli/tree-sitter-m install but before Initialise YDB workspace. The step runs make manifest-check (the local target landed with WA4) which regenerates the manifest and runs git diff --exit-code dist/stdlib-manifest.json dist/errors.json. Engine-free — only needs python3 + git, both already installed by the prior steps. Not added to the iris-portability-check job because the manifest is engine-agnostic (one drift gate is sufficient). Not added to the local make check target — TDD loops touch ; doc: blocks frequently and would trip the gate during normal iteration; CI is the authoritative gate, make manifest-check is the on-demand local equivalent.
  • Pre-flight fixes to tools/gen-manifest.py to make the gate viable. The first manifest landed in WA4 had a generated_at: <wall-clock> field that changed every run, which would have caused the gate to fail spuriously on every commit. Removed the field outright — the information it carried (when was the manifest produced) is already covered by stdlib_version (sourced from CHANGELOG) and git log dist/stdlib-manifest.json (commit history). Also dropped import datetime and the _dt.UTC reference (Python 3.11+ only) for cross-version portability. Verified determinism with two consecutive make manifest runs — byte-identical output.
  • Unblocks. Wave A close: WA6 (frontmatter on docs/modules/*.md, no deps) + WA8 (release tag) are the remaining stdlib-side rows. WA7 (errors.json) was already covered by WA4's generator. WA2 (backfill) and WA3 (lint rule in m-cli) remain.

WA6 — Doc frontmatter

Status. done (2026-05-08).

Goal. Every docs/modules/stdXXX.md gains YAML frontmatter (per plan § 3.3 schema).

Approach.

  • Hand-edit each file (only ~20 modules).
  • Treat the manifest as authoritative when frontmatter and manifest disagree (consumers read manifest first).
  • Optional: small check script that warns on drift (tools/check-doc-frontmatter.m); not a CI gate yet.

Acceptance. Every per-module markdown parses as YAML+Markdown and carries the documented field set.

Out of scope. Generating these markdowns from the manifest — deferred. The markdown is human-written prose; the frontmatter is just structured metadata on top.

Progress log.

  • 2026-05-08 — Pivoted from "hand-edit each file" to a small idempotent backfill tool, tools/write-module-frontmatter.py (~180 LoC). Reads dist/stdlib-manifest.json (synopsis, labels, errors per module) plus docs/modules/index.md (phase + tag tables, conformance corpora table, cross-module dependency bullets) and prepends a YAML frontmatter block to each docs/modules/std*.md. Idempotent on re-run — files that already start with ---\n are skipped; --force regenerates. New Makefile target make frontmatter invokes it.
  • What landed: all 32 module docs now carry frontmatter — module / tag / phase / stable / since / synopsis / labels / errors / conformance / see_also. Spot-check on docs/modules/stdjson.md shows the right shape (phase: Phase 2, tag: v0.2.0, conformance: ['tests/conformance/json/'], see_also: ['STDLOG', 'STDSEED'] — derived from index.md cross-deps).
  • Calibration vs the plan §3.3 schema. The plan listed storage as a field; dropped it from the auto-derived set because it pointers at an in-doc anchor and changes per-module (better expressed in the prose itself). All other plan fields are populated. Stable defaults to "stable" for every module currently in a tagged release; experimental/deprecated will need hand-edits when those states arise.
  • Known limitations the user can iterate on:
    • labels includes internal helpers (e.g. STDJSON's parseArray, encodeValue) because the manifest currently lists every label with a ; doc: block. Once WA2 backfills @internal tags and the manifest regenerates, make frontmatter --force cleans up the labels list.
    • errors is [] everywhere because no @raises tags exist yet — also resolves with WA2.
    • synopsis is the verbatim first line of the routine header with m-stdlib — and trailing period stripped. Some have parenthetical track/tag noise (e.g. STDHTTP: "HTTP/1.1 client (track H3, target tag v0.4.0)"). Cleaning these is a separate src/ pass — not WA6's scope.
    • see_also is derived from index.md's cross-deps section — modules without a runtime dep relationship get []. Hand-edits welcome; the tool's --force would clobber them today, so a future iteration should diff hand-curated fields and merge rather than overwrite.
  • Unblocks WA8 (release tag) — the only remaining stdlib-side row. WA2 (backfill) and WA3 (m-cli lint) are cross-repo.

WA7 — Errors registry

Status. done (2026-05-08; structurally rolled into WA4).

Goal. dist/errors.json is the inverted index: U-STD* code → producing module + labels.

Approach.

  • Extend tools/gen-manifest.m to emit a second file. Same source data, different shape.
  • Cheap derivative — adds a few lines to the generator.

Acceptance. make manifest writes both dist/stdlib-manifest.json and dist/errors.json; the errors index covers every ,U-STDxxx-NAME, referenced in any @raises tag.

Out of scope. Per-error long-form descriptions — those live in the routine headers and per-module docs.

Progress log.

  • 2026-05-08 — Done as part of WA4. tools/gen-manifest.py walks every label's @raises tags and aggregates an inverted {code: {module, labels: [...]}} index, written to dist/errors.json alongside the manifest. The CI manifest-check gate (WA5) covers this file too. dist/errors.json is currently {} because no @raises tags exist in src/ — once WA2 backfill adds them, the file populates without further generator work.

WA8 — Wave A release

Status. done (2026-05-08; tagged v0.5.0 locally, not pushed).

Goal. Tag a release that ships the manifest, errors registry, frontmatter, and doc-grammar guide as one user-facing event. Update docs/modules/index.md with a "Machine-readable surface" subsection linking both dist/ artefacts.

Approach.

  • CHANGELOG entry under a new tag (decide v0.5.0 vs v0.4.1 based on whether Phase 4 module work has also accumulated — coordinate with the live module-tracker.md).
  • Add the "Machine-readable surface" subsection to docs/modules/index.md linking the manifest, errors.json, and the new grammar guide.
  • Ensure docs/modules/index.md and the manifest agree on the module list.

Acceptance. The new tag exists; CHANGELOG documents Wave A; docs/modules/index.md links the new artefacts.

Out of scope. Wave B/C/D consumer rollout — those waves can ship under their own tags later.

Progress log.

  • 2026-05-08 (prep) — All non-tag-cut prep work landed:
    • CHANGELOG.md gains an [Unreleased] section that captures Wave A — M-doc tag grammar v1, manifest generator + dist/, CI manifest-drift gate, frontmatter on all 32 module docs, the index.md machine-readable surface section. Frames the release as doc + tooling only (no src/STD*.m runtime behaviour change). Calls out WA2 + WA3 as the high-leverage Wave-A follow-ons.
    • docs/modules/index.md gains a "Machine-readable surface" subsection (right under the lead-in paragraph, before "Phase 1") — table of artefacts (dist/stdlib-manifest.json, dist/errors.json, docs/guides/m-doc-grammar.md, per-module frontmatter) plus regeneration commands (make manifest, make frontmatter, make manifest-check).
    • Pre-flight fix to tools/gen-manifest.py: read_stdlib_version() now skips an [Unreleased] heading and returns the most-recent versioned entry, so the manifest's stdlib_version stays anchored to the last shipped tag (v0.4.0 today) until WA8's tag is cut. Without this fix, adding [Unreleased] to CHANGELOG would have flipped stdlib_version to empty string and the WA5 gate would have fired spuriously.
  • Pending — user decision points:
    • Version number. v0.5.0 (SemVer minor-bump for new tooling surface) vs v0.4.1 (treat the docs/tooling delta as a patch). Recommendation: v0.5.0 — the manifest is a new public API consumers will depend on, even if no src/ runtime changes.
    • Timing. Tag now (Wave A is shippable as-is — manifest + frontmatter + grammar + CI gate all green), or wait for WA2 backfill to land first so the manifest's tag-derived fields populate before the version cut. Recommendation: tag now (decoupled releases — Wave A tag ships the surface, a follow-on tag ships the populated manifest from WA2).
    • Coordination with module-level work. If a Phase 4 module is mid-flight (Table 2 candidates: STDYAML / STDMATH / STDXFRM / STDNET), bundle vs. solo-cut. The current module-tracker.md says all numbered T-tickets T1-T30 are closed and Table 2 has only proposed (not started) modules — solo-cut is clean today.
  • Action when user gives the go-ahead:
    1. Edit [Unreleased] heading in CHANGELOG.md → [v0.5.0] — 2026-MM-DD (or chosen version + date).
    2. make manifest to regenerate stdlib_version to the new tag.
    3. Commit the CHANGELOG header + manifest update as the release-sync commit (precedent: 53ecf70 for v0.4.0).
    4. git tag -a v0.5.0 -m "<release annotation>". Decide whether to push (git push origin v0.5.0) per project policy.
    5. Flip this row to done.
  • 2026-05-08 (release sync) — User accepted both recommendations. Cut v0.5.0: CHANGELOG [Unreleased] heading flipped to [v0.5.0] — 2026-05-08; make manifest re-ran and stdlib_version flipped to v0.5.0 in both dist/stdlib-manifest.json and dist/errors.json. Tag v0.5.0 cut locally with annotated message enumerating Wave A scope. Tag not pushed — branch is local-only per session policy. Frontmatter not regenerated because since: per-module is anchored to each module's first-shipped tag (e.g. STDJSON's stays v0.2.0), not the rolling stdlib release. Wave A m-stdlib-side: ALL DONE (WA1+WA4+WA5+WA6+WA7+WA8). Cross-repo: WA2 (src/ tag backfill), WA3 (m-cli M-DOC-001 lint), WB1 (m-cli m doc).

Wave B — m-cli m doc family

WB1 — m doc <symbol>

Status. done (2026-05-08; m-cli commit 0024a72 pushed to origin).

Goal. The flagship lookup command, modelled on go doc. Plan § 4.1 has the full UX spec.

Approach.

  • New file in m-cli's command dir (verify path: likely ~/projects/m-cli/src/cmd/doc.m; mirror existing command file conventions).
  • Resolve m-stdlib via the same path the existing m-cli commands use; load dist/stdlib-manifest.json.
  • Three forms: m doc STDJSON (module), m doc STDJSON.parse (label), m doc parse (fuzzy across modules).
  • Output formatter: synopsis on top, signature highlighted, params / returns / raises blocks, example, since / stable / see line, source pointer at the bottom.

Acceptance. All three forms return correct output ≤ 100ms cold for the v0.5.0 manifest size.

Out of scope. Pager support, colour, network lookups (alt manifest sources). Keep v1 lean.

Progress log.

  • 2026-05-08 (m-cli commit 0024a72) — Repurposed the existing m doc CLI from path-based extract-to-Markdown to godoc-style symbol lookup over dist/stdlib-manifest.json. Three-file module (src/m_cli/doc/): lookup.py owns manifest discovery (4-level fallback: --manifest flag → $M_CLI_MANIFEST env → walk-up from cwd → ~/projects/m-stdlib/dist/stdlib-manifest.json) and symbol resolution (classifies into module / module.label / fuzzy bare-name); format.py owns three output forms (long with signature/params/returns/raises/example/source; short = one-line synopsis; json = raw manifest entry); cli.py is the rewritten argparse handler that ties them together. Legacy extract.py / render.py kept on disk as a programmatic surface — only the CLI was repurposed. The 4 old doc_command tests in test_doc.py were dropped since they exercised the path-based CLI behaviour that no longer exists; the 12 library-direct tests for extract/render still pass. WB2 is structurally absorbed into WB1 because --short and --json are wired in this commit too (the plan separated them as a follow-on task; landing them together cost one extra flag definition and three test classes — total cost was negligible vs. shipping them separate). WB3 fuzzy search is also partially live here: m doc <bare-name> does cross-module exact-name lookup with multi-hit listing — the broader m search "phrase" substring scan is still WB3.
  • 30 new tests in tests/test_cli_doc_lookup.py covering: lookup classification (module / dotted / bare / unknown), manifest discovery order (explicit/env/walk-up/missing), all three output forms (module long/short/json + label long/short/json + multi-hit list), and CLI error paths (missing manifest → exit 2, malformed JSON → exit 2, no match → exit 1, found → exit 0). All pass.
  • End-to-end against m-stdlib v0.5.0: m doc STDJSON.parse renders the full godoc-style block with signature $$parse^STDJSON(text, root) → bool, params/returns/raises tables, since/stable/see line, the @example body, and source: src/STDJSON.m:39. m doc parse finds 8 cross-module hits (STDARGS/STDCSV/STDENV/STDJSON/STDSEMVER/STDTOML/STDURL/STDXML each have a parse label). m doc (no args) lists all 32 modules with synopses. Cold latency ~30ms (well under the 100ms target).
  • Pre-existing manifest generator bug surfaced and fixed in m-stdlib (committed alongside this row's update): tools/gen-manifest.py's ROUTINE_LINE_RE regex capped routine-name length at 8 chars (the M89 standard limit), but YDB and IRIS allow up to 31. STDASSERT (9), STDCRYPTO (9), STDCSPRNG (9), STDSEMVER (9), STDCOMPRESS (11) were all returning empty synopsis in the manifest. Relaxed to [A-Z][A-Z0-9]{0,30}. Manifest regenerated; all 32 modules now have non-empty synopses; frontmatter on the affected docs/modules/*.md regenerated to match.
  • Unblocks: WB2 (--json + --short already shipped), WB3 (m search — keyword-search variant of WB1), WB4 (m manifest / m examples / m errors thin wrappers — all read the same manifest), WC1 (VS Code extension can consume the same manifest via the same path-resolution logic via a TypeScript port of find_manifest), WD1 (AI skill regenerator already has its input format pinned).

WB2 — --json and --short flags

Status. done (2026-05-08; landed in the same m-cli commit 0024a72 as WB1).

Goal. Scripting hooks. --json emits the raw manifest entry; --short emits one-line synopsis (godoc -short equivalent).

Approach. Trivial flag-handling on top of WB1.

Acceptance. m doc --json STDJSON.parse | jq .signature returns the signature string.

Out of scope. YAML / TOML output formats. JSON is enough.

Progress log.

  • 2026-05-08 — Shipped together with WB1 in m-cli 0024a72. Verified end-to-end: m doc --json STDFMT.f | python3 -c 'import sys,json; print(json.load(sys.stdin)["signature"])'$$f^STDFMT(template, a1, a2, a3, a4, a5, a6, a7, a8, a9). m doc --short STDASSERT.eqSTDASSERT.eq — Assert actual=expected (string equality).. Both forms covered by tests in tests/test_cli_doc_lookup.py (TestDocCommandShort, TestDocCommandJson).

WB3 — m search <query>

Status. done (2026-05-08; m-cli commit 6542e73 pushed to origin).

Goal. Fuzzy search across manifest synopsis + description + examples.

Approach.

  • Linear scan of the manifest (cheap at this size; revisit if it slows down).
  • Substring match, case-insensitive. Rank hits in synopsis above hits in examples.

Acceptance. m search "json parse" lists relevant labels with their synopses. m search "URL encode" finds STDURL.encode.

Out of scope. True fuzzy ranking (BM25, trigram). Substring is enough for v1.

Progress log.

  • 2026-05-08 (m-cli 6542e73) — Landed at ~/projects/m-cli/src/m_cli/doc/search.py (~115 LoC). AND-style match across query tokens (every space-separated token must appear somewhere in the label's haystack — synopsis ∪ description ∪ examples). Three-tier ranking: tier 0 = primary token in synopsis (highest priority), tier 1 = primary token in description, tier 2 = primary token only in examples. Within tier, results sort by (module, label) for determinism. --limit N truncates output (default 50); a footer message reports showing N of M matches when truncation kicks in. Manifest discovery is shared with m doc via the existing find_manifest() from lookup.py — same fallback chain (--manifest flag → $M_CLI_MANIFEST → walk-up from cwd → ~/projects/m-stdlib/dist/...). Exit codes: 0 = matches found, 1 = no matches, 2 = manifest unreachable. End-to-end against m-stdlib v0.5.0: m search "URL encode" correctly returns STDB64.urldecode + STDB64.urlencode + STDURL.encode (the first two via @example bodies that contain "URL"; STDURL.encode via synopsis). m search "json parse" returns 6 hits ranked by tier (STDJSON.valid + STDLOG.FORMAT in tier 0 because both labels' synopses have "json" or "JSON"; STDJSON.parse + STDJSON.parseFile + STDJSON.lastError + STDJSON.valueOf in tier 1/2). 7 unit tests in tests/test_cli_manifest_subcommands.py::TestSearch covering case-insensitivity, AND-match, ranking, --limit truncation, no-match, missing-query.

WB4 — m manifest, m examples, m errors

Status. done (2026-05-08; m-cli commit 6542e73 pushed to origin — landed in same commit as WB3).

Goal. Thin wrappers that surface the manifest's substructures directly. Plan § 4.3–4.5.

Approach. Three short commands — each ≤ 30 LoC. They wrap reading the manifest / errors.json and emitting the relevant subsection.

Acceptance. Each command does its one job and pipes cleanly into jq or text tools.

Out of scope. Filtering flags beyond what's already there in m doc.

Progress log.

  • 2026-05-08 (m-cli 6542e73) — Three sibling files under ~/projects/m-cli/src/m_cli/doc/, each its own top-level CLI command, all sharing the manifest discovery from lookup.py:
    • m manifest [path] (manifest.py ~80 LoC) — emits the resolved manifest as JSON. With no path → whole manifest. With STDJSON / STDJSON.parse / modules / errors / stdlib_version → just that subtree. Pipes cleanly into jq. Verified: m manifest stdlib_version"v0.5.0"; m manifest STDJSON.encode | jq .signature$$encode^STDJSON(node).
    • m examples [MODULE] (examples.py ~70 LoC) — walks every label's @example bodies and emits module.label: <body> lines for grep-friendliness. With a MODULE arg, scopes the walk to that module. Multi-line example bodies emit one prefixed line per source line. Verified: m examples STDFMTSTDFMT.f: write $$f^STDFMT(...), STDFMT.fn: set a("n")=....
    • m errors (errors.py ~95 LoC) — inverted index over @raises tags. Reads dist/errors.json (m-stdlib's WA7 sidecar) when present and falls back to deriving from the main manifest's per-label raises arrays — both produce identical output. --json emits the same shape as the sidecar for scripting. Verified: m errors lists all 43 codes from m-stdlib v0.5.0, each with producing module + every label that raises it (e.g. U-STDCOMPRESS-CALLOUT-MISSING STDCOMPRESS: gzip, gunzip, deflate, inflate, zstdCompress, zstdDecompress).
  • 17 unit tests in tests/test_cli_manifest_subcommands.py covering: m manifest (full output, MODULE / MODULE.label / top-level subpaths, unknown paths); m examples (all-modules walk, module filter, no-examples module, unknown module); m errors (derivation from manifest, sidecar preference, --json flag, empty manifest). Plus 2 shared tests covering missing-manifest → 2 and malformed-manifest → 2 across all four WB3+WB4 commands.
  • Wave B status: ALL DONE — WB1+WB2+WB3+WB4 = 4/4 rows. The full manifest-reader CLI surface from plan §4 is shipped: m doc <symbol> (long/short/json), m search <query>, m manifest [path], m examples [module], m errors. Cold latency ~30ms per command; manifest is a single ~415KB JSON read on each invocation.

Wave C — VS Code extension

WC1 — Extension v0.1

Status. done (2026-05-08; new repo rafael5/m-stdlib-vscode initial commit 1c15a05 pushed).

Goal. Manifest-driven hover, goto-definition, and completion in VS Code for .m files. Plan § 5.1 has the capability list.

Approach.

  • First decision: new repo ~/projects/m-stdlib-vscode/ vs subdir ~/projects/m-cli/vscode/. Recommend new repo — keeps m-cli's release cycle clean and the VS Code extension publishable independently.
  • TypeScript extension. On activation, locate the workspace's resolved m-stdlib (look for .m-cli.toml upwards; resolve install path the same way m doc does; load dist/stdlib-manifest.json).
  • Register hover provider on ^STD* and parse^STDJSON-style tokens.
  • Register definition provider using source.{file,line} from the manifest.
  • Register completion provider scoped to .m files.
  • Hard scope: this is not a full M LSP. Stdlib symbols only.

Acceptance. Hover on parse^STDJSON shows synopsis + signature. Cmd/Ctrl-click jumps to src/STDJSON.m:39. Typing ^STD shows a completion list of every module.

Out of scope. Full M LSP, project-aware symbols outside m-stdlib, lint integration, format-on-save (m-cli already has those via CLI).

Progress log.

  • 2026-05-08 (m-stdlib-vscode commit 1c15a05) — Repo confirmed as ~/projects/m-stdlib-vscode/ per user direction. Initial v0.1.0 release lands the three providers wired to dist/stdlib-manifest.json from m-stdlib's WA4 generator.
    • src/manifest.ts (~145 LoC) — 4-level discovery resolver mirroring m-cli's find_manifest(): m-stdlib.manifestPath setting → $M_CLI_MANIFEST env var → walk up from each workspace folder → ~/projects/m-stdlib/dist/stdlib-manifest.json fallback. mtime-keyed cache (loadManifest() re-reads when the file's mtime changes). Tilde-expansion for ~/... paths. Full TypeScript types covering the manifest schema.
    • src/tokens.ts (~155 LoC) — text-based token classifier (tokenAt, modulePrefixAt, labelPrefixAt). Recognises ^STDJSON, parse^STDJSON, $$parse^STDJSON(...) at any cursor position inside the matched span. The classifier is intentionally text-based — the extension doesn't spawn tree-sitter-m. Per the plan §5.1 hard-scope, the extension is stdlib-only and that scope is unchanged.
    • src/hover.ts (~165 LoC) — StdlibHoverProvider. Render mirrors m-cli's format_label_long / format_module_long (signature in code-fence; params/returns/raises bullets; since/stable; see-also; example; description; source pointer) but as vscode.MarkdownString. Module hovers list every public label with synopsis. Honours m-stdlib.enableHover.
    • src/definition.ts (~85 LoC) — StdlibDefinitionProvider. Resolves source.file (manifest field, repo-relative) against the manifest's repo root (<distDir>/..) and jumps to source.line (1-based → 0-based VS Code Position). Falls back to opening the routine file at line 1 for module references.
    • src/completion.ts (~115 LoC) — StdlibCompletionProvider. Two trigger shapes: ^STD<prefix> lists every module (Module kind + synopsis as detail); <prefix>^STDJSON (cursor inside the label-prefix region) lists every public label of STDJSON whose name starts with <prefix> (Function kind). Registered with ^ as a completion-trigger char so VS Code surfaces module suggestions the moment the user types it.
    • src/extension.ts (~95 LoC) — activation on onLanguage:m. Registers all three providers, installs a FileSystemWatcher per workspace folder for the resolved manifest path (regenerations from make manifest flow into the editor without reload), surfaces a one-shot status-bar warning when no manifest is reachable.
  • 22 unit tests pass (tests/tokens.test.ts + tests/manifest.test.ts, both running under node --test --experimental-strip-types for direct TS execution without a separate build step). Coverage: token classification across all three forms, completion-prefix detection, manifest discovery order, mtime-keyed cache invalidation, tilde-expansion. tsc clean (tsc -p ./ zero errors with strict: true).
  • Settings exposed: m-stdlib.manifestPath (override discovery), m-stdlib.enableHover / m-stdlib.enableDefinition / m-stdlib.enableCompletion (per-provider toggles, default on).
  • Acceptance verified (manually, against the m-stdlib v0.5.0 manifest): hover on parse^STDJSON renders signature + params + raises + source. Cmd-click jumps to src/STDJSON.m:39. Typing ^STD produces a completion list of all 32 modules with synopses.
  • Out of scope for v0.1, deferred to follow-on work: WC2 (snippet pack — see next row); diagnostic surface for ,U-STD*, references in $ETRAP handlers (would need a richer parse than the current text-based classifier); .vsix packaging for marketplace distribution (the user can npx vsce package when they want a publishable artefact); a real M LSP (out of scope for the entire WC track).
  • Push status: pushed. Repo created via gh repo create rafael5/m-stdlib-vscode --public --source=. --remote=origin; local branch renamed mastermain to match project convention; git push -u origin main completed. Live at https://github.com/m-dev-tools/m-stdlib-vscode.

WC2 — Snippet pack

Status. done (2026-05-08; m-stdlib-vscode commit 7d0723b pushed; v0.2.0 release).

Goal. Canonical idiom snippets. Plan § 5.1.

Approach.

  • VS Code snippet JSON for: STDASSERT suite skeleton, STDFIX with wrapper, STDLOG kv line, STDJSON parse-then-walk.
  • Trigger names: stdassert-suite, stdfix-with, stdlog-kv, stdjson-parse.

Acceptance. Each snippet expands into a working, lint-clean M block.

Out of scope. Snippets for every module; cover only the high-frequency idioms.

Progress log.

  • 2026-05-08 (m-stdlib-vscode 7d0723b) — Four-snippet pack landed at snippets/m.json, wired via package.json contributes.snippets for language: m. Triggers match the WC2 row exactly: stdassert-suite (full suite skeleton with start/@TEST/eq^STDASSERT/report plus runner-discoverable ;@TEST annotation), stdfix-with (one-liner XECUTE inside a TSTART/TROLLBACK scope), stdlog-kv (level-chooser dropdown — INFO/DEBUG/WARN/ERROR/FATAL — followed by event + key + value), stdjson-parse (parse with error-guard + $$type^STDJSON array check + $order walk skeleton). VS Code tabstop syntax with sane defaults; multi-tabstop sync where the same identifier appears in multiple places (e.g. ${3:tCase1} referenced by both the do tCase1 line and the label header).
  • 5 new tests in tests/snippets.test.ts pin: (1) the four prefixes are present; (2) every snippet is scoped to language: m; (3) every body is a non-empty array of strings; (4) prefix↔module correctness (catches "wrong body in wrong slot" mistakes via substring assertion that each snippet body references the named module); (5) package.json contributes.snippets linkage. 27/27 tests now green (22 prior from WC1 + 5 new).
  • Implementation calibrations: (a) $$call in M source has to be JSON-escaped as \\$\\$ so VS Code doesn't mistake the $ for tabstop syntax — backslashes flow through to the snippet engine which emits literal $$; (b) the . dot-block prefix doesn't need escaping (VS Code only treats $, }, \ as snippet-special); (c) bumped package.json version 0.1.0 → 0.2.0 since this is a user-visible feature add (no breaking change).
  • Wave C now COMPLETE: WC1 + WC2 = 2/2 rows. The VS Code surface for m-stdlib is shipped — hover, goto-def, completion, snippets all wired to dist/stdlib-manifest.json. Cross-repo follow-on remaining: only Wave D (AI skill).

Wave D — AI surface

WD1 — AI skill

Status. done (2026-05-08; m-stdlib tools/gen-skill.py + dist/skill/*.md committed; live at ~/claude/skills/m-stdlib/ and registered in ~/claude/CLAUDE.global.md).

Goal. A knowledge-skill at ~/claude/skills/m-stdlib/ regenerated from the manifest. Plan § 6.1.

Approach.

  • New tools/gen-skill.m reads dist/stdlib-manifest.json + dist/errors.json and writes:
    • ~/claude/skills/m-stdlib/SKILL.md (frontmatter with trigger conditions, prose orientation)
    • ~/claude/skills/m-stdlib/manifest-index.md (one line per module + per public label, dense English)
    • ~/claude/skills/m-stdlib/patterns.md (5–15 line idioms for STDASSERT / STDFIX / STDLOG / STDJSON)
    • ~/claude/skills/m-stdlib/error-codes.md (every U-STD* code, origin, mitigation)
  • CI gate: regenerate the skill, fail on diff (same model as the manifest gate).

Acceptance. Skill files exist and load via the standard skill mechanism. A /skill m-stdlib invocation gives the model the surface needed to write idiomatic m-stdlib code.

Out of scope. Hand-tuning skill prose beyond the generator output. If a passage needs prose embellishment, add it to a "static-prose" partial in the generator so re-runs don't lose it.

Progress log.

  • 2026-05-08 — Pivoted from tools/gen-skill.m (planned) to tools/gen-skill.py (Python, mirroring gen-manifest.py's precedent). Same rationale as WA4: there's no clean home for a non-stdlib M routine, Python's stdlib gives clean text generation, and the generator runs against the manifest produced by Python anyway. Open redirect point if the eat-own-dog-food argument outweighs shipping speed.
  • What landed:
    • tools/skill-patterns.md (~250 lines) — hand-curated canonical-idiom library (STDASSERT suite skeleton; STDFIX with/register/invoke; STDMOCK; STDLOG kv + JSON; STDJSON parse-then-walk; STDURL; STDCSV; STDDATE; STDCSPRNG; STDCRYPTO; STDCOMPRESS; STDHTTP; STDARGS; STDFS; STDSNAP). The file is the static input — gen-skill.py copies it through verbatim into dist/skill/patterns.md with a generated header. Edits to the prose go here, NOT to the generated output.
    • tools/gen-skill.py (~265 lines) — reads dist/stdlib-manifest.json + dist/errors.json + tools/skill-patterns.md. Emits four files into dist/skill/:
      • SKILL.md (~4.5 KB) — frontmatter with name, type: knowledge, full description with trigger phrases ("m-stdlib", "STDJSON", "STDASSERT", "STDCRYPTO", "STDLOG", "$$parse^STD", "do start^STDASSERT", "^STD"); body is the catalogue of all 32 modules with their synopses (with m-stdlib — prefix stripped for cleaner reading), a "When to use this skill" guide, a "Companion files" pointer table to the other three, the architectural rules, and a quick-start.
      • manifest-index.md (~26 KB) — one entry per module with every public label rendered as \signature` — synopsis, plus a per-module raises:` summary line. The compact dense-English reference for "I know the module name; show me the labels."
      • patterns.md (~8 KB) — tools/skill-patterns.md copied through with a one-line generated header noting the source.
      • error-codes.md (~3.5 KB) — inverted index over dist/errors.json, grouped by producing module: every U-STDxxx-NAME with the comma-separated list of labels that raise it. The lookup table for $ETRAP handler disambiguation.
    • Makefile targets: skill (regenerate), skill-check (regenerate + diff against committed dist/skill/; mirrors manifest-check), skill-install (copy to ~/claude/skills/m-stdlib/ so Claude can load it as a knowledge skill — one-shot, not in CI).
    • ~/claude/CLAUDE.global.md skills table — added an m-stdlib row alongside the existing knowledge skills (mumps-language, vista-system, etc.) so the global skills registry sees the new entry.
  • Determinism: no timestamps in the generated output; running make skill-check after make skill exits clean (skill: clean). Drift detection works identically to manifest-check.
  • Calibration vs the plan §6.1 sketch: (1) generator is Python not M (same reasoning as WA4); (2) dist/skill/ is the committed staging dir + the CI-gateable artefact, with ~/claude/skills/m-stdlib/ populated via a separate make skill-install step (cleaner than emitting directly to ~/claude/, which lives in a different repo); (3) static prose lives in tools/skill-patterns.md and is copied through verbatim, so prose embellishment doesn't get clobbered by re-runs (the plan called this out as a non-negotiable).
  • Out of scope (deferred): wiring make skill-check into .github/workflows/ci.yml — the gate works locally; CI integration is a small follow-on. Auto-installing on every make manifest — kept as a separate explicit step so users in CI don't accidentally write to ~/claude/.
  • Wave D status: WD1 done. WD2 (tools/gen-doctests.m — emit *DOCTST.m from @example lines) remains. The skill at ~/claude/skills/m-stdlib/ is loadable now: any session that mentions an STD* symbol triggers it, and the four files give the model the catalogue + idioms + error registry it needs to write idiomatic m-stdlib code on the first attempt.

WD2 — Doctest generator

Status. done.

Goal. Make @example lines executable. Plan § 3.4.

What landed.

  • tools/gen-doctests.py (~210 LoC, Python — mirrors the gen-manifest / gen-skill family). Reads dist/stdlib-manifest.json, classifies every @example against a small set of doctest-eligible shapes, emits one tests/STD<MOD>DOCTST.m per module that has at least one accepted example, removes orphaned suites whose modules dropped to zero accepted.
  • Eligible shapes (the rest are skipped silently — --verbose lists the decisions):
    • Pattern A (string-equal): write <expr> ; "<expected>"do eq^STDASSERT(.pass,.fail,<expr>,"<expected>","doctest: <label>").
    • Pattern A-num (numeric-equal): write <expr> ; <number>do eq^STDASSERT(.pass,.fail,<expr>,<number>,...) (no quotes).
    • Pattern A-prefix (substring): write <expr> ; "<prefix>..."do contains^STDASSERT(.pass,.fail,<expr>,"<prefix>",...) (handles e.g. STDCRYPTO.sha256 whose hash starts with ba7816bf...).
  • Free-variable filter (expression_is_self_contained): an example like write $$size^STDFS(path) ; 4096 is structurally Pattern A but references the unbound local path. The classifier strips string/numeric literals, $$label^MODULE openings, $intrinsic references, and ^GLOBAL references, then refuses any expression that still contains a bare identifier. Catches the cases that would otherwise trip M-MOD-024 at lint time and <UNDEF> at run time.
  • ILLUSTRATIVE_LABELS skiplist (6 entries): the residual category — examples that are self-contained write … ; "…" shapes but whose expected value is documented as a placeholder rather than a literal output. Each entry carries a one-line reason: STDDATE.now (current wall-clock), STDMOCK.called / STDMOCK.resolve (require prior mock setup), STDOS.cwd / STDOS.hostname / STDOS.user (host-specific). These were surfaced by running the doctest suite on real engine and seeing them fail with shape-correct-but-value-wrong assertions; documenting the skip in code keeps the "why" attached to the entry.
  • Sanitiser: M label names cannot contain underscores (_ is the concatenation operator), so tDoctest_decode parses as tDoctest_decode (a concat). Generator emits camelCase names instead — tDoctestDecode, tDoctestUrldecode, etc., which match the project's hand-written test-label convention (tEncodeRfcVectors).
  • Indentation: 8 spaces, not tabs — the tree-sitter-m parser rejects leading tabs even though classic M tolerates them. Discovered by bisection when m fmt reported parse error — source did not parse cleanly (4 error nodes) against an early tab-indented draft.
  • Lint suppression in the generated header: ; m-lint: disable-file=M-MOD-020,M-MOD-001,M-MOD-031 — long-routine, line-length, and magic-literal warnings are intentional in doctest suites where the assertion lines bake in literal arguments straight from @example.
  • 15 emitted suites, 51 generated doctests, all passing on the engine. Suites span: STDB64 (4), STDCRYPTO (1), STDDATE (6), STDFIX (1), STDFMT (1), STDFS (4), STDHEX (3), STDJSON (1), STDMATH (1), STDREGEX (1), STDSEMVER (8), STDSTR (12), STDTOML (1), STDURL (5), STDUUID (2). Full make test (47 suites including the 32 hand-written + 15 doctest) reports 2562/2562 assertions green.
  • Makefile targets: make doctest (regenerate), make doctest-check (drift gate — regenerate + diff against committed copies; mirrors manifest-check and skill-check), make doctest-run (execute the suite via m test tests/STD*DOCTST.m). The runner is a separate target rather than part of make check because it requires a live vista-meta engine connection — make check is the fast lint+fmt+test loop and stays engine-bound only via make test. Once the doctest invariant is stable, the runner can be folded into make ci.

Calibration vs plan § 3.4. Plan envisaged "two @example shapes" — a bare-invocation form and a self-asserting do eq^STDASSERT(...) form. Reality: the second form (already-asserting examples) appears almost exclusively in STDASSERT itself and references free counter variables that aren't valid outside an actual test routine, so emitting them verbatim isn't useful. The bare-invocation form is the pivotal case, and we extended it three ways (string / numeric / prefix). The skiplist mechanism for illustrative-only examples is new — the plan didn't anticipate that some write … ; "…" examples are documented placeholder values rather than literal expectations. The Python implementation (matching gen-manifest.py / gen-skill.py) replaces the plan's "tools/gen-doctests.m" so the generator family is uniform.

Out of scope (deferred).

  • Wiring make doctest-check into .github/workflows/ci.yml — small follow-on; gate works locally and the file family is committed so drift is visible in PRs anyway.
  • Source @example reformulation — six labels (the ILLUSTRATIVE_LABELS skiplist) document a placeholder value rather than a literal expected output. Reformulating their @example tags to be self-contained (e.g. write $$valid^STDDATE($$now^STDDATE()) ; 1 for STDDATE.now) would let us drop the skiplist; deferred because the current docs are clearer with the placeholder shape.
  • Multi-statement examples — Pattern D in the classification (e.g. do set rc=$$parse^STDXML(text,.tree)) is a multi-step setup-then-assert idiom that needs richer parsing to emit. Those examples remain illustrative-only for now.

Files touched. tools/gen-doctests.py (new, ~210 LoC), tests/STD{B64,CRYPTO,DATE,FIX,FMT,FS,HEX,JSON,MATH,REGEX,SEMVER,STR,TOML,URL,UUID}DOCTST.m (15 generated, committed), Makefile (3 new targets + .PHONY extension), this tracker.

Wave D status: COMPLETE. WD1 (AI skill) + WD2 (doctest generator) both shipped. The discoverability + tooling plan as a whole is closed at 16/16 rows.


Sequencing summary

A compact dependency view, useful when planning a session:

Wave A:
  WA1 ──┬──► WA2 ──┐
        ├──► WA3   │
        └──► WA4 ──┼──► WA5
                   ├──► WA7 ──┐
              WA6 ─┴──────────┴──► WA8

Wave B (depends on WA4):
  WA4 ──► WB1 ──► WB2
       └─► WB3
       └─► WB4 (also needs WA7 for `m errors`)

Wave C (depends on WA4):
  WA4 ──► WC1 ──► WC2

Wave D:
  WA4 ──► WD1
  WA1 + WA2 ──► WD2

The shortest path to a useful new capability is WA1 → WA4 → WB1: once those three are done, every subsequent piece (other Wave A items, the rest of Wave B, all of Wave C, Wave D) becomes easier.