annot-product-docs
@ingcreators/annot-product-docs is the core of the
living-product-docs flow: MDX parser, Playwright screen
fixture, byte-stable comment-block rewriter, drift detector,
and the annot-docs CLI.
Tier A — Node-only, no DOM. Loads playwright-core at CLI
runtime for sync / lint. The library surface (parser, match
resolver, drift detector) is DOM-free.
Parser
Section titled “Parser”import { parseMdx, parseMdxFile } from "@ingcreators/annot-product-docs";
const parsed = parseMdxFile("docs/books/spec/SC-001.mdx");// {// frontmatter: { annot: { id, title, xlsx: ... }, meta: {} },// body: "# Login screen\n\n<Screen ...>...",// snapshot: "- textbox [name=...]...", // verbatim// attributes: { e1: "id=\"email\"...", ... },// overlays: [{ screenId, number, match, intent, content }, ...],// }parseMdx(source) operates on a string; parseMdxFile(path)
reads from disk. Both are pure — no side effects, no IO beyond
the optional file read.
Match resolver
Section titled “Match resolver”import { parseSnapshot, resolveMatch } from "@ingcreators/annot-product-docs";
const snapshot = parseSnapshot(rawSnapshotYaml);const ref = resolveMatch(snapshot, { role: "textbox", name: "Email" });// → "e1" or undefined when unresolvedmatch.under constrains the search to descendants of an
ancestor that itself matches the under-clause; match.nth
picks the Nth match.
Playwright productDocs fixture
Section titled “Playwright productDocs fixture”import { test } from "@ingcreators/annot-product-docs";
test("login", async ({ page, productDocs }) => { await page.goto("/login"); await productDocs.sync({ id: "login", mdxPath: "docs/spec/SC-001.mdx" });});productDocs.sync(options) re-syncs the annot:snapshot +
annot:attributes blocks in the named MDX. Options:
| Option | Default | Purpose |
|---|---|---|
id | required | Which <Screen id> block to update |
mdxPath | required | The MDX file to rewrite |
strict | false | Throw when an <Overlay match> doesn’t resolve |
screenshot | {} | Forwarded to page.screenshot() |
The fixture writes the screenshot to the path the
<Screen src> references — typically ./shots/<id>.png
relative to the MDX.
Comment-block rewriter
Section titled “Comment-block rewriter”updateCommentBlocks(source, { snapshot, attributes }) performs
the byte-stable rewrite that screen.capture calls
internally. Useful when you’re building a custom tour runner:
import { updateCommentBlocks } from "@ingcreators/annot-product-docs";
const rewritten = updateCommentBlocks(mdxSource, { snapshot: yamlWithBoxes, attributes: extractedAttrSet,});await writeFile(mdxPath, rewritten);The rewrite is byte-stable: rerunning with the same inputs produces a byte-identical output. Empty diff on no-change runs.
Drift detector
Section titled “Drift detector”import { detectDrift, detectDriftFromYaml, type DriftFinding,} from "@ingcreators/annot-product-docs";
const findings: DriftFinding[] = await detectDriftFromYaml( storedSnapshotYaml, liveSnapshotYaml, { tier: "strict" });Returns a flat list of findings. Each finding carries:
interface DriftFinding { kind: "added" | "removed" | "renamed" | "role-changed" | "duplicated" | "attribute-drift"; severity: "error" | "warning"; mdxPath?: string; lineNumber?: number; detail: string; // human-readable message}annot-docs init # scaffold config + sampleannot-docs sync --url <url> # refresh snapshot/attrs across MDXsannot-docs lint --url <url> # report drift; non-zero exit on errorsannot-docs lint --json --ci # CI-shaped output (JSON + warnings fail)annot-docs lint --fix # auto-refresh stored blocks--url defaults to http://localhost:5173 for the dev
case. --ci activates the severity-bucket exit-code logic;
--json emits machine-readable findings on stdout.