Skip to content

Fix the documented shape of projectMap's heading map - #18

Merged
coddingtonbear merged 1 commit into
mainfrom
fix/readme-projectmap-shape
Aug 2, 2026
Merged

Fix the documented shape of projectMap's heading map#18
coddingtonbear merged 1 commit into
mainfrom
fix/readme-projectmap-shape

Conversation

@coddingtonbear

Copy link
Copy Markdown
Owner

What was wrong

README § "Inspecting a document" documented projectMap(buildModel(document)) as returning headings as an array of paths:

headings: [["Meeting Notes"], ["Meeting Notes", "Action Items"]]

The engine returns a tree nested by containment. The prose on top of it was wrong in the same way — "each entry is an array whose length is that heading's level", "a null element marks a skipped level" — and the version hash shown (c23234) was not the hash of the sample document either. pages/how_to.md (a typedoc project document) carried the same drifted example.

This shape was never real. 1.x's getDocumentMap returned Record<"A::B", { marker, content, level }> (see src/types.ts at 629227e^); 2.0's projectMap returns the containment tree. Neither ever produced an array of paths, so the documented example could not have worked in any released version.

Evidence

Observed output, not read off the source — the README's own sample document, through the library and through the CLI:

$ node dist/cli.js print-map sample.md
{
  "version": "329b63",
  "frontmatterFields": ["status"],
  "headings": { "Meeting Notes": { "Action Items": {} } },
  "blocks": []
}

The prose claims were checked the same way:

Claim now in the README Observed
leaf maps to {} {"Meeting Notes":{"Action Items":{}}}
skipped level is not a hole (# One then ### Deep) {"One":{"Deep":{}}} — no placeholder
an empty heading is the key "" (# One then ##) {"One":{"":{}}}
duplicate keys carry a marker suffix (unchanged paragraph, re-verified) {"Log":{"Monday":{}},"Log␣␣":{"Tuesday":{}}}, blocks: ["abc","abc␣␣"]

Guard against re-drift

src/tests/docs.map.test.ts (following the existing docs.rename.test.ts / docs.whitespace.test.ts precedent) reads the sample document out of README.md and asserts both directions:

  • the projection of that document equals the documented map, version hash included → an engine change fails here;
  • README.md and pages/how_to.md still quote that map verbatim → a docs edit fails here.

Plus the prose claims: leaf {}, headingTreePaths enumeration, every advertised path resolving through readTarget, skipped level, empty-heading key.

Negative control — with the old documented values substituted, the assertions fail as intended:

✕ old documented shape is NOT what the engine produces
    Expected: [["Meeting Notes"], ["Meeting Notes", "Action Items"]]
    Received: {"Meeting Notes": {"Action Items": {}}}
✕ a wrong version hash is caught
    Expected: "c23234"
    Received: "329b63"

npm test: 519 passed, 21 suites. tsc --noEmit: clean. No source change — src/ is untouched apart from the new test, so there is no runtime behavior to regress.

Where this could be worse than before

  • Pinning the version hash in the README is new exposure: editing the sample document now breaks a test. That is deliberate (the old hash was wrong and nothing caught it), and the failure is a one-line update with the message pointing at it.
  • The regex extracting const document = ... from README.md could go stale if the snippet is restructured. It throws a named error explaining what moved rather than silently passing on a wrong document.
  • The migration-section bullet is an addition beyond the strict ask. It is there because getDocumentMap users are the readers most likely to expect the flat record; drop it if you'd rather keep that section at two bullets.

Honest gaps

  • The typedoc comments in src/projection.ts describe the shape correctly, so they are untouched — but headingTreePaths's "in document order" claim is not strictly true: a heading whose text is an integer-like string (# 12) becomes an integer-index key and sorts ahead of the others (# Beta / # 12 / # Alpha / # 2 enumerates as [["2"],["12"],["Beta"],["Alpha"]]). That is an engine/contract question, not a README fix, so it is filed separately rather than silently patched here.
  • The rendered typedoc site is not regenerated in this PR (docs/ is gitignored and built separately); pages/how_to.md is the source that feeds it.
  • site/index.html was checked for the same drift and has none — it never shows the map's JS shape.

🤖 Generated with Claude Code

https://claude.ai/code/session_011UMMqb4R5YU17gjVPrhva8

The README's "Inspecting a document" example showed `headings` as an array
of paths -- `[["Meeting Notes"], ["Meeting Notes", "Action Items"]]` -- a
shape no released version has produced. 1.x's `getDocumentMap` returned a
flat `Record` keyed by `::`-joined strings; 2.0's `projectMap` returns a
tree nested by containment. A reader following the example indexed into an
array that is really an object, and the prose built on it was wrong too:
the tree carries no levels, so there is no "length is the heading's level"
and no `null` standing in for a skipped one.

Rewrite the snippet and its prose against the real shape, point at
`headingTreePaths` for turning the tree into addresses, and fix the same
drifted copy in pages/how_to.md. Note the map's shape change in the 1.x
migration section, since `getDocumentMap` users are the readers most
likely to expect the old flat record.

Add docs.map.test.ts alongside the existing docs.* tests: it reads the
sample document out of README.md, asserts the projection matches the
documented map (version hash included), and asserts the docs still quote
that map verbatim -- so an engine change and a docs edit each fail a
different assertion instead of silently drifting apart again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UMMqb4R5YU17gjVPrhva8
@coddingtonbear
coddingtonbear merged commit c176118 into main Aug 2, 2026
1 check passed
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