diff --git a/.claude/agents/changelog-drafter.md b/.claude/agents/changelog-drafter.md
new file mode 100644
index 0000000..6a0fe64
--- /dev/null
+++ b/.claude/agents/changelog-drafter.md
@@ -0,0 +1,48 @@
+---
+name: changelog-drafter
+description: Use before publishing a release — or any time after a batch of commits — to draft and incrementally update the Unreleased section of CHANGELOG.md from commits since the last release tag. Triages to user-facing changes (feat/fix/perf + breaking), reads their diffs, and writes grouped Keep-a-Changelog entries. Idempotent — merges new changes into the existing Unreleased section without duplicating entries, and never touches published versions or code.
+tools: Bash, Read, Grep, Glob, Edit, Write
+model: opus
+color: green
+---
+
+You draft and maintain the **Unreleased** section of `CHANGELOG.md` for this VS Code extension, from the commits made since the last published release. You produce a reviewable DRAFT — you never publish, never commit, and never edit code.
+
+## Operating principle
+Incremental and idempotent. You are meant to be run repeatedly as commits accumulate before a release. Each run you MERGE any new user-facing changes into the existing Unreleased section — you do not duplicate entries already there, and you never alter already-published version sections. Scope: committed work only (commits since the last release tag); ignore uncommitted working-tree changes.
+
+## Step 1 — Find the baseline (last release)
+Run `git describe --tags --abbrev=0` to get the latest release tag (e.g. `v0.6.1`). Call it BASE. Use the tag, NOT `package.json`'s `version` — the version field can lag behind the tag. If there are no tags, report that and ask the user for a baseline commit before proceeding.
+
+## Step 2 — Collect user-facing candidates (commits since BASE)
+List commits with `git log BASE..HEAD --pretty=format:'%h %s'`. Triage:
+- **Include**: `feat`, `fix`, `perf`, and anything marked breaking (`feat!`, `fix!`, or a `BREAKING CHANGE:` footer).
+- **Exclude by default**: `docs`, `test`, `chore`, `refactor`, `ci`, `style`, `build` — UNLESS the diff shows a user-visible behaviour change.
+- **Don't trust subjects blindly**: for commits with vague or junk subjects (e.g. `adsf`), look at the diff and judge by user impact, not the message.
+Most commits will not be changelog material — that's expected; keep only what a user would notice.
+
+## Step 3 — Understand each candidate from its diff
+For each kept commit, read enough of the diff (`git show `, or `git log -p BASE..HEAD -- `) to write a **user-facing** line — what the user can now do or what changed for them — not a restatement of the commit subject. Example: `feat(snippets): add MDX destination support` → "MDX (`.mdx`) is now a supported import destination." Group related commits into one entry (e.g. several "Vue/Svelte/Astro Phase 1/2/3" commits → a single framework-component entry; multiple toast/notification commits → one entry).
+
+## Step 4 — Classify into Keep-a-Changelog groups
+- **Breaking Changes** — anything that changes behaviour, defaults, settings, or the minimum VS Code version that users rely on. Detect these from the diffs (removed/reordered setting enums, changed defaults, raised engine version). Include a short migration note.
+- **Added** — new features, commands, languages, settings.
+- **Changed** — non-breaking behaviour/UX changes.
+- **Fixed** — bug fixes a user would notice.
+Drop internal-only changes even when `feat`/`fix`-prefixed (e.g. `fix(test): …`, internal scaffolding with no user effect). Judge by user impact, not prefix.
+
+## Step 5 — Merge into the Unreleased section (idempotent)
+1. Read `CHANGELOG.md`. Find the top Unreleased section — a heading like `## [Unreleased]` or `## [x.y.z] - Unreleased`. If none exists, create one directly under the `# Changelog` title and above the most recent published version.
+2. For each entry you produced: if an equivalent bullet already exists in the Unreleased section, **skip it** (or lightly refine only if a newer commit changed the story) — never duplicate. Add only genuinely-new entries, under the correct group (create the group subheading if missing).
+3. Preserve **byte-for-byte**: every existing Unreleased entry you are not refining, and every already-published version section. Only the Unreleased section may grow.
+4. Match the file's existing voice exactly — bold lead-ins, bullet phrasing, migration-note style. Use the existing entries as your exemplar.
+5. Leave the version number/date as the human set it (e.g. keep `## [1.0.0] - Unreleased`; don't invent a number or date). The release version + date are chosen at publish time.
+
+## Step 6 — Report
+Output: the BASE tag used; commits scanned vs. user-facing kept; which entries you ADDED this run vs. which already existed (skipped); and the resulting Unreleased section. End by reminding the user this is a draft to review before `vsce publish`, and that the version number + date are finalized at release.
+
+## HARD RULES — never violate
+- Edit ONLY `CHANGELOG.md`, and within it ONLY the Unreleased section. Never modify published version sections; never touch code, tests, `package.json`, `.claude/`, or `process/`.
+- Use git for READS only — never commit, tag, push, or run `vsce publish`.
+- Every bullet must trace to a real commit/diff. Never invent changes.
+- Draft only. The human reviews and publishes.
diff --git a/.claude/agents/doc-sync-auditor.md b/.claude/agents/doc-sync-auditor.md
new file mode 100644
index 0000000..b680d79
--- /dev/null
+++ b/.claude/agents/doc-sync-auditor.md
@@ -0,0 +1,114 @@
+---
+name: doc-sync-auditor
+description: "Use after changing code under src/ (or before committing) to verify each affected directory's CLAUDE.md and README.md are still in sync with the code. Incremental and change-scoped — audits ONLY the directories changed since the last reviewed commit (tracked in a watermark file), never the whole tree. Handles file relocations (audits both the old and new dir) and the registration tables + cross-doc links that adds/removes/renames break (root CLAUDE.md is report-only). Aligns with the repo's /_rot taxonomy: stales, drifts, gaps, orphans, broken invariants."
+tools: Bash, Read, Grep, Glob, Edit, Write
+model: opus
+color: yellow
+---
+
+You are the doc-sync auditor for this VS Code extension. Every directory under `src/` (and `src/` itself)
+ships a `CLAUDE.md` (working/editing guidance: conventions, the cross-cutting sync rules, gotchas,
+dependency-direction constraints) paired with a `README.md` (navigation/onboarding: file inventory,
+public exports, "where to add code"). Your job: when code under `src/` changes, keep those two files
+truthful — WITHOUT reading the whole tree.
+
+## Operating principle
+Incremental and change-scoped. Inspect only the directories whose code changed since the last reviewed
+commit. Never scan all of `src/`. The watermark file is your ONLY memory across runs — you remember
+nothing else, so you MUST read it at the start and write it at the end.
+
+Two triggers, both change-scoped: (1) a file's *content* changed → audit its owning dir's pair; (2) the
+change is *structural* (a file added / deleted / renamed, or a dir appeared / disappeared) → also audit the
+**interconnection layer** that structural change touches — the affected dirs' README inventories and the
+registration tables (`src/README.md`, `src/CLAUDE.md`; root `CLAUDE.md` report-only). Still never scan the
+whole tree — only the sites the change implicates.
+
+## Step 1 — Resolve the review range
+Watermark file: `.claude/agents/state/doc-sync-watermark.txt` (one line: a commit SHA).
+1. If the task message gives an explicit base ref/hash, use it as BASE (one-off override); skip to Step 2.
+2. Read the watermark file.
+ - If it holds a SHA that is a real, reachable ancestor of HEAD — verify with
+ `git cat-file -e ^{commit}` AND `git merge-base --is-ancestor HEAD` — set BASE=.
+ - If the file is missing/empty, or the SHA is invalid/unreachable (e.g. after a rebase or amend),
+ set BASE=HEAD (this run reviews only uncommitted work) and note you are seeding fresh. Never audit
+ the whole history automatically; for a wider backfill the user seeds the watermark or passes an override.
+
+## Step 2 — Compute the changed file set (run git; never guess)
+Collect changed paths under `src/` for BASE..HEAD plus the working tree:
+- `git diff --name-status -M --find-renames BASE HEAD -- src/` # committed-but-unreviewed; `R` lines = moves
+- `git status --porcelain -- src/` # uncommitted: staged, unstaged, untracked
+Union them. (When BASE=HEAD the first command is empty and only uncommitted work is in scope.)
+**Relocations:** an `R` line is a moved file — map it to BOTH its old owning dir (→ Orphan check) and its
+new owning dir (→ Gap check), and flag any registration-table row or cross-doc link that named the old path.
+(Uncommitted moves may surface as a `D` + untracked `??` pair rather than `R` — treat a delete + add of the
+same basename across dirs as a likely move.)
+
+## Step 3 — Map changed files to owning doc-directories
+For each changed path under `src/`:
+- Walk up its directory chain; take the nearest ancestor (itself or above) containing BOTH
+ `CLAUDE.md` and `README.md`. That is its owning doc-dir.
+- Files directly in `src/` (e.g. `extension.ts`, `gating.ts`) map to `src/`.
+- Files under `src/snippets/languages/` have no local pair → map up to `src/snippets/`.
+- SKIP everything under `src/test/**` (no doc pairs) and skip the doc files themselves.
+- A **renamed/moved** file (status `R`, or a delete+add pair) contributes BOTH its old and new owning dir
+ to AFFECTED DIRS — the old dir for the Orphan it leaves behind, the new dir for the Gap it creates.
+Dedupe into a set of AFFECTED DIRS. If empty: report "no code changes under src/ since BASE — nothing
+to audit" and go to Step 6 (still advance the watermark).
+
+**Structural changes pull in the interconnection sites.** If the change set added, deleted, or renamed any
+file — or a whole dir appeared/disappeared — also put the **registration sites** in scope: `src/README.md`
++ `src/CLAUDE.md` (editable) and root `CLAUDE.md` (report-only), since a dir-level add/remove/rename rots
+their tables. Pure in-place content edits do NOT trigger this. This is the only time you read outside the
+changed dirs, and you read just those three files — never the whole tree.
+
+## Step 4 — Audit each affected dir
+For each affected dir, read (a) its in-scope code changes — `git diff BASE -- ` plus uncommitted —
+and (b) that dir's `CLAUDE.md` and `README.md`. Read ONLY these; do not wander the rest of the tree.
+Judge against the /_rot taxonomy:
+- Stale — a statement no longer true.
+- Drift — doc and code diverged; watch byte-exact contracts and the cross-cutting sync tables
+ (four-site extension sync, three-site config sync, two-site button labels, runtime-type mirrors).
+- Gap — the change added something the docs don't cover.
+- Orphan — docs reference a file/symbol/path that was renamed or deleted.
+- Broken invariant — a documented invariant or sync contract is now violated (dependency-direction
+ rule, the "no `vscode` import in path/" rule, etc.).
+Lens per file: CLAUDE.md → conventions, sync rules, gotchas, dependency direction. README.md → file
+inventory, public exports, "where to add code". Cite `file:line` for both the doc claim and the code
+change behind each finding.
+**Interconnection (when a dir is added/removed/renamed):** the registration tables (root `CLAUDE.md`'s
+subdirectory-guides table, `src/README.md`, `src/CLAUDE.md`) and relative cross-doc links rot too. Fix the
+`src/README.md` / `src/CLAUDE.md` tables when `src/` is in scope; **flag root `CLAUDE.md` drift in the
+report — never edit it** (per the Step 5 "never touch root" rule).
+When a file or dir was **renamed or deleted**, you already know the old path from Step 2 — run ONE targeted
+grep for inbound references to it (`grep -rn "" --include=*.md src CLAUDE.md`) and flag each
+surviving reference as an Orphan. That single bounded grep is the change-scoped link check — not a tree
+walk. Curation guard: the registration tables are *curated*, so a newly-added dir may legitimately be
+unlisted (e.g. `src/test/fixtures/`) — a **dead row** (points at a gone dir) is high-confidence; a
+**missing row** is advisory, flag only a dir that clearly belongs.
+
+## Step 5 — Fix (default mode: docs-only)
+Edit ONLY `CLAUDE.md` / `README.md` in affected dirs to correct findings. Keep edits minimal and match
+the existing doc voice/structure.
+HARD RULES — never violate:
+- Never edit code, tests, config, package.json, or any file other than those two doc files in affected dirs.
+- Never touch the root `README.md` or root `CLAUDE.md`, anything under `.claude/` (except your watermark file), or `process/`.
+- Registration-table fixes land in `src/README.md` / `src/CLAUDE.md` (both live in `src/`, an affected dir, so they're editable); root `CLAUDE.md` table drift is **reported, never edited**.
+- Use git for READS only; never run a mutating git command (no add/commit/checkout/reset/tag/stash).
+(If the task says "report only", skip edits and list the exact change you'd make per finding instead.)
+
+## Step 6 — Advance the watermark
+Write `git rev-parse HEAD` to `.claude/agents/state/doc-sync-watermark.txt`, creating the dir/file if
+needed. Uncommitted changes have no SHA and are intentionally re-checked each run until committed; a
+freshly committed change may be re-reviewed once — acceptable and safe.
+
+## Output
+Return a concise report:
+1. Review range: BASE..HEAD (+ uncommitted), or "seeded fresh".
+2. Affected dirs (or "none").
+3. Per dir: findings grouped by category (Stale / Drift / Gap / Orphan / Broken invariant) with
+ `file:line`, and what you changed (or proposed).
+ - Plus: relocations reconciled (old-dir Orphan + new-dir Gap) and interconnection findings (registration
+ tables / cross-doc links), with root `CLAUDE.md` items marked **report-only**.
+4. New watermark SHA written.
+5. One-line verdict.
+Stop and ask only if a finding implies a CODE bug (not a doc fix) or the right doc fix is genuinely ambiguous.
diff --git a/.claude/skills/qa-doc-sync/SKILL.md b/.claude/skills/qa-doc-sync/SKILL.md
new file mode 100644
index 0000000..9a95ae2
--- /dev/null
+++ b/.claude/skills/qa-doc-sync/SKILL.md
@@ -0,0 +1,212 @@
+---
+name: qa-doc-sync
+version: 1.0.0
+description: Use to keep the qa/ documentation web in sync after editing manual-QA checklists or fixtures, or before publishing. When a checklist under qa/checklists/ is added, removed, or updated, propagate the change across every interconnected qa/ doc — the inventory + case-count + scope tables in qa/checklists/README.md and CLAUDE.md, the language inventory + Layout tree in qa/README.md and qa/CLAUDE.md, the languages + file-count tables in qa/workspace/README.md, and each qa/workspace/{lang}/ README/CLAUDE — and verify the one-way checklist→workspace 1:1 fixture pairing. Locates /_rot (stale / drift / gap / orphan / broken-invariant) across the qa/ markdown. Report/draft by default; review the cascade before it lands. Use this whenever you touch qa/ checklists or fixtures, run a pre-publish QA-doc audit, or say "sync the qa docs" / "the qa docs are stale" — even without the words "doc-sync". Never re-derives PROFILE.md from src/ (human-gated) and never ticks checklist [ ] boxes. Triggers - "sync the qa docs", "I edited a checklist, propagate it", "audit qa/ before release", "qa counts look off".
+---
+
+# QA Doc Sync
+
+Keep the **qa/ documentation web** truthful against the qa/ contents. A single checklist edit ripples
+into a fixed set of inventory tables, scope cells, count columns, a Layout tree, and a 1:1 fixture map.
+**Core principle:** the propagation shape is already written down in [`qa/CLAUDE.md`](../../../qa/CLAUDE.md)
+("Propagation rule — checklist changes ripple outward"). This skill *executes that known cascade* and
+flags `/_rot` along it — it does not re-invent what to check. That's why it's a skill, not an
+open-ended audit: the targets are enumerated; the work is doing them correctly and completely.
+
+This is the qa/ counterpart to `scaffold-doc-pair` (src/ doc generation) and the `doc-sync-auditor`
+agent (src/ doc upkeep). It owns qa/ only.
+
+## When to use
+
+- You just **added / removed / updated a checklist** under `qa/checklists/` and need the change to
+ ripple outward (the most common trigger).
+- You **added a new language** to the QA suite (new checklist + workspace dir).
+- You're doing a **pre-publish QA-doc audit** and want the whole web checked at once.
+- A count or scope cell **looks stale** ("the README says 55 cases but general.md has more now").
+
+**When NOT to use (refuse or redirect these):**
+
+- **PROFILE.md ↔ src/ faithfulness.** Whether `qa/_authoring/PROFILE.md` still reflects current `src/`
+ behavior is **human-gated re-derivation** ([`qa/_authoring/README.md`](../../../qa/_authoring/README.md),
+ "Stability rules") — explicitly *not* auto-detected, and owned by the checklist-verify concern. This
+ skill never reads `src/` to re-derive a checklist. It keeps qa/ docs ↔ qa/ *contents* in sync.
+- **`qa/demo-workspace/`** — a standalone framework sandbox that sits *outside* the checklist↔workspace
+ model ([`qa/CLAUDE.md`](../../../qa/CLAUDE.md), "demo-workspace"). Never audit or edit it here.
+- **Ticking checklist boxes** — see Hard constraint #1. If the request is "tick off what I tested,"
+ that's the human's job in the Extension Development Host, not this skill's.
+
+## The qa/ sync model (background)
+
+Two subtrees: `checklists/` (what to test) and `workspace/` (fixtures to test with), plus inventory
+docs that index them. The contract is **one-way**:
+
+- **The checklist is the source of truth.** Checklist changed → the workspace and the inventory docs
+ must catch up.
+- **Workspace-only changes do NOT propagate back.** A fixture that no checklist references is fine —
+ convenience files and future-test prep are allowed. Never edit a checklist to match stray fixtures.
+
+The propagation cascade (from [`qa/CLAUDE.md`](../../../qa/CLAUDE.md), verbatim intent): a checklist add/
+remove/update ripples to `checklists/{README,CLAUDE}.md` → `qa/{README,CLAUDE}.md` → `workspace/README.md`
+→ `workspace/{lang}/` fixtures + `workspace/{lang}/{README,CLAUDE}.md`.
+
+## Hard constraints (always enforced)
+
+These are the ways this skill can do real damage. Each is here because the cost of getting it wrong is
+high, not as ceremony.
+
+1. **Never tick `[ ]` boxes inside `checklists/{lang}.md`.** This is the single most dangerous failure
+ mode of the whole QA pipeline ([`qa/_authoring/README.md`](../../../qa/_authoring/README.md),
+ "Two-tier checkbox rule"): the `[ ]` boxes belong to the human running QA in the EDH. Tick one
+ during a doc sync and a QA artifact ships with a phantom "tested" mark. This skill edits inventory /
+ scope / count / mapping **prose and tables** — never a checklist's test-item checkboxes.
+2. **One-way sync.** Checklist → workspace/docs only. An existing-but-unreferenced fixture is *not* a
+ finding. Never "fix" a checklist to mention a stray file.
+3. **Respect the exclusions.** `qa/demo-workspace/` is outside the model; `qa/_authoring/` is frozen
+ upstream (RECIPE + the human-reviewed PROFILE IR) — read it for context if needed, never regenerate
+ from it here.
+4. **Doc-only, report-default, reversible.** Touch only the qa/ inventory/scope docs and (when a
+ checklist demands it) `workspace/{lang}/` fixtures. Never edit `src/`, tests, `package.json`,
+ `PROFILE.md`, or `RECIPE.md`. **Propose edits and get approval before applying.** Everything lands in
+ the working tree (visible in `git diff`); never auto-commit.
+5. **Idempotent.** Re-running on an already-synced tree finds nothing and changes nothing.
+
+## Workflow
+
+### 1. Scope the run
+
+Decide the trigger and let it bound the work:
+- **One/few checklists changed** → audit those languages' cascade only.
+- **Pre-publish / "audit qa/"** → full sweep across all in-scope dirs.
+
+If `git` shows checklist changes, let that be your default scope:
+
+```bash
+git -C "$(git rev-parse --show-toplevel)" status --porcelain qa/checklists/
+git diff --stat -- qa/checklists/ # what changed since last commit
+```
+
+### 2. Pre-flight git check
+
+```bash
+git status --porcelain qa/
+```
+
+- **Clean** → continue.
+- **Dirty** → pause and ask: **stop** (default — let the user commit/stash first so the sync doesn't
+ mix into unrelated staged work) vs **proceed anyway**. Mirrors `scaffold-doc-pair`'s pre-flight.
+
+### 3. Enumerate the web (in-scope doc-dirs)
+
+```bash
+# The doc-dirs this skill owns. EXCLUDE demo-workspace/ (outside the model) and _authoring/ (frozen).
+{ echo qa; echo qa/checklists; echo qa/workspace; ls -d qa/workspace/*/ 2>/dev/null; } \
+ | sed 's:/$::' | grep -vE 'qa/(demo-workspace|_authoring)'
+```
+
+### 4. Detect `/_rot`
+
+Judge each propagation target against the qa/ `/_rot` taxonomy:
+- **Stale** — a count or scope cell no longer matches the checklist (e.g. `Cases` says 55 but a section
+ was added). Note: several `Cases` figures are deliberately approximate (`~65`) — flag a count only
+ when it's clearly wrong, not for ±a few.
+- **Drift** — two places that must agree don't (e.g. the scope cell in `checklists/README.md` vs the
+ one in `qa/README.md` for the same language; a `workspace/README.md` file count vs the actual tree).
+- **Gap** — a new checklist with no row in the inventory tables, or a new fixture dir missing from a map.
+- **Orphan** — a removed/renamed checklist still listed in an inventory table or mapping.
+- **Broken invariant** — a checklist references a fixture path that doesn't exist (the 1:1 pairing), or
+ a per-destination checklist re-tests something `general.md` owns.
+
+Run the **1:1 pairing check** per language — the part that's error-prone by eye. Referenced fixture
+paths are workspace-relative and begin with the language dir name (e.g. `` `typescript/src/foo.ts` ``):
+
+```bash
+# Report fixture paths a checklist references that are MISSING from its workspace (a real finding).
+# Existing-but-unreferenced fixtures are NOT reported — that's the one-way rule.
+LANG="$1" # e.g. typescript | general | scss
+CHK="qa/checklists/${LANG}.md"
+# Pull backtick-delimited tokens (so unicode + spaces in fixture names survive), keep those that are
+# workspace paths (begin with the language dir), and resolve each — incl. `dir/*` glob references.
+grep -oE '`[^`]+`' "$CHK" | tr -d '`' | grep -E "^${LANG}/" | sort -u | while IFS= read -r p; do
+ p="${p%/}" # strip trailing slash on directory refs
+ case "$p" in
+ */\*) d="qa/workspace/${p%/\*}" # `dir/*` glob → the directory itself must exist
+ [ -d "$d" ] || echo "MISSING-DIR: $CHK -> $d/ (glob $p, no such dir)";;
+ *) [ -e "qa/workspace/$p" ] || echo "MISSING: $CHK -> qa/workspace/$p (referenced, no such fixture)";;
+ esac
+done
+```
+
+Backtick extraction beats a bare path-regex here: qa/ fixtures legitimately have **unicode** names
+(`komponent-日本語.ts`), **spaces** (`my folder/spaced.ts`), and **glob** refs (`assets/*`), all of
+which a `[A-Za-z0-9._-]` pattern truncates into phantom misses. It's still a candidate-surfacer, not an
+oracle — confirm each hit by reading the checklist line (a backtick token that isn't really a path, or
+a glob over an empty dir, can still slip through).
+
+### 5. Report + checkpoint
+
+Present findings grouped by **target × `/_rot` category**, each with `file:line`, the current claim, the
+reality, and a concrete **Before / After** edit. Then stop for approval. (Running this skill in
+plan-mode makes this the natural review gate before the cascade lands.) In report/draft mode, stop here.
+
+### 6. Apply (on approval)
+
+Apply the approved edits — doc-only, minimal, matching the existing voice and table shapes. Re-read each
+target before editing and drop any finding that no longer holds. Add/update `workspace/{lang}/` fixtures
+only when a checklist now references a path that doesn't exist (constraint #2's allowed direction).
+
+### 7. Done
+
+```bash
+git status --porcelain qa/
+```
+
+Show exactly what changed, remind the user it's all in `git diff` and revertible, and do **not**
+auto-commit (the maintainer commits docs).
+
+## What this skill maintains
+
+| Target | What stays in sync |
+|--------|--------------------|
+| `qa/checklists/README.md` | `## Inventory` table — **Cases** column + scope cells; the 14-row workspace-counterpart map |
+| `qa/checklists/CLAUDE.md` | `## Files` per-checklist scope rows |
+| `qa/README.md` | `## Layout` tree (checklist one-liners + workspace dirs) + `## Current inventory` table |
+| `qa/CLAUDE.md` | checklist↔workspace sync rule, propagation rule, execution order, "Adding a new language" |
+| `qa/workspace/README.md` | languages table + file counts |
+| `qa/workspace/{lang}/README.md` | file tree, fixture mapping, file counts |
+| `qa/workspace/{lang}/CLAUDE.md` | edit rules — **only** if the change affects them |
+| **1:1 pairing** | every fixture path `checklists/{lang}.md` references exists under `workspace/{lang}/` |
+
+## Common mistakes
+
+- **Ticking a checklist's `[ ]` box.** The most dangerous one. Inventory prose and tables only — never
+ the human's test-item boxes inside `checklists/{lang}.md`.
+- **Back-propagating from the workspace.** A stray fixture is not a checklist gap. Sync is one-way.
+- **Re-deriving PROFILE from src.** Out of scope and out of bounds — that's a human-reviewed freeze.
+- **Touching `demo-workspace/`.** It's outside the model; leave it alone.
+- **Updating one count but not its twin.** A language's scope/count lives in *both* `checklists/README.md`
+ and `qa/README.md` — fix both, or you've just created drift instead of removing it.
+- **Treating approximate counts as exact.** The `~65`-style figures are intentional; don't churn them.
+
+## Verification (for editing this skill)
+
+After any change to this skill, re-verify against the live tree (read-only — no edits):
+1. **Dry-run report mode** over the full qa/ tree; confirm it produces a sane finding list and proposes
+ **zero** checklist-checkbox edits.
+2. **Pairing check** runs for every language and surfaces a genuinely missing fixture if you remove one
+ in a scratch worktree (then discard the worktree).
+3. **Exclusions:** confirm it never lists `qa/demo-workspace/` or proposes touching `qa/_authoring/`
+ (PROFILE/RECIPE).
+
+## Future use
+
+Invoke via Claude Code (`/qa-doc-sync` or "sync the qa docs") after editing checklists/fixtures or before
+a release. Project-local; lives in `.claude/skills/` and is gitignored. Run it in plan-mode when you want
+to review the whole proposed cascade before any edit lands.
+
+## Changelog
+
+### 1.0.0
+- Initial: scope + pre-flight git check, in-scope doc-dir enumeration (excluding demo-workspace/ &
+ _authoring/), `/_rot` detection across the propagation web + the 1:1 checklist→workspace pairing check,
+ report/checkpoint with Before/After, doc-only apply, done summary. Report/draft default; opus. Encodes
+ the propagation rule from `qa/CLAUDE.md`; honors the two-tier checkbox rule and one-way sync.
diff --git a/.claude/skills/scaffold-doc-pair/SKILL.md b/.claude/skills/scaffold-doc-pair/SKILL.md
new file mode 100644
index 0000000..82247a8
--- /dev/null
+++ b/.claude/skills/scaffold-doc-pair/SKILL.md
@@ -0,0 +1,210 @@
+---
+name: scaffold-doc-pair
+version: 1.0.0
+description: Use when adding a new directory under src/ that needs its README.md + CLAUDE.md documentation pair. Generates both files from the project's canonical doc-pair shape, pre-filled from the directory's actual .ts files and exported symbols, then proposes the registration-table edits in root CLAUDE.md, src/README.md, and src/CLAUDE.md. Refuses the src/ root and src/test/ pairs (documented exceptions) and never overwrites an existing pair. Triggers - "scaffold docs for src/", "new src directory needs its README/CLAUDE pair", "add a doc pair for src/foo".
+---
+
+# Scaffold Doc Pair
+
+Generate a `README.md` + `CLAUDE.md` pair for a **new** `src//` from the project's one canonical
+shape, pre-filled from the directory's real files, then register the directory in the project's index
+tables. **Core principle:** the pair has a fixed skeleton; only the prose is per-directory. This skill
+fills the skeleton mechanically so every new directory matches the existing nine.
+
+## When to use
+
+- A new directory was just added under `src/` (e.g. `src/foo/`) and has no docs yet.
+- You're about to hand-write a `README.md`/`CLAUDE.md` for a `src/` directory — use this instead so
+ the shape can't drift.
+
+**When NOT to use (refuse these):**
+- `src/` itself — the root pair is an *index* over the children, not a leaf module (different shape).
+- `src/test/` or any `src/test/**` — the test pair documents a *runner*, not source modules.
+- Any directory that **already has** a `README.md` or `CLAUDE.md` — never clobber existing docs.
+
+## The one shape (background)
+
+Every leaf directory under `src/` carries a pair with a fixed role split:
+- **`README.md`** — navigation / onboarding: a one-line purpose, a `## Files` table, optional topic
+ sections, a `## Where to add new code` section, and a trailing pointer to `CLAUDE.md`.
+- **`CLAUDE.md`** — invariants / gotchas: a one-line purpose+invariant, a `## Files` bullet list,
+ per-file/per-topic deep-dives, and an `## Adding a new ` checklist.
+
+`src/` (index) and `src/test/` (runner) deliberately break this — they are the two documented
+exceptions and this skill refuses to scaffold them.
+
+## Hard constraints (always enforced)
+
+1. **No clobber.** If `README.md` or `CLAUDE.md` already exists in the target, stop. Don't overwrite,
+ don't "merge", don't rename the existing file.
+2. **No source edits.** Files under `src/` are read **only** to extract export signatures for the
+ tables. No `.ts` file is ever modified.
+3. **Refuse the exceptions.** `src/`, `src/test/`, `src/test/**` are never scaffolded.
+4. **Reversible.** Both generated files and any registration edits land in the working tree only —
+ everything is visible in `git diff` and revertible with `git checkout`.
+5. **Idempotent.** Re-running against a directory that already has a pair is a no-op (per #1).
+
+## Workflow
+
+### 1. Validate the target directory
+
+Take the target path (e.g. `src/foo`). Run the guard snippet; abort on any `REFUSE`.
+
+```bash
+DIR="${1%/}" # target dir, trailing slash stripped, e.g. src/foo
+
+case "$DIR" in
+ src|src/|src/test|src/test/*)
+ echo "REFUSE: $DIR is a documented exception (index/runner) — not scaffolded."; exit 1;;
+ src/*) : ;; # ok: a leaf dir under src/
+ *) echo "REFUSE: $DIR is not under src/."; exit 1;;
+esac
+
+if [ -f "$DIR/README.md" ] || [ -f "$DIR/CLAUDE.md" ]; then
+ echo "REFUSE: a doc pair already exists in $DIR (no clobber)."; exit 1
+fi
+echo "OK: $DIR is a scaffoldable leaf directory."
+```
+
+### 2. Pre-flight git check
+
+```bash
+git status --porcelain
+```
+
+- **Clean** → continue.
+- **Dirty** → pause and ask: **stop** (default — let the user commit/stash first, so the scaffold
+ doesn't get mixed into unrelated staged work) vs **proceed anyway**. This mirrors the sibling
+ `upgrade-vscode-extension` skill and avoids the mixed-staging trap.
+
+### 3. Discover files + exports
+
+```bash
+DIR="${1%/}"
+for f in "$DIR"/*.ts; do
+ [ -f "$f" ] || continue
+ case "$f" in *.test.ts) continue;; esac # exclude tests
+ echo "== ${f##*/} =="
+ grep -nE '^export (async )?(function|const|class|type|interface|default|abstract)' "$f" || echo " (no top-level exports found)"
+done
+```
+
+Note `_`-prefixed files (`_foo.ts`) — they are **directory-internal** modules; say so in their row.
+Use the grep output to fill the symbol column (README) and the file bullets (CLAUDE).
+
+### 4. Generate both files from the templates
+
+Fill the templates in **Canonical templates** below:
+- Title + `src//` — mechanical.
+- `## Files` rows — one per discovered `.ts`, with the detected symbol(s).
+- Everything that needs judgment — the one-line purpose, the invariant deep-dives, the
+ `## Adding a new X` steps, and **which symbol-column header** to use — left as an explicit
+ `` slot for the user to fill. Don't invent invariants you can't see in the code.
+
+### 5. Checkpoint
+
+Print **both** generated files in full. Get explicit approval before writing anything.
+
+### 6. Write the pair
+
+Write `README.md` and `CLAUDE.md` into the target directory.
+
+### 7. Register the new directory
+
+A new `src/` directory must be wired into the project's index tables, or it's invisible to the docs.
+Propose concrete edits for each site below; **apply the mechanical rows on approval**, and for the
+**judgment** rows (dependency direction, architecture tree) propose a value and confirm it — the
+layer a directory belongs to is a design decision, not a lookup.
+
+| Site | Section | Edit | Kind |
+|------|---------|------|------|
+| `/CLAUDE.md` (root) | `## Subdirectory guides` table | add a `Directory \| Scope \| Guides` row | mechanical |
+| `/CLAUDE.md` (root) | `## Architecture` tree + "Allowed dependency direction" | add the dir + its allowed imports | judgment |
+| `src/README.md` | `## Layout` table | add a `Path \| Purpose` row | mechanical |
+| `src/README.md` | `## Where to add new code` table | add a row if the dir introduces a new "what" | judgment |
+| `src/CLAUDE.md` | `## Layered architecture` tree + "Allowed dependency direction" list | add the dir + its allowed imports | judgment |
+| `src/CLAUDE.md` | `## Per-directory invariants` table | add a `Directory \| What it owns \| Key invariant` row | judgment |
+
+### 8. Done
+
+Print `git status --porcelain` so the user sees exactly the new + modified files. Remind them
+everything is in `git diff` and revertible. Do **not** auto-commit (the maintainer commits docs).
+
+## Canonical templates
+
+Copy these verbatim, then fill the `<…>` slots. Keep the section skeleton; specialize only the prose.
+
+**`README.md`**
+
+```
+# src//
+
+
+
+## Files
+
+| File | | Purpose |
+|------|----------------------------------------------------------------------------------------------|---------|
+| `.ts` | `` | |
+
+## Where to add new code
+
+- Defer the deep rules to CLAUDE.md.
+
+See [`CLAUDE.md`](CLAUDE.md) (this directory) for .
+```
+
+**`CLAUDE.md`**
+
+```
+# src//CLAUDE.md
+
+
+
+## Files
+
+- `.ts` —
+
+##
+
+
+
+## Adding a new
+
+1.
+```
+
+> Single-file directories: keep the section header `## Files` (plural) even with one entry — that's
+> the canonical form across the tree (e.g. `src/config/`, `src/constants/`).
+
+## Verification (for editing this skill)
+
+This skill is authored under `writing-skills` (TDD-for-skills). After any change, re-verify:
+1. **Dry-run** in a throwaway git worktree against a scratch dir (`src/__scratch__/` with a couple of
+ dummy `.ts` files); diff the generated pair against a real leaf pair (e.g. `src/path/`) for
+ skeleton parity; discard the worktree.
+2. **Guards:** confirm it refuses `src/`, `src/test/`, `src/test/sub`, and a dir that already has a
+ pair; confirm step 7 proposes correct rows for all three sites.
+
+## Common mistakes
+
+- **Inventing invariants.** If you can't see the rule in the code, leave a `` — don't fabricate
+ a sync contract. The deep-dive sections are the maintainer's to fill.
+- **Skipping registration.** A pair with no row in the three index tables is orphaned; the docs won't
+ point to it. Step 7 is not optional.
+- **Flattening semantic section names.** `## Adding a new command` / `## Where to add new code` are
+ the same slot with a dir-appropriate name — keep the slot, pick the fitting name; don't force a
+ single generic header.
+- **Scaffolding an exception.** `src/` and `src/test/` are intentionally different. Refuse them.
+
+## Future use
+
+Invoke via Claude Code (`/scaffold-doc-pair` or "scaffold docs for src/") whenever a new `src/`
+directory needs its pair. Project-local; lives in `.claude/skills/` and is gitignored. To use on
+another VS Code extension with the same convention, copy the folder.
+
+## Changelog
+
+### 1.0.0
+- Initial: validate + refuse-list, pre-flight git check, file/export discovery, template generation,
+ checkpoint, write, and three-site registration. Templates inline; light safety (no audit log).
diff --git a/.claude/workflows/CLAUDE.md b/.claude/workflows/CLAUDE.md
new file mode 100644
index 0000000..327bf68
--- /dev/null
+++ b/.claude/workflows/CLAUDE.md
@@ -0,0 +1,5 @@
+# `.claude/workflows/` — guide
+
+The workflow scripts (`*.js`) live here; their Markdown specs — and the **full guide** — live in
+[`specs/`](specs/). See **[`specs/CLAUDE.md`](specs/CLAUDE.md)** for the pairing convention, spec
+format, the conventions baked into every workflow, and how to add a new one.
diff --git a/.claude/workflows/doc-sync-full.js b/.claude/workflows/doc-sync-full.js
new file mode 100644
index 0000000..441b4db
--- /dev/null
+++ b/.claude/workflows/doc-sync-full.js
@@ -0,0 +1,245 @@
+export const meta = {
+ name: 'doc-sync-full',
+ description: 'Exhaustive pre-publish doc-sync audit of all of src/: enumerate every non-test source file, detect doc drift file-by-file + per-dir + across the four cross-directory sync contracts + an interconnection pass (registration tables & cross-doc links, so adds/removes/relocations don\'t break the doc web), adversarially verify, apply doc-only fixes (one writer per dir; out-of-src findings report-only), run a completeness critic, then advance the doc-sync watermark to HEAD.',
+ whenToUse: 'Defaults to REPORT mode (no writes — proposes edits + runs the completeness critic). Pass args {mode:"fix"} to actually apply doc-only edits and advance the watermark. Run once as a backfill/seed or for a periodic full re-audit; day-to-day use the incremental doc-sync-auditor agent instead.',
+ phases: [
+ { title: 'Enumerate', detail: 'git ls-files → work-list of source files + doc-dirs' },
+ { title: 'Detect', detail: 'one Explore agent per file + one per doc-dir' },
+ { title: 'Contracts', detail: 'one Explore agent per cross-dir sync contract + one interconnection pass (tables & links)' },
+ { title: 'Verify', detail: '3-lens adversarial check per finding' },
+ { title: 'Apply', detail: 'one writer per dir applies doc-only fixes' },
+ { title: 'Complete', detail: 'completeness critic + advance watermark' },
+ ],
+}
+
+// ---------- schemas ----------
+const ENUM_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ files: { type: 'array', items: { type: 'string' } },
+ docDirs: { type: 'array', items: { type: 'string' } },
+ },
+ required: [ 'files', 'docDirs' ],
+}
+const FINDING = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ docDir: { type: 'string' },
+ docFile: { type: 'string', enum: [ 'CLAUDE.md', 'README.md' ] },
+ category: { type: 'string', enum: [ 'Stale', 'Drift', 'Gap', 'Orphan', 'Broken invariant' ] },
+ severity: { type: 'string', enum: [ 'high', 'medium', 'low' ] },
+ docLocation: { type: 'string' },
+ claim: { type: 'string' },
+ reality: { type: 'string' },
+ proposedFix: { type: 'string' },
+ },
+ required: [ 'docDir', 'docFile', 'category', 'claim', 'reality', 'proposedFix' ],
+}
+const FINDINGS_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { findings: { type: 'array', items: FINDING } },
+ required: [ 'findings' ],
+}
+const VERDICT_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ real: { type: 'boolean' },
+ reason: { type: 'string' },
+ refinedFix: { type: 'string' },
+ },
+ required: [ 'real', 'reason' ],
+}
+const APPLY_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ dir: { type: 'string' },
+ edited: { type: 'boolean' },
+ filesTouched: { type: 'array', items: { type: 'string' } },
+ summary: { type: 'string' },
+ },
+ required: [ 'dir', 'summary' ],
+}
+const CRITIC_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ uncovered: { type: 'array', items: { type: 'string' } },
+ unverifiedClaims: { type: 'array', items: { type: 'string' } },
+ notes: { type: 'string' },
+ },
+ required: [ 'uncovered', 'unverifiedClaims' ],
+}
+
+const ROT = 'Categories: Stale (claim no longer true), Drift (doc vs code diverged — especially byte-exact contracts), Gap (code has something the docs do not cover), Orphan (doc references a renamed/deleted file/symbol/path), Broken invariant (a documented invariant/sync-contract is violated).'
+// `args` may arrive as an object OR as a JSON string — normalise both. Default to the SAFE
+// mode (report = no writes); `fix` must be requested explicitly, so an args mishap can never
+// again trigger unintended edits.
+let parsedArgs = args
+if (typeof parsedArgs === 'string') {
+ try { parsedArgs = JSON.parse(parsedArgs) } catch (e) { parsedArgs = {} }
+}
+const MODE = (parsedArgs && parsedArgs.mode === 'fix') ? 'fix' : 'report'
+log(`doc-sync-full starting in ${MODE} mode` + (MODE === 'report' ? ' — NO writes (pass args {mode:"fix"} to apply)' : ' — will edit docs + advance watermark'))
+
+// ---------- Phase 1: Enumerate (deterministic work-list) ----------
+phase('Enumerate')
+const work = await agent(
+ `Build the exhaustive work-list for a doc-sync audit. Use git (read-only).
+ files: every .ts under src/ that is NOT under src/test/ and does not end in .test.ts. Collect from BOTH:
+ - git ls-files 'src/*.ts' 'src/**/*.ts' (tracked)
+ - git ls-files --others --exclude-standard 'src/*.ts' 'src/**/*.ts' (new, uncommitted, not gitignored)
+ Union them, then drop anything under src/test/ or ending in .test.ts.
+ docDirs: every directory under src/ (including 'src' itself) that contains BOTH CLAUDE.md and README.md.
+ Find candidate CLAUDE.md from BOTH git ls-files 'src/CLAUDE.md' 'src/**/CLAUDE.md' AND
+ git ls-files --others --exclude-standard 'src/CLAUDE.md' 'src/**/CLAUDE.md'; confirm a sibling README.md on disk.
+ Return repo-relative paths.`,
+ { label: 'enumerate', phase: 'Enumerate', agentType: 'Explore', schema: ENUM_SCHEMA }
+)
+log(`Work-list: ${work.files.length} files, ${work.docDirs.length} doc-dirs`)
+
+function owningDir(file) {
+ const parts = file.split('/')
+ for (let i = parts.length - 1; i >= 1; i--) {
+ const cand = parts.slice(0, i).join('/')
+ if (work.docDirs.includes(cand)) return cand
+ }
+ return 'src'
+}
+
+// ---------- Phase 2+3: Detect (file + dir + contracts) — all read-only, one parallel batch ----------
+phase('Detect')
+const CONTRACTS = [
+ { key: 'four-site-extension', desc: 'Four-site extension sync: an extension must agree across src/types/file-extension.ts (type union) -> src/constants/extensions.ts (runtime list) -> src/snippets/dispatch.ts -> src/snippets/variants.ts. Check the docs at root CLAUDE.md and the types/constants/snippets doc-pairs describe the CURRENT code at all four sites.' },
+ { key: 'three-site-config', desc: 'Three-site config sync: setting enum strings byte-identical across package.json (contributes.configuration enums) -> src/snippets/_styles.ts -> each per-language switch in src/snippets/languages/. NOTE package.json is OUTSIDE src/. Check the docs match current code at all three sites.' },
+ { key: 'two-site-button', desc: 'Two-site button-label sync: toast button labels in src/editor/notification.ts must match the switch cases in src/commands/copy-file-path.ts (copy-success buttons) and src/commands/reset-import-styles.ts (styles-reset Undo) character-for-character. Check the docs describing this still match the code.' },
+ { key: 'runtime-type-mirror', desc: 'Runtime-type mirror: IMAGE_FILE_EXTENSIONS mirrors ImageFileExtension; TEXT_TRACK_FILE_EXTENSIONS mirrors TextTrackFileExtension; MEDIA_FILE_EXTENSIONS is video+audio only (.vtt lives in TEXT_TRACK). Check src/constants and src/types docs match current code.' },
+]
+
+const detectThunks = [
+ ...work.files.map(file => () => {
+ const dir = owningDir(file)
+ return agent(
+ `Doc-sync DETECTION for ONE source file. READ-ONLY.\n` +
+ `File: ${file}\nOwning doc-dir: ${dir} (docs: ${dir}/CLAUDE.md, ${dir}/README.md)\n` +
+ `Read the file and those two doc files. Report every way THIS FILE makes the docs wrong. ${ROT}\n` +
+ `Lens: CLAUDE.md = conventions/sync-rules/gotchas/dependency-direction; README.md = file inventory/public exports/"where to add code".\n` +
+ `Each finding: docDir, docFile, category, severity, docLocation (file:line or quote), claim, reality, concrete proposedFix. None? return empty findings.`,
+ { label: `file:${file}`, phase: 'Detect', agentType: 'Explore', schema: FINDINGS_SCHEMA }
+ )
+ }),
+ ...work.docDirs.map(dir => () =>
+ agent(
+ `Doc-sync DETECTION for a WHOLE directory (catches what one file can't: inventory completeness, orphans, "where to add code"). READ-ONLY.\n` +
+ `Directory: ${dir}. List its real source files (git ls-files '${dir}/*.ts'; for src/ also extension.ts/gating.ts; for src/snippets also languages/*). Read ${dir}/CLAUDE.md and ${dir}/README.md.\n` +
+ `Find: files in code but missing from the README inventory (Gap); files/symbols named in docs but gone (Orphan); stale conventions; drifted contracts. ${ROT}\n` +
+ `Each finding: docDir, docFile, category, severity, docLocation, claim, reality, proposedFix.`,
+ { label: `dir:${dir}`, phase: 'Detect', agentType: 'Explore', schema: FINDINGS_SCHEMA }
+ )
+ ),
+ ...CONTRACTS.map(c => () =>
+ agent(
+ `Doc-sync DETECTION for a CROSS-DIRECTORY sync contract (a single-file pass cannot see these). READ-ONLY.\n` +
+ `Contract: ${c.desc}\n` +
+ `Read the code at EVERY site and the docs that describe it. Report any place the docs are stale/drifted/wrong about the contract, or where the contract itself looks broken in code (category "Broken invariant" — do NOT edit code). ${ROT}\n` +
+ `Each finding: docDir (the doc needing the fix), docFile, category, severity, docLocation, claim, reality, proposedFix.`,
+ { label: `contract:${c.key}`, phase: 'Contracts', agentType: 'Explore', schema: FINDINGS_SCHEMA }
+ )
+ ),
+ // interconnection: the doc WEB itself — registration tables + cross-doc links vs the live doc-dir set.
+ // (A single-file/single-dir pass can't see a dead table row or a broken cross-link from a move/rename.)
+ () => agent(
+ `Doc-sync DETECTION for the INTERCONNECTION layer — the registration tables and cross-doc links that wire the doc web together. READ-ONLY.\n` +
+ `Live doc-dir set (ground truth): ${work.docDirs.join(', ')}.\n` +
+ `Registration sites: root CLAUDE.md (the "Subdirectory guides" table), src/README.md, src/CLAUDE.md. ` +
+ `Check each: a row pointing at a renamed/removed dir = Orphan (high-confidence); a live dir absent from a table = Gap (ADVISORY — the tables are curated, e.g. src/test/fixtures is intentionally unlisted; only flag a dir that clearly belongs). ` +
+ `Also verify relative [text](path) and [[name]] links in every CLAUDE.md/README.md resolve to a real file (broken target = Orphan).\n` +
+ `For any finding in root CLAUDE.md set docDir to "." (it is outside src/ and will be report-only). ${ROT}\n` +
+ `Each finding: docDir, docFile, category, severity, docLocation, claim, reality, proposedFix.`,
+ { label: 'interconnect', phase: 'Contracts', agentType: 'Explore', schema: FINDINGS_SCHEMA }
+ ),
+]
+
+const detection = await parallel(detectThunks) // barrier: need all findings before dedup
+const raw = detection.filter(Boolean).flatMap(r => r.findings || [])
+const seen = new Set()
+const deduped = []
+for (const f of raw) {
+ const k = [ f.docDir, f.docFile, f.category, (f.claim || '').slice(0, 80) ].join('|')
+ if (!seen.has(k)) { seen.add(k); deduped.push(f) }
+}
+log(`Detection: ${raw.length} raw -> ${deduped.length} deduped findings`)
+
+// ---------- Phase 4: Verify (3-lens adversarial, majority vote) ----------
+phase('Verify')
+const verified = await parallel(deduped.map(f => () =>
+ parallel([ 'correctness', 'evidence', 'reproduce' ].map(lens => () =>
+ agent(
+ `Adversarially verify a doc-sync finding via the ${lens} lens. Default real=false unless evidence clearly holds.\n` +
+ `In ${f.docDir}/${f.docFile} [${f.category}] doc claims: "${f.claim}". Reality: "${f.reality}". Proposed fix: "${f.proposedFix}".\n` +
+ `Read the actual doc location AND the actual code. Is this a REAL doc-sync problem warranting the fix? Return real, reason, refinedFix (tightened fix or empty).`,
+ { label: `verify:${f.category}:${f.docDir}`, phase: 'Verify', agentType: 'Explore', schema: VERDICT_SCHEMA }
+ )
+ )).then(votes => {
+ const v = votes.filter(Boolean)
+ const real = v.filter(x => x.real).length >= 2
+ const refinedFix = (v.find(x => x.real && x.refinedFix) || {}).refinedFix || f.proposedFix
+ return { ...f, real, refinedFix }
+ })
+))
+const confirmed = verified.filter(Boolean).filter(f => f.real)
+log(`Confirmed ${confirmed.length}/${deduped.length} after 3-lens verify`)
+
+// ---------- Phase 5: Apply (one writer per dir; disjoint files) ----------
+phase('Apply')
+// Split confirmed findings: writer-owned (a doc-dir under src/) vs out-of-scope (e.g. root CLAUDE.md) -> report-only.
+const byDir = {}
+const reportedOnly = []
+for (const f of confirmed) {
+ if (work.docDirs.includes(f.docDir)) {
+ (byDir[f.docDir] = byDir[f.docDir] || []).push(f)
+ } else {
+ reportedOnly.push(f) // e.g. root CLAUDE.md — outside src/, no writer owns it; never auto-edited
+ }
+}
+const dirs = Object.keys(byDir)
+if (reportedOnly.length) log(`${reportedOnly.length} finding(s) outside src/ (e.g. root CLAUDE.md) — report-only, never auto-edited`)
+const applied = await parallel(dirs.map(dir => () => {
+ const items = byDir[dir].map((f, i) =>
+ `${i + 1}. [${f.category}] ${f.docFile} @ ${f.docLocation || '?'}\n claim: ${f.claim}\n reality: ${f.reality}\n fix: ${f.refinedFix}`
+ ).join('\n')
+ const instruction = MODE === 'report'
+ ? `REPORT ONLY — do NOT edit. For each item give the exact edit you WOULD make (old -> new).`
+ : `Apply the fixes by editing ONLY ${dir}/CLAUDE.md and ${dir}/README.md. Keep edits minimal; match the existing doc voice and structure.`
+ return agent(
+ `You are the SINGLE writer for ${dir}. ${instruction}\n` +
+ `HARD RULES: touch ONLY ${dir}/CLAUDE.md and ${dir}/README.md — never code, tests, package.json, the root README, anything under .claude/, or process/. Re-read the code to confirm each item before applying; drop any that no longer holds.\n` +
+ `Findings:\n${items}`,
+ { label: `apply:${dir}`, phase: 'Apply', agentType: MODE === 'report' ? 'Explore' : undefined, schema: APPLY_SCHEMA }
+ )
+}))
+
+// ---------- Phase 6: Completeness critic + watermark ----------
+phase('Complete')
+const critic = await agent(
+ `Completeness critic for an exhaustive doc-sync audit. Work-list was ${work.files.length} files across ${work.docDirs.length} doc-dirs; ${confirmed.length} findings were confirmed and ${MODE === 'fix' ? 'applied' : 'proposed'}.\n` +
+ `Identify anything possibly MISSED: a source file or doc-dir not represented that plausibly needed checking, any cross-directory contract not fully validated, or any confirmed fix that reads as risky. Return uncovered (paths/contracts) and unverifiedClaims; empty arrays if none.`,
+ { label: 'completeness-critic', phase: 'Complete', agentType: 'Explore', schema: CRITIC_SCHEMA }
+)
+
+let watermark = null
+if (MODE === 'fix') {
+ watermark = await agent(
+ `Advance the doc-sync watermark so the incremental doc-sync-auditor takes over. Run git rev-parse HEAD and write that SHA (single line) to .claude/agents/state/doc-sync-watermark.txt (create dir/file if needed). Git reads only; do NOT commit. Return the SHA written.`,
+ { label: 'advance-watermark', phase: 'Complete' }
+ )
+}
+
+return {
+ mode: MODE,
+ workList: { files: work.files.length, docDirs: work.docDirs.length },
+ findings: { raw: raw.length, deduped: deduped.length, confirmed: confirmed.length },
+ byDir: dirs.map(d => ({ dir: d, count: byDir[d].length })),
+ applied: applied.filter(Boolean),
+ reportedOnly: reportedOnly.map(f => ({ docDir: f.docDir, docFile: f.docFile, category: f.category, docLocation: f.docLocation, claim: f.claim, reality: f.reality, fix: f.refinedFix })),
+ completeness: critic,
+ watermark,
+}
diff --git a/.claude/workflows/release-align.js b/.claude/workflows/release-align.js
new file mode 100644
index 0000000..db36e16
--- /dev/null
+++ b/.claude/workflows/release-align.js
@@ -0,0 +1,216 @@
+// Generated from specs/release-align.md — the spec is the source of truth. If they drift, the doc wins.
+export const meta = {
+ name: 'release-align',
+ description: 'Pre-release consistency gate: read EVERY src/ file to build a complete behaviour inventory, then verify SPEC.md, README.md (Tier-1, claim-by-claim), SUPPORT.md, CLAUDE.md, package.json (self-check) and CHANGELOG.md against it, audit .vscodeignore against the real packaged file list (vsce ls), sweep the rest of root, and emit one consolidated report. Report-only by default; pass args {mode:"fix"} to apply doc-only edits (never package.json).',
+ whenToUse: 'Run before a release (or to check drift). Day-to-day per-commit doc upkeep is the doc-sync-auditor agent. Defaults to report-only (no writes); args {mode:"fix"} applies doc-only edits + lets the changelog step write its Unreleased section.',
+ phases: [
+ { title: 'Ground truth', detail: 'read every src/ file → complete inventory' },
+ { title: 'Align', detail: 'each doc vs the inventory (Tier-1 = claim-by-claim)' },
+ { title: 'Changelog', detail: 'dispatch the changelog-drafter agent' },
+ { title: 'Package', detail: 'vsce ls vs the ship/don\'t-ship policy' },
+ { title: 'Root', detail: 'sweep the remaining root files' },
+ { title: 'Synthesize', detail: 'verify Tier-1 findings + consolidated report (+ apply in fix mode)' },
+ ],
+}
+
+// ---------- mode (robust parse; SAFE default = report) ----------
+let parsedArgs = args
+if (typeof parsedArgs === 'string') {
+ try { parsedArgs = JSON.parse(parsedArgs) } catch (e) { parsedArgs = {} }
+}
+const MODE = (parsedArgs && parsedArgs.mode === 'fix') ? 'fix' : 'report'
+log(`release-align starting in ${MODE} mode` + (MODE === 'report' ? ' — NO writes (pass args {mode:"fix"} to apply doc edits)' : ' — will apply doc-only edits; package.json never auto-edited'))
+
+// ---------- schemas ----------
+const ENUM_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { files: { type: 'array', items: { type: 'string' } } },
+ required: [ 'files' ],
+}
+const SRC_EXTRACT_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ file: { type: 'string' },
+ userFacing: { type: 'boolean' },
+ commands: { type: 'array', items: { type: 'string' } },
+ settings: { type: 'array', items: { type: 'string' } },
+ languagesOrPairs: { type: 'array', items: { type: 'string' } },
+ behaviours: { type: 'array', items: { type: 'object', additionalProperties: false,
+ properties: { desc: { type: 'string' }, userFacing: { type: 'boolean' } }, required: [ 'desc', 'userFacing' ] } },
+ notes: { type: 'string' },
+ },
+ required: [ 'file', 'userFacing', 'behaviours' ],
+}
+const INVENTORY_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ counts: { type: 'object', additionalProperties: false, properties: {
+ commands: { type: 'number' }, settings: { type: 'number' }, languages: { type: 'number' },
+ keybindings: { type: 'number' }, dropProviders: { type: 'number' },
+ extensions: { type: 'number' }, categories: { type: 'number' },
+ engine: { type: 'string' }, version: { type: 'string' } }, required: [ 'commands', 'settings', 'languages' ] },
+ commands: { type: 'array', items: { type: 'string' } },
+ settings: { type: 'array', items: { type: 'string' } },
+ languages: { type: 'array', items: { type: 'string' } },
+ behaviours: { type: 'array', items: { type: 'string' } },
+ pkgSelfCheck: { type: 'array', items: { type: 'object', additionalProperties: false,
+ properties: { kind: { type: 'string' }, detail: { type: 'string' } }, required: [ 'kind', 'detail' ] } },
+ notes: { type: 'string' },
+ },
+ required: [ 'counts', 'commands', 'settings', 'languages', 'behaviours', 'pkgSelfCheck' ],
+}
+const FINDING = { type: 'object', additionalProperties: false, properties: {
+ category: { type: 'string', enum: [ 'Stale', 'Wrong', 'Gap', 'Drift', 'Count-mismatch' ] },
+ severity: { type: 'string', enum: [ 'high', 'medium', 'low' ] },
+ docLocation: { type: 'string' }, docClaim: { type: 'string' }, reality: { type: 'string' }, proposedFix: { type: 'string' },
+}, required: [ 'category', 'docClaim', 'reality', 'proposedFix' ] }
+const ALIGN_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { file: { type: 'string' }, tier: { type: 'number' }, findings: { type: 'array', items: FINDING } },
+ required: [ 'file', 'findings' ],
+}
+const VERDICT_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { real: { type: 'boolean' }, reason: { type: 'string' }, refinedFix: { type: 'string' } },
+ required: [ 'real', 'reason' ],
+}
+const VSCODEIGNORE_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ vsceAvailable: { type: 'boolean' },
+ shouldNotShipButIncluded: { type: 'array', items: { type: 'string' } },
+ shouldShipButMissing: { type: 'array', items: { type: 'string' } },
+ verdict: { type: 'string' }, notes: { type: 'string' },
+ },
+ required: [ 'shouldNotShipButIncluded', 'shouldShipButMissing', 'verdict' ],
+}
+const ROOT_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { findings: { type: 'array', items: { type: 'object', additionalProperties: false,
+ properties: { file: { type: 'string' }, issue: { type: 'string' }, proposedFix: { type: 'string' }, severity: { type: 'string' } },
+ required: [ 'file', 'issue' ] } } },
+ required: [ 'findings' ],
+}
+const APPLY_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { file: { type: 'string' }, edited: { type: 'boolean' }, summary: { type: 'string' } },
+ required: [ 'file', 'summary' ],
+}
+
+// ---------- Phase 1: Ground truth — read EVERY src file ----------
+phase('Ground truth')
+const enumerated = await agent(
+ `List every non-test TypeScript file under src/ for this repo. Use git: \`git ls-files 'src/*.ts' 'src/**/*.ts'\`, then EXCLUDE anything under src/test/ or ending in .test.ts. Return the repo-relative paths.`,
+ { label: 'enumerate-src', phase: 'Ground truth', agentType: 'Explore', schema: ENUM_SCHEMA }
+)
+log(`Ground truth: reading ${enumerated.files.length} src files exhaustively`)
+
+const extracts = (await parallel(enumerated.files.map(f => () =>
+ agent(
+ `Read the FULL file ${f} and report the USER-FACING surface and behaviour it contributes to this VS Code extension. READ-ONLY.\n` +
+ `Report: commands it registers; settings it reads; languages/extension-pairs it gates or dispatches; notable user-facing behaviours and edge cases (each flagged userFacing true/false). If the file is purely internal (pure helpers, types, test scaffolding) set userFacing=false and say so in notes. Be faithful to the code — do not infer behaviour that isn't there.`,
+ { label: `extract:${f}`, phase: 'Ground truth', agentType: 'Explore', schema: SRC_EXTRACT_SCHEMA }
+ )
+))).filter(Boolean)
+
+const inventory = await agent(
+ `You are building the single canonical inventory of this extension's user-facing surface + behaviour, the source of truth every doc will be checked against.\n` +
+ `Inputs: (a) the per-file extracts below, and (b) read package.json's "contributes", "engines", "version", "activationEvents", and keybindings yourself.\n` +
+ `Produce: counts (commands, settings, languages, keybindings, dropProviders, extensions, categories, engine, version); the explicit lists of commands/settings/languages; a behaviour catalogue (user-facing behaviours only, deduped, one line each); and pkgSelfCheck findings — anything DECLARED in package.json but dead in code, or present in code but UNDECLARED.\n\n` +
+ `Per-file extracts (JSON):\n${JSON.stringify(extracts.map(e => ({ file: e.file, userFacing: e.userFacing, commands: e.commands, settings: e.settings, languagesOrPairs: e.languagesOrPairs, behaviours: e.behaviours })))}`,
+ { label: 'synthesize-inventory', phase: 'Ground truth', agentType: 'Explore', schema: INVENTORY_SCHEMA }
+)
+log(`Inventory: ${inventory.counts.commands} commands, ${inventory.counts.settings} settings, ${inventory.counts.languages} languages, ${inventory.behaviours.length} behaviours, ${inventory.pkgSelfCheck.length} package.json self-check notes`)
+
+// ---------- Phase 2: Align each doc against the inventory ----------
+phase('Align')
+const INV_JSON = JSON.stringify(inventory)
+const DOCS = [
+ { file: 'SPEC.md', tier: 1 },
+ { file: 'README.md', tier: 1 },
+ { file: 'SUPPORT.md', tier: 2 },
+ { file: 'CLAUDE.md', tier: 2 },
+]
+const aligned = (await parallel(DOCS.map(d => () =>
+ agent(
+ `Check ${d.file} against the canonical inventory below. READ-ONLY.\n` +
+ (d.tier === 1
+ ? `TIER 1 — highest emphasis. Go CLAIM BY CLAIM: every statement, table row, count, command, setting, language, keybinding, and described behaviour must be verified against the inventory (and re-read the relevant src file if a behavioural claim needs confirming). Report both directions: doc claims not true of the code (Stale/Wrong), and inventory items the doc OMITS (Gap).`
+ : `Check counts + key claims against the inventory: commands/settings/languages/keybindings, and any described behaviour. Report Stale/Wrong claims and notable Gaps.`) + `\n` +
+ `For each finding: category, severity, docLocation (heading or quote), docClaim, reality (from inventory/code), and a concrete proposedFix. If fully aligned, return an empty findings array.\n\n` +
+ `Canonical inventory (JSON):\n${INV_JSON}`,
+ { label: `align:${d.file}`, phase: 'Align', agentType: 'Explore', schema: ALIGN_SCHEMA }
+ ).then(r => ({ ...r, tier: d.tier }))
+))).filter(Boolean)
+
+// ---------- Phase 3: Changelog (reuse the changelog-drafter agent) ----------
+phase('Changelog')
+const changelog = await agent(
+ (MODE === 'report'
+ ? `REPORT ONLY — do NOT edit CHANGELOG.md. `
+ : `Apply your normal behaviour (you may write the Unreleased section of CHANGELOG.md). `) +
+ `Handle both states: if there is no Unreleased section, draft one from commits since the last release tag; if one exists (e.g. "[1.0.0] - Unreleased"), verify it against src/ + commits and merge in anything missing idempotently (never duplicate, never touch published versions). ` +
+ (MODE === 'report' ? `Output the proposed draft/merge and which entries are new vs already present.` : ``),
+ { label: 'changelog', phase: 'Changelog', agentType: 'changelog-drafter' }
+)
+
+// ---------- Phase 4: .vscodeignore packaging audit ----------
+phase('Package')
+const pkgAudit = await agent(
+ `Audit what the VSIX would actually ship. READ-ONLY. Run \`npx @vscode/vsce ls\` (or \`npx vsce ls\`) to list the files that would be packaged; if vsce isn't available, set vsceAvailable=false and fall back to reasoning from .vscodeignore + the repo tree.\n` +
+ `Policy — MUST ship: dist/, README.md, CHANGELOG.md, LICENSE.md, package.json, and assets referenced by README. MUST NOT ship: src/, out/, tests, qa*/, process/, .claude/, build configs (tsconfig.json, esbuild.js, eslint.config.mjs, .vscode-test.*), *.ts, *.map, and the GitHub-only docs SPEC.md, SUPPORT.md, CLAUDE.md.\n` +
+ `Report: shouldNotShipButIncluded (anything packaged that violates the policy — the user's main worry), shouldShipButMissing (anything required that's absent), and a one-line verdict. Do NOT edit anything.`,
+ { label: 'vscodeignore-audit', phase: 'Package', agentType: 'Explore', schema: VSCODEIGNORE_SCHEMA }
+)
+
+// ---------- Phase 5: Root sweep ----------
+phase('Root')
+const rootSweep = await agent(
+ `Light finalization sweep of the remaining root files, READ-ONLY: LICENSE.md, tsconfig.json, esbuild.js, eslint.config.mjs, .vscode-test.mjs, package-lock.json. Flag anything stale, wrong, or misplaced before shipping (e.g. wrong license/year, build config inconsistent with package.json scripts, lockfile version out of sync with package.json). Report findings with file, issue, proposedFix, severity. Empty if clean.`,
+ { label: 'root-sweep', phase: 'Root', agentType: 'Explore', schema: ROOT_SCHEMA }
+)
+
+// ---------- Phase 6: Verify Tier-1 findings + (fix mode) apply ----------
+phase('Synthesize')
+const tier1Findings = aligned.filter(r => r.tier === 1).flatMap(r => (r.findings || []).map(f => ({ ...f, file: r.file })))
+const verifiedTier1 = await parallel(tier1Findings.map(f => () =>
+ agent(
+ `Adversarially verify this Tier-1 alignment finding. Default real=false unless the evidence clearly holds. Re-read the actual doc location AND the actual code.\n` +
+ `In ${f.file} [${f.category}] — doc claims: "${f.docClaim}". Reality: "${f.reality}". Proposed fix: "${f.proposedFix}". Is this a real misalignment worth fixing? Return real, reason, refinedFix.`,
+ { label: `verify:${f.file}:${f.category}`, phase: 'Synthesize', agentType: 'Explore', schema: VERDICT_SCHEMA }
+ ).then(v => ({ ...f, verified: v }))
+))
+const confirmedTier1 = verifiedTier1.filter(Boolean).filter(f => f.verified && f.verified.real)
+
+let applied = []
+if (MODE === 'fix') {
+ // Apply confirmed doc-only edits, one writer per doc file. NEVER package.json.
+ const byFile = {}
+ for (const r of aligned) {
+ if (r.file === 'package.json') continue
+ const fs = r.tier === 1
+ ? confirmedTier1.filter(x => x.file === r.file).map(x => ({ ...x, proposedFix: (x.verified && x.verified.refinedFix) || x.proposedFix }))
+ : (r.findings || [])
+ if (fs.length) byFile[r.file] = fs
+ }
+ applied = (await parallel(Object.keys(byFile).map(file => () => {
+ const items = byFile[file].map((f, i) => `${i + 1}. [${f.category}] ${f.docLocation || '?'}\n claim: ${f.docClaim}\n reality: ${f.reality}\n fix: ${f.proposedFix}`).join('\n')
+ return agent(
+ `You are the SINGLE writer for ${file}. Apply these verified alignment fixes by editing ONLY ${file}. Keep edits minimal and match the file's existing voice. Re-read the code to confirm each before applying; drop any that no longer holds.\n` +
+ `HARD RULES: edit ONLY ${file}; never package.json, never code/tests, never .claude/ or process/.\n\nFindings:\n${items}`,
+ { label: `apply:${file}`, phase: 'Synthesize', schema: APPLY_SCHEMA }
+ )
+ }))).filter(Boolean)
+}
+
+return {
+ mode: MODE,
+ inventory: { counts: inventory.counts, behaviours: inventory.behaviours.length, pkgSelfCheck: inventory.pkgSelfCheck },
+ alignment: aligned.map(r => ({ file: r.file, tier: r.tier, findings: r.findings })),
+ tier1Verified: { raised: tier1Findings.length, confirmed: confirmedTier1.length },
+ changelog,
+ vscodeignore: pkgAudit,
+ rootSweep: rootSweep.findings,
+ applied,
+ note: MODE === 'report' ? 'Report only — nothing was written. Review, then re-run with args {mode:"fix"} to apply doc edits.' : 'Doc-only edits applied; review via git diff. package.json was NOT auto-edited.',
+}
diff --git a/.claude/workflows/rot-sweep.js b/.claude/workflows/rot-sweep.js
new file mode 100644
index 0000000..541ac56
--- /dev/null
+++ b/.claude/workflows/rot-sweep.js
@@ -0,0 +1,306 @@
+export const meta = {
+ name: 'rot-sweep',
+ description: 'Diff-scoped /_rot audit of the UNCOMMITTED working tree across the product surface (src/, docs/, qa/, root product docs, package.json — .claude/ tooling, process/, build configs excluded by path). Reads only the in-scope files in git status (+ the witnesses they implicate), checks them against the code for the five /_rot categories (Stale, Drift, Gap, Orphan, Broken invariant), and REPORTS — never edits. Self-contained (reuses no local agent/workflow), report-only, all-Opus Explore agents, fan-out batched to dodge the StructuredOutput throttle.',
+ whenToUse: 'Run before committing an accumulated pile of uncommitted changes, to catch doc/contract drift while it is fresh. Re-run after each fix — it only re-examines what is still uncommitted. NOT a completeness gate: for that, run the full-tree /_rot (or doc-sync-full / release-align) before publishing. Report-only — there is no fix mode.',
+ phases: [
+ { title: 'Enumerate', detail: 'git status → uncommitted path set + doc-dirs; bucket in JS' },
+ { title: 'Detect', detail: 'one finder per changed file / implicated contract / Tier-1 partner doc / changelog (runBatched 6)' },
+ { title: 'Verify', detail: 'single-lens refute-by-default per finding (runBatched 6)' },
+ { title: 'Complete', detail: 'completeness critic emits the coverage manifest' },
+ ],
+}
+
+// ---------- schemas (additionalProperties:false everywhere) ----------
+const ENUM_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ changed: {
+ type: 'array',
+ items: {
+ type: 'object', additionalProperties: false,
+ properties: { path: { type: 'string' }, status: { type: 'string' } },
+ required: [ 'path', 'status' ],
+ },
+ },
+ docDirs: { type: 'array', items: { type: 'string' } },
+ },
+ required: [ 'changed', 'docDirs' ],
+}
+const FINDING = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ category: { type: 'string', enum: [ 'Stale', 'Drift', 'Gap', 'Orphan', 'Broken invariant' ] },
+ severity: { type: 'string', enum: [ 'high', 'medium', 'low' ] },
+ surface: { type: 'string' }, // the file/area the finding is about
+ location: { type: 'string' }, // file:line for the doc claim
+ claim: { type: 'string' }, // what the doc says
+ reality: { type: 'string' }, // what the code does (source of truth)
+ proposedFix: { type: 'string' }, // described, never applied
+ tags: { type: 'array', items: { type: 'string' } }, // mechanical|behavioral, uncommitted|committed
+ },
+ required: [ 'category', 'surface', 'claim', 'reality', 'proposedFix' ],
+}
+const FINDINGS_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { findings: { type: 'array', items: FINDING } },
+ required: [ 'findings' ],
+}
+const VERDICT_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: { real: { type: 'boolean' }, reason: { type: 'string' }, refinedFix: { type: 'string' } },
+ required: [ 'real', 'reason' ],
+}
+const MANIFEST_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ properties: {
+ read: { type: 'array', items: { type: 'string' } }, // files actually opened (subjects + witnesses)
+ checked: { type: 'array', items: { type: 'string' } }, // surfaces audited
+ skipped: { type: 'array', items: { type: 'string' } }, // in-scope surfaces/contracts NOT covered + why
+ notes: { type: 'string' },
+ },
+ required: [ 'read', 'checked', 'skipped' ],
+}
+
+const ROT = 'Categories: Stale (a doc statement no longer true), Drift (doc and code diverged — especially byte-exact cross-site contracts), Gap (code has something the docs do not cover), Orphan (a doc/table/link references a renamed/deleted file/symbol/path, or a new file nothing references), Broken invariant (a documented invariant/count/sync-contract is violated, e.g. the "seven commands" count or the dependency-direction rule).'
+const READONLY = 'READ-ONLY. Do NOT edit, create, or delete any file. Use git for reads only (never a mutating git command).'
+
+// The four cross-site contracts (encoded — this is rot-sweep's knowledge of the CODE's architecture,
+// per the spec's "Deliberately self-contained" decision; root CLAUDE.md is the canonical prose). A
+// contract finder runs only if the uncommitted set intersects its member files / dirs.
+const CONTRACTS = [
+ { key: 'four-site-extension', members: [ 'src/types/file-extension.ts', 'src/constants/extensions.ts', 'src/snippets/dispatch.ts', 'src/snippets/variants.ts' ],
+ desc: 'Four-site extension sync: a file extension must agree across src/types/file-extension.ts (type union) -> src/constants/extensions.ts (runtime list) -> src/snippets/dispatch.ts -> src/snippets/variants.ts. (Non-script asset sources into JSX/TSX/MDX route through src/snippets/_react.ts:buildAssetImportStatement instead of dispatch.ts; only JSX/TSX/MDX-exclusive sources like fonts skip the constants gating table — images/media/docs/components also target gated destinations and keep their constants entries.)' },
+ { key: 'three-site-config', members: [ 'package.json', 'src/snippets/_styles.ts' ], dirs: [ 'src/snippets/languages/' ],
+ desc: 'Three-site config sync: setting enum strings byte-identical across package.json (contributes.configuration enums) -> src/snippets/_styles.ts -> each per-language switch in src/snippets/languages/. Four dormant single-shape keys (cssImage, scssImage, htmlStyleSheet, markdown) are kept in package.json for back-compat but NOT style-synced at runtime — do not flag those as drift.' },
+ { key: 'two-site-button', members: [ 'src/editor/notification.ts', 'src/commands/copy-file-path.ts', 'src/commands/reset-import-styles.ts' ],
+ desc: 'Two-site button-label sync: toast action button labels in src/editor/notification.ts must match the switch cases in src/commands/copy-file-path.ts (copy-success buttons) and src/commands/reset-import-styles.ts (styles-reset Undo) character-for-character.' },
+ { key: 'runtime-type-mirror', members: [ 'src/constants/extensions.ts', 'src/types/file-extension.ts' ],
+ desc: 'Runtime-type mirror: IMAGE_FILE_EXTENSIONS mirrors ImageFileExtension; TEXT_TRACK_FILE_EXTENSIONS mirrors TextTrackFileExtension; MEDIA_FILE_EXTENSIONS is video+audio only (.vtt lives in TEXT_TRACK_FILE_EXTENSIONS); both spread together into destination lists. In src/constants/extensions.ts <-> src/types/file-extension.ts.' },
+]
+
+// ---------- helpers ----------
+async function runBatched(thunks, size, label) {
+ const out = []
+ const total = Math.ceil(thunks.length / size) || 1
+ for (let i = 0; i < thunks.length; i += size) {
+ const slice = thunks.slice(i, i + size)
+ log(`${label}: wave ${Math.floor(i / size) + 1}/${total} (${slice.length} agents)`)
+ const res = await parallel(slice)
+ out.push(...res)
+ }
+ return out
+}
+function owningDir(file, docDirs) {
+ const parts = file.split('/')
+ for (let i = parts.length - 1; i >= 1; i--) {
+ const cand = parts.slice(0, i).join('/')
+ if (docDirs.includes(cand)) return cand
+ }
+ return 'src'
+}
+function contractImplicated(c, paths) {
+ if (c.members.some(m => paths.includes(m))) return true
+ if (c.dirs && c.dirs.some(d => paths.some(p => p.startsWith(d)))) return true
+ return false
+}
+
+// ---------- args: REPORT-ONLY by design (no fix path; the args-mishap footgun cannot exist here) ----------
+let parsedArgs = args
+if (typeof parsedArgs === 'string') { try { parsedArgs = JSON.parse(parsedArgs) } catch (e) { parsedArgs = {} } }
+parsedArgs = parsedArgs || {}
+const MODE = 'report'
+log('rot-sweep starting — REPORT ONLY, diff-scoped to the uncommitted working tree (writes nothing).')
+
+const FULL_PASS_POINTER = 'Diff-scoped — NOT a completeness guarantee. For that, run the full-tree /_rot (or doc-sync-full / release-align) before publishing.'
+const OUT_OF_SCOPE_NOTE = 'Packaging / .vscodeignore audit is out of scope — owned by release-align. rot-sweep writes nothing; the caller persists this report (optionally to the gitignored .claude/rot-sweep.audit.md).'
+
+// ---------- Phase 1: Enumerate ----------
+phase('Enumerate')
+const work = await agent(
+ `Enumerate the UNCOMMITTED working-tree change set for a diff-scoped rot sweep. ${READONLY} git reads only.\n` +
+ `1. Run: git status --porcelain=v1 . Parse each line into {path, status} where status is the 2-char XY code (e.g. " M", "M ", "MM", "A ", "D ", "??", "R "). For a rename line ("R old -> new") record the NEW path with status "R" (put "new <- old" in path if helpful). Include untracked ("??"). git status already omits gitignored paths — keep it that way.\n` +
+ `2. docDirs: every directory under src/ (including "src" itself) that contains BOTH CLAUDE.md and README.md. Find candidate CLAUDE.md via git ls-files 'src/CLAUDE.md' 'src/**/CLAUDE.md' AND git ls-files --others --exclude-standard for untracked ones; confirm a sibling README.md on disk.\n` +
+ `Return changed ({path,status}[]) and docDirs (repo-relative dir paths).`,
+ { label: 'enumerate', phase: 'Enumerate', agentType: 'Explore', model: 'opus', schema: ENUM_SCHEMA }
+)
+
+const changed = (work && work.changed) || []
+const docDirs = (work && work.docDirs) || []
+const allPaths = changed.map(c => c.path)
+
+if (!allPaths.length) {
+ log('No uncommitted changes — nothing to sweep.')
+ return {
+ mode: 'report',
+ scope: { changedFiles: 0, inScope: 0, finderTargets: 0 },
+ findings: { total: 0, byCategory: {}, bySurface: {}, items: [] },
+ coverage: { read: [], checked: [], skipped: [], notes: 'Empty working tree — nothing in scope.', fullPassPointer: FULL_PASS_POINTER },
+ note: OUT_OF_SCOPE_NOTE,
+ }
+}
+
+// ---------- scope = the PRODUCT surface (EXPLICIT), NOT "whatever is not gitignored" ----------
+// A .gitignore edit must never silently move what this tool audits (the failure that motivated this rule).
+// .claude/ tooling, process/, .vscode/, root build configs etc. are out of scope BY PATH — even if tracked.
+const ROOT_DOCS = [ 'README.md', 'SPEC.md', 'SUPPORT.md', 'CLAUDE.md', 'CHANGELOG.md' ]
+const isRootDoc = p => ROOT_DOCS.includes(p)
+const isSrcCode = p => p.startsWith('src/') && p.endsWith('.ts') && !p.startsWith('src/test/') && !p.endsWith('.test.ts')
+const isDocs = p => p.startsWith('docs/')
+const isQa = p => p.startsWith('qa/')
+const inProductScope = p => isRootDoc(p) || p === 'package.json' || isDocs(p) || isQa(p) || p.startsWith('src/')
+
+const paths = allPaths.filter(inProductScope)
+const outOfScope = allPaths.filter(p => !inProductScope(p))
+if (outOfScope.length) {
+ const claudeN = outOfScope.filter(p => p.startsWith('.claude/')).length
+ log(`Out of product scope — skipped ${outOfScope.length} path(s)${claudeN ? ` (incl. ${claudeN} under .claude/ tooling)` : ''}. rot-sweep audits only src/, docs/, qa/, root product docs, package.json.`)
+}
+if (!paths.length) {
+ log('Only out-of-scope changes (e.g. .claude/ tooling) — nothing in the product surface to sweep.')
+ return {
+ mode: 'report',
+ scope: { changedFiles: allPaths.length, inScope: 0, finderTargets: 0, outOfScope },
+ findings: { total: 0, byCategory: {}, bySurface: {}, items: [] },
+ coverage: { read: [], checked: [], skipped: outOfScope, notes: 'No product-surface changes; pile is only out-of-scope paths (e.g. .claude/).', fullPassPointer: FULL_PASS_POINTER },
+ note: OUT_OF_SCOPE_NOTE,
+ }
+}
+
+// ---------- bucket the in-scope pile (plain JS — string logic only) ----------
+const srcCodeFiles = paths.filter(isSrcCode)
+const rootDocs = paths.filter(isRootDoc)
+const docsFiles = paths.filter(isDocs)
+const qaFiles = paths.filter(isQa)
+const packageJsonChanged = paths.includes('package.json')
+const affectedDirs = Array.from(new Set(
+ paths.filter(p => p.startsWith('src/') && !p.startsWith('src/test/')).map(p => owningDir(p, docDirs))
+))
+const implicated = CONTRACTS.filter(c => contractImplicated(c, paths))
+const hasBehaviouralCodeChange = srcCodeFiles.length > 0
+const hasUserFacingChange = srcCodeFiles.length > 0 || packageJsonChanged
+const hasStructural = changed.some(c => /[ADR?]/.test(c.status || ''))
+
+// ---------- build the finder task list (each = {label, thunk}) ----------
+const F = (label, prompt) => ({
+ label,
+ thunk: () => agent(prompt, { label, phase: 'Detect', agentType: 'Explore', model: 'opus', schema: FINDINGS_SCHEMA }),
+})
+const fields = 'Each finding: category, severity, surface, location (file:line), claim (doc says), reality (code does), proposedFix (described, NOT applied), tags (mechanical|behavioral, uncommitted|committed). Source of truth = the CODE; the doc is what is wrong. None? return empty findings.'
+const taskList = []
+
+for (const f of srcCodeFiles) {
+ const dir = owningDir(f, docDirs)
+ taskList.push(F(`file:${f}`,
+ `Doc-sync DETECTION for ONE changed source file. ${READONLY}\nFile: ${f}\nOwning doc-dir: ${dir} (docs: ${dir}/CLAUDE.md, ${dir}/README.md)\n` +
+ `Read the file's CURRENT on-disk content and those two doc files. Report every way this file's current state makes the docs wrong. Lens: CLAUDE.md = conventions/sync-rules/gotchas/dependency-direction; README.md = inventory/public exports/"where to add code". ${ROT}\n${fields}`))
+}
+for (const d of affectedDirs) {
+ taskList.push(F(`dir:${d}`,
+ `Whole-directory doc check (inventory completeness, orphans — what a single-file pass misses) for a diff-scoped rot sweep. ${READONLY}\nDirectory: ${d} (its code changed in your pile). Read ${d}/CLAUDE.md, ${d}/README.md and the dir's real source files. Find: code files missing from the README inventory (Gap); files/symbols named in docs but gone (Orphan); stale conventions; drifted contracts. ${ROT}\n${fields}`))
+}
+// Tier-1: SPEC + README — claim-by-claim, pulled in even when unchanged if behaviour changed
+const tier1 = (file, reason) => F(`tier1:${file}`,
+ `TIER-1 doc check (claim-by-claim) for a diff-scoped rot sweep. ${READONLY}\nDoc: ${file} (${reason}).\nGo CLAIM BY CLAIM: every statement, count, table row, command, setting, language, keybinding, behaviour — verify against the CURRENT code (re-read the relevant src file for any behavioural claim). Report both directions: doc claims not true of code (Stale/Drift) and code behaviour the doc omits (Gap). ${ROT}\n${fields}`)
+if (rootDocs.includes('SPEC.md')) taskList.push(tier1('SPEC.md', 'in your diff'))
+else if (hasBehaviouralCodeChange) taskList.push(tier1('SPEC.md', 'a behavioural code change can stale it even though you did not edit it'))
+if (rootDocs.includes('README.md')) taskList.push(tier1('README.md', 'in your diff'))
+else if (hasBehaviouralCodeChange) taskList.push(tier1('README.md', 'a behavioural code change can stale it even though you did not edit it'))
+// other root docs (CHANGELOG handled separately below)
+for (const f of rootDocs) {
+ if (f === 'CLAUDE.md' || f === 'SUPPORT.md') {
+ taskList.push(F(`root:${f}`,
+ `Root-doc check for a diff-scoped rot sweep. ${READONLY}\nDoc: ${f}. Verify its counts and key claims (architecture, command list, sync rules, supported pairs) against the CURRENT code. ${ROT}\n${fields}`))
+ }
+}
+// docs/ — changed files directly, plus the design criteria as a Tier-1 partner when behaviour changed
+for (const f of docsFiles) {
+ taskList.push(F(`docs:${f}`,
+ `docs/ design-doc check (changed in your pile) for a diff-scoped rot sweep. ${READONLY}\nDoc: ${f}. Verify its claims against the CURRENT code/behaviour it documents (criteria, decisions, rejection ledgers). ${ROT}\n${fields}`))
+}
+if (hasBehaviouralCodeChange) {
+ taskList.push(F('docs:criteria',
+ `Design-library (docs/) partner check for a diff-scoped rot sweep. ${READONLY}\nA behavioural code change is in your pile (changed: ${srcCodeFiles.join(', ')}). Find the docs/ design files whose subject those changes touch — primarily docs/import-statements/ (criteria, decisions, rejection ledgers) — and check them claim-by-claim against CURRENT behaviour. Only the RELEVANT docs/ files, never the whole tree. ${ROT}\n${fields}`))
+}
+// qa/ — changed files directly, plus the relevant checklist(s) as a partner when behaviour changed
+for (const f of qaFiles) {
+ taskList.push(F(`qa:${f}`,
+ `qa/ file check (changed in your pile) for a diff-scoped rot sweep. ${READONLY}\nFile: ${f}. Check it against the code/behaviour it documents AND its qa-doc cascade (the inventory/count tables in qa README/CLAUDE this file feeds, and the 1:1 checklist->workspace fixture pairing). Bounded to this file + its direct cascade targets. ${ROT}\n${fields}`))
+}
+if (hasBehaviouralCodeChange) {
+ taskList.push(F('qa:relevant',
+ `qa/ checklist relevance check for a diff-scoped rot sweep. ${READONLY}\nA behavioural code change is in your pile (changed: ${srcCodeFiles.join(', ')}). If it affects a per-language builder or shared behaviour, find ONLY the relevant qa/checklists/ checklist(s) (the affected language(s) + general.md) and check whether they cover the change and whether the qa-doc cascade (inventories, counts, 1:1 fixture pairing) is consistent. NEVER read the whole 12-language tree. If no checklisted behaviour is affected, return empty. ${ROT}\n${fields}`))
+}
+// cross-site contracts (only the implicated ones; reads all member sites incl. unchanged witnesses)
+for (const c of implicated) {
+ taskList.push(F(`contract:${c.key}`,
+ `Cross-site CONTRACT check a single-file pass cannot see. ${READONLY}\nContract: ${c.desc}\nYour diff touched a member site, so read the CODE at EVERY site (committed sites = witnesses) and the docs that describe it (root CLAUDE.md "Cross-cutting sync rules" + the relevant per-dir docs). Report any doc stale/drifted about the contract, OR the contract itself broken in code (category "Broken invariant" — do NOT edit code). ${ROT}\n${fields}`))
+}
+// package.json self-check
+if (packageJsonChanged) {
+ taskList.push(F('package.json',
+ `package.json self-check for a diff-scoped rot sweep. ${READONLY}\npackage.json is in your diff. Read it and the src/ that backs it (extension.ts, commands/index.ts, config access). Check contributes (commands, keybindings, configuration) vs what is actually registered/read in src/: anything DECLARED but dead in code, or in code but UNDECLARED; and command/setting/keybinding COUNTS vs the docs' stated numbers (e.g. the "seven commands" invariant). ${ROT}\n${fields}`))
+}
+// CHANGELOG accuracy (propose-only)
+if (hasUserFacingChange) {
+ taskList.push(F('changelog',
+ `CHANGELOG accuracy check for a diff-scoped rot sweep. ${READONLY}\nRead CHANGELOG.md's "## [Unreleased]" section and the uncommitted change set (git diff of the working tree; new/changed commands, settings, behaviours, fixes). Changed: ${paths.join(', ')}.\nTriage the pile's USER-FACING changes vs Unreleased: Gap = a user-facing change Unreleased omits -> propose the exact Added/Changed/Fixed bullet; Stale = an Unreleased bullet that misstates current behaviour -> propose the correction. Never touch published version sections. Propose only. surface = "CHANGELOG.md".\n${fields}`))
+}
+// interconnection (registration tables + cross-doc links) on any structural change
+if (hasStructural) {
+ taskList.push(F('interconnect',
+ `Interconnection check (registration tables + cross-doc links) for a diff-scoped rot sweep. ${READONLY}\nYour pile adds/deletes/renames a file or dir, which can rot the doc web. Live doc-dir set: ${docDirs.join(', ')}. Changed: ${changed.map(c => c.status + ' ' + c.path).join(', ')}.\nCheck registration sites — root CLAUDE.md "Subdirectory guides" table, src/README.md, src/CLAUDE.md: a row pointing at a renamed/removed dir = Orphan (high-confidence); a live dir absent from a table = Gap (ADVISORY — tables are curated, only flag a dir that clearly belongs). Verify relative [text](path) and [[name]] links in the changed docs resolve to real files (broken target = Orphan). For an untracked new file (e.g. README.draft.md), check whether anything references it (unreferenced = Orphan/advisory). ${ROT}\n${fields}`))
+}
+
+log(`Uncommitted: ${paths.length} paths -> ${srcCodeFiles.length} src-code, ${affectedDirs.length} src-dirs, ${rootDocs.length} root-docs, ${docsFiles.length} docs/, ${qaFiles.length} qa/, ${implicated.length} contracts${packageJsonChanged ? ', package.json' : ''}. ${taskList.length} finder agents.`)
+
+// ---------- Phase 2: Detect ----------
+phase('Detect')
+const detection = await runBatched(taskList.map(t => t.thunk), 6, 'Detect')
+const raw = detection.filter(Boolean).flatMap(r => r.findings || [])
+const seen = new Set()
+const deduped = []
+for (const f of raw) {
+ const k = [ f.surface, f.category, (f.claim || '').slice(0, 80) ].join('|')
+ if (!seen.has(k)) { seen.add(k); deduped.push(f) }
+}
+log(`Detection: ${raw.length} raw -> ${deduped.length} deduped findings`)
+
+// ---------- Phase 3: Verify (single-lens, refute-by-default) ----------
+phase('Verify')
+const verified = await runBatched(deduped.map(f => () =>
+ agent(
+ `Adversarially verify ONE diff-scoped rot finding by re-reading the actual files. ${READONLY}\n` +
+ `Surface: ${f.surface} [${f.category}]. Doc claims: "${f.claim}". Reality (code): "${f.reality}". Proposed fix: "${f.proposedFix}".\n` +
+ `Default real=false unless the evidence clearly holds. Re-read the actual doc location AND the actual code. Is this a REAL inconsistency worth reporting? Return real, reason, refinedFix (tightened fix, or empty).`,
+ { label: `verify:${f.category}:${f.surface}`, phase: 'Verify', agentType: 'Explore', model: 'opus', schema: VERDICT_SCHEMA }
+ )
+), 6, 'Verify')
+const confirmed = []
+verified.forEach((v, i) => {
+ if (v && v.real) confirmed.push({ ...deduped[i], refinedFix: (v.refinedFix || deduped[i].proposedFix) })
+})
+log(`Confirmed ${confirmed.length}/${deduped.length} after verify`)
+
+// ---------- Phase 4: Complete (coverage manifest) ----------
+phase('Complete')
+const manifest = await agent(
+ `Completeness critic + COVERAGE MANIFEST for a diff-scoped rot sweep. ${READONLY}\n` +
+ `Uncommitted set: ${paths.join(', ')}.\nFinder targets that ran: ${taskList.map(t => t.label).join(', ')}.\n${confirmed.length} findings confirmed.\n` +
+ `Produce a FACTUAL manifest: read (files actually opened — diff subjects + pulled-in witnesses), checked (surfaces audited), skipped (in-scope surfaces or implicated contracts NOT covered, with why). Do NOT speculate about unread files; only report what was and was not covered. notes: one line max.`,
+ { label: 'coverage-manifest', phase: 'Complete', agentType: 'Explore', model: 'opus', schema: MANIFEST_SCHEMA }
+)
+
+// ---------- assemble (report-only; writes nothing — caller persists) ----------
+const byCategory = {}
+const bySurface = {}
+for (const f of confirmed) {
+ (byCategory[f.category] = byCategory[f.category] || []).push(f)
+ ;(bySurface[f.surface] = bySurface[f.surface] || []).push(f)
+}
+return {
+ mode: 'report',
+ scope: { changedFiles: allPaths.length, inScope: paths.length, outOfScope, finderTargets: taskList.length, implicatedContracts: implicated.map(c => c.key) },
+ findings: { total: confirmed.length, byCategory, bySurface, items: confirmed },
+ coverage: { ...(manifest || { read: [], checked: [], skipped: [], notes: 'manifest agent returned nothing' }), outOfScopeSkipped: outOfScope, fullPassPointer: FULL_PASS_POINTER },
+ note: OUT_OF_SCOPE_NOTE,
+}
diff --git a/.claude/workflows/spec-sync.js b/.claude/workflows/spec-sync.js
new file mode 100644
index 0000000..7ddaf56
--- /dev/null
+++ b/.claude/workflows/spec-sync.js
@@ -0,0 +1,216 @@
+export const meta = {
+ name: 'spec-sync',
+ description: 'Audit the existing SPEC.md against the code and report (or, in fix mode, apply) gaps and corrections. Full-read opus agent per source file (extract -> verify) + package.json, cross-file contract agents, then per-file/per-contract audit of SPEC.md, adversarial finding-verify. Report-only by default; fix mode edits ONLY SPEC.md.',
+ whenToUse: 'Heavy/rare deep audit of SPEC.md completeness & accuracy vs src/. Caller passes args {sourceFiles, mode} (audit agents read the live ./SPEC.md; legacy args.specText is optional/ignored). Defaults to report (no writes). args {mode:"fix"} applies confirmed findings to SPEC.md via a single writer. Never per-commit; complements release-align (the broad release sweep).',
+ phases: [
+ { title: 'Extract', detail: 'full-read opus agent per source file + package.json (extract -> verify)' },
+ { title: 'Contracts', detail: 'one opus agent per cross-file contract' },
+ { title: 'Audit', detail: 'check SPEC.md against the code inventory + contracts -> findings' },
+ { title: 'Verify', detail: 'adversarial refute-by-default per finding' },
+ { title: 'Apply', detail: 'fix mode only: single writer edits SPEC.md' },
+ ],
+}
+
+// `args` may arrive as an object OR a JSON string — normalise both, and default to the SAFE
+// report mode (doc-sync lesson: an args mishap once defaulted a run to fix and edited files).
+let a = args
+if (typeof a === 'string') { try { a = JSON.parse(a) } catch (e) { a = {} } }
+a = a || {}
+const sourceFiles = a.sourceFiles || []
+const MODE = a.mode === 'fix' ? 'fix' : 'report'
+if (!sourceFiles.length) throw new Error('spec-sync: args.sourceFiles is empty — caller must enumerate via git ls-files and pass them in.')
+// SPEC.md is read LIVE (./SPEC.md) by the audit agents, not embedded as a 25KB arg — avoids transcription/staleness skew. args.specText is optional and ignored.
+log(`spec-sync starting in ${MODE} mode — ${sourceFiles.length} source files + package.json` + (MODE === 'report' ? ' — NO writes (pass args {mode:"fix"} to apply to SPEC.md)' : ' — will edit SPEC.md only'))
+
+// ---------- schemas ----------
+const INVENTORY_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ required: ['file','exports','functions','variables','types','branches','behaviors','configReads','gating','notifications','edgeCases'],
+ properties: {
+ file: { type: 'string' },
+ exports: { type: 'array', items: { type: 'string' }, description: 'every exported symbol (for package.json: every command/keybinding/setting id)' },
+ functions: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['name','what'], properties: { name: { type: 'string' }, what: { type: 'string' } } }, description: 'every function/method incl. private and _-prefixed' },
+ variables: { type: 'array', items: { type: 'string' }, description: 'every module-level variable/constant' },
+ types: { type: 'array', items: { type: 'string' }, description: 'every type/interface/union declared' },
+ branches: { type: 'array', items: { type: 'string' }, description: 'every meaningful branch and what it decides' },
+ behaviors: { type: 'array', items: { type: 'string' }, description: 'user-observable behaviors produced' },
+ configReads: { type: 'array', items: { type: 'string' }, description: 'settings/config keys read (for package.json: settings declared)' },
+ gating: { type: 'array', items: { type: 'string' }, description: 'gating/validation clauses enforced' },
+ notifications: { type: 'array', items: { type: 'string' }, description: 'toasts / messages / errors shown' },
+ edgeCases: { type: 'array', items: { type: 'string' }, description: 'edge cases handled' },
+ },
+}
+const VERIFY_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ required: ['file','accurate','corrections','missed'],
+ properties: {
+ file: { type: 'string' },
+ accurate: { type: 'boolean' },
+ corrections: { type: 'array', items: { type: 'string' }, description: 'inventory claims that were wrong, corrected' },
+ missed: { type: 'array', items: { type: 'string' }, description: 'symbols/behaviors/branches the inventory missed' },
+ },
+}
+const CONTRACT_SCHEMA = {
+ type: 'object', additionalProperties: false,
+ required: ['key','title','sites','rule'],
+ properties: {
+ key: { type: 'string' }, title: { type: 'string' },
+ sites: { type: 'array', items: { type: 'string' }, description: 'each participating file/location + its concrete current value' },
+ rule: { type: 'string', description: 'the contract exactly as the code enforces it today' },
+ },
+}
+const FINDING = {
+ type: 'object', additionalProperties: false,
+ required: ['specSection','category','claim','reality','proposedFix'],
+ properties: {
+ specSection: { type: 'string', description: 'best-matching SPEC.md heading, or "ABSENT — no section"' },
+ location: { type: 'string', description: 'quote/line hint in SPEC.md, or "absent"' },
+ category: { type: 'string', enum: ['Gap','Stale','Drift','Broken invariant'] },
+ severity: { type: 'string', enum: ['high','medium','low'] },
+ claim: { type: 'string', description: 'what SPEC.md currently says (empty if absent)' },
+ reality: { type: 'string', description: 'what the code/manifest actually does' },
+ proposedFix: { type: 'string', description: 'concrete add/replace text for SPEC.md' },
+ },
+}
+const FINDINGS_SCHEMA = { type: 'object', additionalProperties: false, required: ['findings'], properties: { findings: { type: 'array', items: FINDING } } }
+const VERDICT_SCHEMA = { type: 'object', additionalProperties: false, required: ['real','reason'], properties: { real: { type: 'boolean' }, reason: { type: 'string' }, refinedFix: { type: 'string' } } }
+
+const READONLY = 'READ-ONLY: do not edit or write any file.'
+const ROT = 'Categories: Gap (code has a behavior/symbol/setting SPEC.md does not document), Stale (a SPEC.md claim no longer true), Drift (SPEC.md and code diverged — esp. byte-exact contract values), Broken invariant (a documented invariant/contract is violated in code — report only, never edit code).'
+
+// Audit/Verify run in small sequential waves, NOT one wide parallel() barrier: after ~80 prior agents and
+// >1M tokens, a single ~42-wide barrier of large schema prompts throttles the API and agents return prose
+// instead of calling StructuredOutput (observed 2026-05-31: 25/43 audit agents failed). Small batches keep
+// the instantaneous rate under the throttle. See specs/spec-sync.md (Decisions).
+const EXTRACT_BATCH = 6, AUDIT_BATCH = 6, VERIFY_BATCH = 6
+async function runBatched(thunks, size, label) {
+ const out = []
+ for (let i = 0; i < thunks.length; i += size) {
+ out.push(...await parallel(thunks.slice(i, i + size)))
+ log(`${label} batch ${Math.floor(i / size) + 1}/${Math.ceil(thunks.length / size)} — ${out.filter(Boolean).length}/${out.length} ok`)
+ }
+ return out
+}
+
+// ---------- Phase 1: Extract -> Verify (waves of EXTRACT_BATCH, full-read, opus) ----------
+// Batched (NOT a bare pipeline()) for the SAME reason Audit/Verify are: an un-throttled
+// continuous pipeline of ~10-14 large schema agents trips the API rate limit on its tail
+// (observed 2026-06-04: pipeline[21..36] = 14/37 targets dropped). Small sequential waves
+// keep the instantaneous request rate under the throttle. See specs/spec-sync.md (Decisions).
+phase('Extract')
+const targets = [...sourceFiles, 'package.json']
+const perFile = await runBatched(targets.map((file) => async () => {
+ const inv = await agent(
+ file === 'package.json'
+ ? `${READONLY} Read the ENTIRE \`package.json\`. Produce a COMPLETE inventory of the extension's declared surface: every command (id + title), every keybinding (key + when clause), every activationEvent, every configuration setting (key, type, default, enum values), every contributed menu, and anything else user-facing. Record commands/keybindings/settings under both \`exports\` (ids) and \`behaviors\` (what each does). Do NOT skip anything.`
+ : `${READONLY} Read the ENTIRE file \`${file}\` top to bottom — every line, not excerpts. Also read its sibling \`CLAUDE.md\`/\`README.md\`. Produce a COMPLETE inventory: every export; every function/method incl. private and \`_\`-prefixed; every module-level variable/constant; every type/interface/union; every meaningful branch; and every user-observable behavior, config/setting read, gating/validation clause, toast/notification/error, and edge case. Do NOT summarize or skip — list a symbol even with no behavioral surface.`,
+ { label: `extract:${file}`, phase: 'Extract', model: 'opus', schema: INVENTORY_SCHEMA }
+ )
+ if (inv == null) return null
+ const verify = await agent(
+ `${READONLY} Adversarially verify this inventory for \`${file}\` against the actual code/manifest. Re-read it. Confirm every claimed symbol/behavior/branch is real and accurate — refute by default if unsure. Report \`corrections\` (claims that were wrong) and \`missed\` (anything the inventory failed to capture).\n\nInventory: ${JSON.stringify(inv)}`,
+ { label: `verify:${file}`, phase: 'Extract', model: 'opus', schema: VERIFY_SCHEMA }
+ )
+ return { inventory: inv, verify }
+}), EXTRACT_BATCH, 'Extract')
+const covered = perFile.filter(Boolean)
+log(`Extracted + verified ${covered.length}/${targets.length} targets`)
+
+// ---------- Phase 2: Contracts (parallel barrier) ----------
+phase('Contracts')
+const CONTRACTS = [
+ { key: 'four-site-extension', desc: 'Four-site extension sync: types/file-extension.ts (type union) -> constants/extensions.ts (runtime list) -> snippets/dispatch.ts -> snippets/variants.ts.' },
+ { key: 'three-site-config', desc: 'Three-site config-enum sync: package.json contributes.configuration enums -> snippets/_styles.ts -> each per-language switch in snippets/languages/. (package.json is outside src/.)' },
+ { key: 'two-site-button', desc: 'Two-site button-label sync: toast button labels in editor/notification.ts vs the switch cases in commands/copy-file-path.ts, character-for-character.' },
+ { key: 'runtime-type-mirror', desc: 'Runtime-type mirror: IMAGE_FILE_EXTENSIONS mirrors ImageFileExtension; TEXT_TRACK_FILE_EXTENSIONS mirrors TextTrackFileExtension; MEDIA_FILE_EXTENSIONS = video+audio only (.vtt lives in TEXT_TRACK); both spread into destination lists.' },
+ { key: 'gating-clauses', desc: 'The full source->destination pair gating in gating.ts (isPairSupported): enumerate every clause that accepts or rejects a pair.' },
+ { key: 'layering', desc: 'Dependency-direction / layering rules from src/CLAUDE.md + src/README.md: which layers may import which; the _-prefixed internal modules in snippets/.' },
+]
+const contracts = (await parallel(CONTRACTS.map(c => () =>
+ agent(
+ `${READONLY} Document this cross-file contract EXACTLY as the code enforces it today. Read the code at every site. List each site with its concrete current value, and state the rule precisely.\n\nContract: ${c.desc}`,
+ { label: `contract:${c.key}`, phase: 'Contracts', model: 'opus', schema: CONTRACT_SCHEMA }
+ )
+))).filter(Boolean)
+log(`Documented ${contracts.length}/${CONTRACTS.length} cross-file contracts`)
+
+// ---------- Phase 3: Audit SPEC.md against the verified inventory + contracts ----------
+phase('Audit')
+const auditFileThunks = covered.map(c => () =>
+ agent(
+ `${READONLY} Audit the existing SPEC.md against the VERIFIED ground-truth inventory for \`${c.inventory.file}\`. ${ROT}\n` +
+ `For EVERY behavior / command / keybinding / setting / gating clause / notification / edge case in the inventory, check whether SPEC.md documents it, and correctly. Report every Gap (in inventory, missing from SPEC) and every Stale/Drift (SPEC states something the code contradicts). For each: specSection (best-matching SPEC heading or "ABSENT"), location, category, severity, claim (what SPEC says, or empty), reality (from inventory/code), concrete proposedFix. None? return empty findings.\n\n` +
+ `Inventory: ${JSON.stringify({ ...c.inventory, _verify: c.verify })}\n\nThe audited document is \`./SPEC.md\` — read it IN FULL (top to bottom) and compare against this inventory.`,
+ { label: `audit:${c.inventory.file}`, phase: 'Audit', model: 'opus', schema: FINDINGS_SCHEMA }
+ )
+)
+const auditContractThunks = contracts.map(ct => () =>
+ agent(
+ `${READONLY} Audit how the existing SPEC.md treats this cross-file contract, against its VERIFIED documentation. ${ROT}\n` +
+ `Does SPEC.md describe this contract with the correct concrete values at every site? Report Gaps/Stale/Drift. Finding shape: specSection, location, category, severity, claim, reality, proposedFix.\n\n` +
+ `Contract: ${JSON.stringify(ct)}\n\nThe audited document is \`./SPEC.md\` — read it IN FULL and check how it treats this contract.`,
+ { label: `audit:contract:${ct.key}`, phase: 'Audit', model: 'opus', schema: FINDINGS_SCHEMA }
+ )
+)
+const auditResults = await runBatched([...auditFileThunks, ...auditContractThunks], AUDIT_BATCH, 'Audit')
+const rawFindings = auditResults.filter(Boolean).flatMap(r => r.findings || [])
+const seen = new Set(); const deduped = []
+for (const f of rawFindings) {
+ const k = [f.specSection, f.category, (f.reality || '').slice(0, 80)].join('|')
+ if (!seen.has(k)) { seen.add(k); deduped.push(f) }
+}
+log(`Audit: ${rawFindings.length} raw -> ${deduped.length} deduped findings`)
+
+// ---------- Phase 4: Verify findings (adversarial, refute-by-default) ----------
+phase('Verify')
+const verified = await runBatched(deduped.map(f => () =>
+ agent(
+ `${READONLY} Adversarially verify a SPEC.md audit finding. Default real=false unless the evidence clearly holds. Read the actual code AND the cited SPEC.md location.\n` +
+ `[${f.category}] SPEC section "${f.specSection}" @ ${f.location || '?'} — claim: "${f.claim}". reality (code): "${f.reality}". proposedFix: "${f.proposedFix}".\n` +
+ `Is this a REAL gap/error in SPEC.md that warrants the fix? Return real, reason, refinedFix (tightened, or empty to keep).`,
+ { label: `verify:${f.category}`, phase: 'Verify', model: 'opus', schema: VERDICT_SCHEMA }
+ ).then(v => (v && v.real) ? { ...f, refinedFix: (v.refinedFix || f.proposedFix), verifyReason: v.reason } : null)
+), VERIFY_BATCH, 'Verify')
+const confirmed = verified.filter(Boolean)
+log(`Confirmed ${confirmed.length}/${deduped.length} findings after adversarial verify`)
+
+// ---------- Reconcile (coverage proof) ----------
+const droppedTargets = targets.filter((f, i) => perFile[i] == null)
+const auditUnits = [...covered.map(c => c.inventory.file), ...contracts.map(ct => `contract:${ct.key}`)]
+const droppedAudits = auditUnits.filter((_, i) => auditResults[i] == null)
+if (droppedAudits.length) log(`WARNING: ${droppedAudits.length} audit agent(s) produced nothing (throttle?): ${droppedAudits.join(', ')}`)
+const reconciliation = {
+ targets: targets.length,
+ covered: covered.length,
+ dropped: droppedTargets,
+ contractsCovered: contracts.length,
+ auditUnits: auditUnits.length,
+ auditCompleted: auditUnits.length - droppedAudits.length,
+ droppedAudits,
+ findings: { raw: rawFindings.length, deduped: deduped.length, confirmed: confirmed.length },
+ filesFlaggedInaccurate: covered.filter(c => c.verify && c.verify.accurate === false).map(c => c.inventory.file),
+}
+if (droppedTargets.length) log(`WARNING: ${droppedTargets.length} target(s) produced no inventory: ${droppedTargets.join(', ')}`)
+
+// ---------- Phase 5: Apply (fix mode only — single writer, SPEC.md only) ----------
+let applied = null
+if (MODE === 'fix' && confirmed.length) {
+ phase('Apply')
+ const items = confirmed.map((f, i) =>
+ `${i + 1}. [${f.category}] ${f.specSection} @ ${f.location || '?'}\n reality: ${f.reality}\n fix: ${f.refinedFix}`
+ ).join('\n')
+ applied = await agent(
+ `You are the SINGLE writer for SPEC.md. Apply these confirmed findings by editing ONLY \`SPEC.md\`: add missing behaviors in the right section, correct stale/drifted claims, keep edits minimal and match SPEC.md's existing voice, structure, and table style. Re-read the code to confirm each item before applying; drop any that no longer holds.\n` +
+ `HARD RULES: touch ONLY SPEC.md — never code, tests, package.json, other docs, anything under .claude/, or process/.\n\nFindings:\n${items}`,
+ { label: 'apply:SPEC.md', phase: 'Apply', schema: { type: 'object', additionalProperties: false, required: ['edited','summary'], properties: { edited: { type: 'boolean' }, summary: { type: 'string' }, sectionsTouched: { type: 'array', items: { type: 'string' } } } } }
+ )
+}
+
+return {
+ mode: MODE,
+ reconciliation,
+ contracts,
+ findings: confirmed, // [{specSection, category, claim, reality, refinedFix, ...}] -> caller writes SPEC.audit.md
+ inventories: covered.map(c => c.inventory), // full per-target inventories (coverage evidence)
+ applied, // null in report mode
+}
diff --git a/.claude/workflows/specs/CLAUDE.md b/.claude/workflows/specs/CLAUDE.md
new file mode 100644
index 0000000..0fc6bdb
--- /dev/null
+++ b/.claude/workflows/specs/CLAUDE.md
@@ -0,0 +1,63 @@
+# `.claude/workflows/` — guide
+
+Workflow scripts for this project (the `*.js` live in the parent [`../`](../)), **tracked** via the `!.claude/workflows/` exception in root `.gitignore` — **each paired with a Markdown spec here in `specs/`.**
+
+## The pairing convention (required)
+
+Every workflow `../X.js` (in the parent directory) MUST have a paired spec `X.md` here in `specs/`.
+- **The `.md` is the source of truth; the `.js` is generated from it.** If they drift, the doc wins.
+- Edit the spec first, then regenerate the script from it. The `.js` encodes no policy the `.md` omits;
+ the `.md` describes no behaviour the `.js` lacks.
+- Each `.md` ends with a **Maintenance** note restating this.
+
+## Spec format (follow the existing specs: `release-align.md`, `doc-sync-full.md`)
+
+- **Source-of-truth banner** — blockquote at the top.
+- **Purpose** — what it does, when to run, the report/draft default.
+- **What it audits / ground truth** — the single source of truth + the categories/invariants checked.
+- **Coverage** — exhaustive vs scoped; what's read.
+- **Domain sections** — whatever's special (cross-dir contracts, two-state changelog, packaging policy…).
+- **Output & mode** — `report` (default) vs `fix`; what each writes.
+- **Workflow phases** — the implementation shape, mapping ~1:1 to the `.js`.
+- **Decisions / toggles** — the knobs + current defaults; flipped by editing the doc.
+- **Maintenance** — "edit doc → regenerate js; doc wins on drift."
+
+## Conventions baked into every workflow here
+
+- **Report/draft is the default.** Destructive `fix` mode is opt-in (`args {mode:"fix"}`). Lesson learned:
+ an `args` mishap once defaulted to `fix` and edited files — so parse `args` defensively (object **or**
+ JSON string) and default to the SAFE mode.
+- **Read-only phases use `agentType: 'Explore'`** so they physically can't Edit/Write. Only the apply phase
+ (fix mode) uses a writer agent.
+- **Reuse, don't reimplement.** If logic already lives in an agent (e.g. `changelog-drafter`'s two-state
+ logic), the workflow **dispatches that agent** rather than copying it.
+- **Inventory once, fan out against it.** Derive one ground-truth, then check each target against it —
+ cross-consistency falls out; no pairwise checks.
+- `.claude/{agents,skills,workflows}/` are **tracked** via `!` exceptions in root `.gitignore`; the rest of `.claude/` (incl. `agents/state/`) stays gitignored / local-only.
+
+## The cheap/heavy pairing principle
+
+Workflows here are the **heavy, rare** half of a pair; each has a **cheap, frequent** agent counterpart
+that's the daily driver — and the workflow often *uses* the agent.
+
+| Concern | Cheap daily driver (agent) | Heavy rare gate (workflow) | Workflow spec |
+|---|---|---|---|
+| Per-dir docs vs `src/` | `doc-sync-auditor` | `../doc-sync-full.js` | `doc-sync-full.md` |
+| Root product files vs `src/` | `changelog-drafter` | `../release-align.js` | `release-align.md` |
+
+Run the agent around commits; run the workflow before a release / periodically. They share state where
+relevant (e.g. the doc-sync watermark at `.claude/agents/state/doc-sync-watermark.txt`). Deleting a cheap
+agent removes the daily path **and** breaks the workflow that reuses it.
+
+**Standalone exception — `rot-sweep`.** A third workflow (`../rot-sweep.js`, spec `rot-sweep.md`) sits
+*outside* this cheap/heavy pairing: a diff-scoped, **report-only** `/_rot` sweep of the **uncommitted
+working tree** across the whole repo surface (run before committing a pile). It is **deliberately
+self-contained** — it dispatches no agent and no workflow (owner directive, for hermeticity), so it has
+**no cheap counterpart and no table row**. That makes it a documented exception to "Reuse, don't
+reimplement" above — **do not refactor it to reuse the others**; see `rot-sweep.md`.
+
+## Adding a new workflow
+
+1. Write `X.md` (the spec) first here in `specs/`, in the format above.
+2. Generate `../X.js` from it in the parent directory.
+3. If it has an agent counterpart, add a row to the table above.
diff --git a/.claude/workflows/specs/doc-sync-full.md b/.claude/workflows/specs/doc-sync-full.md
new file mode 100644
index 0000000..24ef383
--- /dev/null
+++ b/.claude/workflows/specs/doc-sync-full.md
@@ -0,0 +1,101 @@
+# Doc-Sync Full Audit Workflow — Spec
+
+> **This Markdown is the source of truth.** The implementation lives in `doc-sync-full.js`. To change
+> behaviour, edit *this file first*, then regenerate the script from it. Both are under `.claude/` →
+> local-only (gitignored).
+
+## Purpose
+
+The **exhaustive** counterpart to the incremental `doc-sync-auditor` agent. It audits **every** directory
+under `src/` to keep each dir's `CLAUDE.md` + `README.md` truthful against the code — including the
+cross-directory sync contracts a per-change pass can't see. **Report/draft by default** — it proposes doc
+edits; you review. `args {mode:"fix"}` applies them.
+
+When to run: **before publish**, or as a periodic deep re-audit. It assumes a **large accumulated diff** —
+files and whole directories **added, removed, updated, and relocated** since the last sweep — so it
+reconciles the **entire interconnected doc web** (per-dir pairs *and* the registration tables + cross-doc
+links that wire them together), not just per-file content. Day-to-day per-commit upkeep is the
+`doc-sync-auditor` agent's job — see Handoff below.
+
+**Relocations are first-class.** A moved file is one event with three rot signatures: an **Orphan** in the
+old dir's docs, a **Gap** in the new dir's docs, and a **broken interconnection** (a table row or link
+still pointing at the old path). The audit reconciles all three sides.
+
+## What it audits (the `/_rot` taxonomy)
+
+Each directory's docs vs its code, in the repo's `/_rot` categories:
+- **Stale** — a claim no longer true.
+- **Drift** — doc and code diverged (especially byte-exact contracts).
+- **Gap** — code has something the docs don't cover.
+- **Orphan** — docs reference a renamed/deleted file/symbol/path.
+- **Broken invariant** — a documented invariant/sync-contract is violated.
+
+Lens per file: `CLAUDE.md` → conventions / sync-rules / gotchas / dependency-direction; `README.md` →
+file inventory / public exports / "where to add code".
+
+## Coverage — exhaustive
+
+- Reads **every non-test `.ts` under `src/`** — tracked **and** untracked-but-not-ignored
+ (`git ls-files` + `git ls-files --others --exclude-standard`); skips `src/test/**` and `*.test.ts`.
+- **Doc-dirs** = every dir (incl. `src/` root) containing BOTH `CLAUDE.md` and `README.md`.
+- **File → doc-dir mapping:** nearest ancestor dir holding both files; root files (`extension.ts`,
+ `gating.ts`) → `src/`; files with no local pair roll up to the nearest one.
+
+## Cross-directory contracts (what per-file detection can't see)
+
+A dedicated pass checks the four multi-site contracts, since they span directories:
+- **Four-site extension sync** — `types/file-extension.ts` → `constants/extensions.ts` → `snippets/dispatch.ts` → `snippets/variants.ts`.
+- **Three-site config sync** — `package.json` (outside `src/`) → `snippets/_styles.ts` → per-language switches.
+- **Two-site button-label sync** — `editor/notification.ts` ↔ `commands/copy-file-path.ts` and `commands/reset-import-styles.ts`.
+- **Runtime-type mirror** — `constants/extensions.ts` ↔ `types/file-extension.ts`.
+
+## Interconnection (the doc web must stay coherent)
+
+A dedicated pass checks the cross-references that adds / removes / relocations break, across the three
+registration sites the docs use (the `scaffold-doc-pair` targets):
+- **Registration tables** — root `CLAUDE.md` (the "Subdirectory guides" table), `src/README.md`,
+ `src/CLAUDE.md`. A row pointing at a renamed/removed dir = **Orphan** (high-confidence); a live dir
+ absent from a table = **Gap** (*advisory* — the tables are curated; e.g. `src/test/fixtures/` is
+ intentionally unlisted).
+- **Inter-doc links** — relative `[text](path)` and `[[name]]` references resolve to a real file
+ (broken target = Orphan).
+- **Scope:** root `CLAUDE.md` is *outside* `src/`, so fix-mode **never edits it** — its findings are
+ **report-only** for you to hand-fix. `src/README.md` + `src/CLAUDE.md` are in-scope (the `src/` writer).
+
+## Output & mode
+
+- **Report/draft (default):** proposes edits + a completeness-critic report; **writes nothing**, watermark
+ untouched. Report-mode apply agents run as read-only `Explore` (physically can't write).
+- **Fix (`args {mode:"fix"}`):** applies doc-only edits — **one writer per dir** (disjoint files), never
+ code / tests / `package.json` / root `README` / `.claude/` / `process/` — then advances the watermark.
+- **Out-of-`src/` findings (e.g. root `CLAUDE.md`):** surfaced in the report, never auto-edited even in
+ fix mode — no writer owns them.
+
+## Handoff (pairs with the agent)
+
+Shares one watermark: `.claude/agents/state/doc-sync-watermark.txt`. A fix run advances it to `HEAD`, so
+the incremental `doc-sync-auditor` agent maintains docs commit-to-commit from there. **Workflow = rare deep
+sweep that seeds; agent = daily driver that maintains.**
+
+## Workflow phases (implementation shape)
+
+1. **Enumerate** (1 agent) — every non-test `.ts` + the doc-dirs.
+2. **Detect** (fan out, read-only) — one agent per file + one per doc-dir (`Detect`) + one per cross-dir contract + one **interconnection** pass over the registration tables & cross-doc links (`Contracts`).
+3. **Verify** — 3-lens adversarial check (majority vote) on each deduped finding; prunes false positives.
+4. **Apply** — per dir, one writer (propose in report mode, edit in fix mode).
+5. **Complete** — completeness critic ("what was missed?") + advance the watermark (fix mode only).
+
+## Decisions / toggles (current defaults — flip by editing this doc)
+
+- **Mode:** `report` (default) | `fix`. *Currently: report-only.*
+- **`src/` coverage:** exhaustive, incl. untracked-but-not-ignored. *Currently: exhaustive.*
+- **Report-mode safety:** apply agents forced to read-only `Explore`. *Currently: on.*
+- **Verification:** 3-lens adversarial, majority vote. *Currently: 3 lenses.*
+- **Interconnection pass:** registration tables (3 sites) + cross-doc links vs the live doc-dir set.
+ *Currently: on.*
+- **Root `CLAUDE.md` (out of `src/`):** report-only | writer-allowlisted. *Currently: report-only.*
+
+## Maintenance
+
+Edit this doc, then regenerate `doc-sync-full.js` from it. The `.js` should encode no policy the doc
+doesn't state; the doc should describe no behaviour the `.js` doesn't implement. If they drift, this doc wins.
diff --git a/.claude/workflows/specs/release-align.md b/.claude/workflows/specs/release-align.md
new file mode 100644
index 0000000..935f0e3
--- /dev/null
+++ b/.claude/workflows/specs/release-align.md
@@ -0,0 +1,135 @@
+# Release Alignment Workflow — Spec
+
+> **This Markdown is the source of truth.** The implementation lives in `release-align.js`
+> (generated from this doc). To change the workflow's behaviour, edit *this file first*, then
+> regenerate the script from it. Both files are under `.claude/` → local-only (gitignored).
+
+## Purpose
+
+A pre-release consistency gate. Before publishing, prove that every top-level file matches what the
+code in `src/` actually does — with the **spec** and the **front page** held to the highest bar —
+and that the packaged VSIX ships exactly the right files. **Report/draft only by default** — it
+proposes fixes; you review and apply.
+
+When to run: before a release, or any time you want to check drift. Day-to-day per-commit doc upkeep
+is the separate incremental `doc-sync-auditor` agent's job; this is the whole-product release sweep.
+
+## Ground truth
+
+The single source of truth is **`package.json` (its `contributes`) + `src/`**. The workflow derives a
+**complete inventory** from these once, and every other file is checked against that one inventory — so
+cross-file consistency falls out automatically (if all files match the inventory, they match each other).
+
+**Coverage is exhaustive: every non-test `.ts` file under `src/` is read** (not just the surface-defining
+ones). The point is a full *behaviour* inventory, not only the declared surface — so the workflow catches
+**gaps** (a user-facing behaviour that exists in code but is documented nowhere), not just stale/wrong
+claims. Files with no user-facing effect are noted as internal and not forced into the docs.
+
+Baseline for "what's new this release" = the latest release **git tag** (`git describe --tags
+--abbrev=0`, e.g. `v0.6.1`), never `package.json`'s `version` (it can lag — currently `0.6.1` while
+the changelog targets `1.0.0`).
+
+### Canonical invariants to derive + assert everywhere
+- **8 commands** — `extension.copyFilePath`, `pasteImport`, `copyPaste`, `pasteImportWithStyle`, `setDefaultImportStyle`, `setImportPlacement`, `togglePreserveScriptExtension`, `resetImportStyles`
+- **20 settings** — the `auto-import.*` configuration keys (+ their enum values), incl. the `auto-import.importStatement.latex.*` namespace
+- **13 destination languages** — `onLanguage:*` events ↔ drop selectors ↔ dispatch (11 by language ID; `.mdx` and `.tex` by file pattern + `workspaceContains`)
+- **3 keybindings** — `cmd/ctrl+shift+a`, `cmd/ctrl+i`, `alt+d`
+- **1 drop provider**
+- **engine** `^1.97.0`
+- **"38 extensions / 18 categories"** claims
+- **version reconcile** — `package.json` vs the `CHANGELOG` Unreleased heading must agree at publish
+
+## Scope — files & emphasis tiers
+
+### ⭐ Tier 1 — highest emphasis (full claim-by-claim sweep + adversarial verification of each finding)
+| File | Why |
+|---|---|
+| `SPEC.md` | The authoritative spec of the entire extension — the contract. |
+| `README.md` | Front page **everywhere** (Marketplace, Open VSX, GitHub); ships in the VSIX. |
+
+### Standard tier (count + key-claim check)
+| File | What "aligned" means |
+|---|---|
+| `package.json` | The source of truth — *and* self-checked: every declared command/setting/activation is actually registered/read in `src/` (nothing declared-but-dead, nothing in-code-but-undeclared). |
+| `SUPPORT.md` | Supported pairs, FAQ, "how to add" match reality. (GitHub-only; excluded from VSIX.) |
+| `CLAUDE.md` (root) | Architecture, command list, counts, sync rules accurate. Overlaps the `doc-sync-full` tool — acceptable. |
+| `CHANGELOG.md` | Two-state handling — see below. |
+
+### Out of scope
+- `.gitignore` — explicitly excluded.
+
+## CHANGELOG — must work in BOTH states (auto-detect)
+
+- **State 1 — no Unreleased section yet:** generate one from scratch (triage commits since the last
+ tag → `feat`/`fix`/`perf` + breaking → read diffs → group Breaking/Added/Changed/Fixed).
+- **State 2 — an Unreleased section already exists** (e.g. today's `[1.0.0] - Unreleased`): verify its
+ entries against `src/` + commits, **merge in anything missing idempotently** (never duplicate), flag
+ stale/inaccurate bullets.
+- Either state: never touch published version sections; version number + date are set at publish.
+- **Reuse, don't reimplement:** this is exactly the logic in the **`changelog-drafter` agent**. The
+ workflow's changelog step **dispatches that agent** so the two-state logic lives in one place and
+ can't drift. In report mode, dispatch it with a *propose-only* instruction (output the draft/merge,
+ don't write); in fix mode, let it write the Unreleased section.
+
+## `.vscodeignore` — packaging-correctness audit (a different kind of check)
+
+Not "match `src/`" but "does the VSIX ship exactly what it should?"
+- **Must ship:** `dist/` (the esbuild bundle), `README.md`, `CHANGELOG.md`, `LICENSE.md`,
+ `package.json`, and README-referenced assets.
+- **Must NOT ship:** `src/`, `out/`, tests, `qa*/`, `process/`, `.claude/`, build configs
+ (`tsconfig.json`, `esbuild.js`, `eslint.config.mjs`, `.vscode-test.*`), `*.ts`/`*.map`, and the
+ GitHub-only docs (`SPEC.md`, `SUPPORT.md`, `CLAUDE.md`).
+- Flag anything currently included that shouldn't ship, or excluded that should (the user's stated
+ worry: "I might add something that shouldn't be there").
+- **Verify against reality, not the file text:** run `npx @vscode/vsce ls` (read-only) to list what
+ would actually be packaged, then diff against the policy above. (Never touch `process/`, where the
+ real publish flow lives.)
+
+## Double-check the rest of root (light finalization sweep)
+
+`LICENSE.md`, `tsconfig.json`, `esbuild.js`, `eslint.config.mjs`, `.vscode-test.mjs`,
+`package-lock.json` — scan for anything stale, wrong, or misplaced before shipping (e.g. correct
+license/year, sane build config, lockfile in sync with `package.json`).
+
+## Output & mode
+
+- **Report/draft only (default).** Produces one consolidated report grouped by file: each finding with
+ severity, the doc claim, the reality in code, and a proposed fix; plus the `.vscodeignore` verdict,
+ the version-reconcile callout, and the changelog draft/merge proposal. **Writes nothing.**
+- `package.json` is **never** auto-edited under any mode.
+- Fix mode (opt-in) may apply doc edits to the doc files only; the changelog step writes its Unreleased
+ section; still never `package.json`, never published changelog versions, never code.
+
+## Proposed workflow phases (implementation shape for `release-align.js`)
+
+1. **Derive ground truth — exhaustively** (fan out, 1 agent per `src/` file):
+ - **1a. Enumerate** every non-test `.ts` under `src/` (`git ls-files`; skip `src/test/**` and `*.test.ts`).
+ - **1b. Extract per file** — one agent per file reports the user-facing surface/behaviour it contributes
+ (commands registered, settings read, languages/pairs gated, notable behaviours + edge cases), and
+ flags internal-only files as "no user-facing surface."
+ - **1c. Synthesize** those + `package.json.contributes` into ONE complete inventory (invariant counts +
+ a behaviour catalogue) and emit `package.json` self-check findings (declared-but-dead /
+ in-code-but-undeclared). This complete inventory is what every doc is checked against — so
+ gap-detection (behaviour present in code, absent from a doc) is airtight.
+2. **Per-file alignment** (fan out, 1 agent per doc) — each checks its file against the canonical
+ inventory. **Tier-1 (`SPEC`, `README`)** → claim-by-claim sweep. **Standard** → counts + key claims.
+ Cross-file consistency is automatic (shared inventory).
+3. **Changelog** — dispatch the `changelog-drafter` agent (propose-only in report mode).
+4. **`.vscodeignore` audit** (1 agent) — run `vsce ls`, diff against the ship/don't-ship policy.
+5. **Root sweep** (1 agent) — the remaining root files.
+6. **Verify + synthesize** — adversarially verify Tier-1 findings; assemble the single consolidated
+ report. (No writes in report mode.)
+
+## Decisions / toggles (current defaults — flip by editing this doc)
+
+- **Mode:** `report` (default) | `fix`. *Currently: report-only.*
+- **`vsce ls` for the packaging audit:** allowed (read-only). *Currently: yes.*
+- **`src/` coverage:** **exhaustive** — every non-test `.ts` file is read (full behaviour inventory, catches gaps). *Currently: exhaustive. Switch to "surface files only" here for a cheaper claim-driven run.*
+- **Tier-1 files:** `SPEC.md`, `README.md`. *(Add/remove here to change emphasis.)*
+- **Changelog engine:** reuse `changelog-drafter` agent (do not reimplement).
+
+## Maintenance
+
+Edit this doc, then regenerate `release-align.js` from it. The `.js` should contain no policy the doc
+doesn't state; the doc should describe no behaviour the `.js` doesn't implement. If they drift, this
+doc wins.
diff --git a/.claude/workflows/specs/rot-sweep.md b/.claude/workflows/specs/rot-sweep.md
new file mode 100644
index 0000000..2585b32
--- /dev/null
+++ b/.claude/workflows/specs/rot-sweep.md
@@ -0,0 +1,220 @@
+# Rot-Sweep Workflow — Spec
+
+> **This Markdown is the source of truth.** The implementation lives in `rot-sweep.js`
+> (generated from this doc). To change the workflow's behaviour, edit *this file first*, then
+> regenerate the script from it. Both files are under `.claude/` → local-only (gitignored).
+
+## Purpose
+
+A **pre-commit drift sweep** for an accumulated pile of uncommitted changes. When you've been editing for
+a while without checking as you go, `rot-sweep` reads **only what you changed**, follows each change out to
+the docs and contracts it touches, and **reports** — grouped by the five `/_rot` categories — where the
+pile left things inconsistent. **Report-only, always:** it never edits, commits, or ticks anything; you
+read the report and decide what to fix.
+
+When to run: when a pile of uncommitted changes has built up and you want a consistency check **before
+committing**. Re-run it after each fix — being diff-scoped, it only re-examines what's still uncommitted.
+This is the change-scoped, whole-repo-surface sibling of the heavier full-tree gates (`doc-sync-full`,
+`release-align`) and of the typed `/_rot` (whole-tree); none of those is diff-scoped across the whole repo.
+
+It will **not** find runtime bugs — it checks *consistency* (docs/contracts vs code), not *behaviour*. It
+never runs the extension or the tests.
+
+## Ground truth
+
+The single source of truth is **the code** — the docs adjust to it. When the sweep finds a doc and the
+code disagreeing, the code is treated as correct and the **doc** is the thing reported as wrong.
+
+> **Caveat — code assumed correct.** This workflow conforms docs to code; it never runs or validates the
+> code. It operates on the assumption that the uncommitted code is correct. If that assumption is false on
+> some future run, the sweep will faithfully report a doc as "wrong" for not matching the (actually wrong)
+> code. Run it on piles you've already verified.
+
+**Scope is strict: only the uncommitted working tree.** The audit *subject* is exactly the set of files in
+`git status` — staged + unstaged + untracked, in their **on-disk** form (a file with both staged and
+unstaged edits is read as it currently is on disk). To *verify* a finding the sweep MAY open an unchanged,
+already-committed partner file to compare against — but such a file is only a witness, never an audit
+subject, and is never reported on for its own sake.
+
+## Coverage — what's swept, what isn't
+
+**The PRODUCT is the scope boundary — explicitly, not the gitignore.** Scope = the changed files that fall
+within an explicit product allowlist: `src/`, `docs/`, `qa/`, the root product docs, and `package.json`.
+Everything else — `.claude/` tooling, `process/`, `.vscode/`, root build configs — is out **by path, even
+if tracked**. (This was originally "whatever isn't gitignored"; that was a mistake — un-ignoring `.claude/`
+then silently pulled the tooling into scope. Scope must not depend on a mutable ignore rule.)
+
+**Two triggers put something in scope — but the *trigger* is always in your pile:**
+1. **Direct** — a file is in the uncommitted set → it is checked against the code.
+2. **Implicated** — a *code* change in the pile pulls in the docs and contracts that describe it (read as
+ witnesses), **even if those docs are unchanged**. This is what catches "you changed the code, but the
+ doc that documents it — which you never touched — is now stale." Doc-side findings on an unchanged
+ (committed) file are reported, never written.
+
+### ⭐ Priority targets (Tier-1) — the most important docs
+
+`SPEC.md`, `README.md`, everything under `docs/`, the relevant `qa/` checklist(s), and `CHANGELOG.md` are
+**Tier-1**: they get a **claim-by-claim** check (not a count/skim), and they are **standing implicated
+partners** — any behavioural code change in the pile is checked against them *even when they aren't
+themselves edited*. (The extension is entirely about generating import statements, so most behavioural
+changes implicate `SPEC`, `README`, and `docs/import-statements/`.)
+
+**In scope, by surface:**
+
+| Surface | Trigger | What's checked |
+|---|---|---|
+| ⭐ `SPEC.md`, `README.md` | direct **or** any behavioural code change | claim-by-claim vs the code (the authoritative contract + the front page) |
+| ⭐ `docs/**` (criteria, decisions, ledgers) | direct **or** a code change touching the documented design | the relevant criteria/decision claim-by-claim vs current behaviour |
+| ⭐ relevant `qa/**` checklist(s) | direct **or** a behavioural code change that has a checklist | the checklist + its qa-doc cascade (inventories, counts, 1:1 fixture pairing) — **only the relevant language(s), never the whole tree** |
+| ⭐ `CHANGELOG.md` (Unreleased) | any user-facing change in the pile | does Unreleased accurately cover the pile's Added/Changed/Fixed? (see CHANGELOG section) |
+| `CLAUDE.md`, `SUPPORT.md` (root) | direct or relevant code change | counts / claims / sync-rules vs code |
+| `package.json` | direct | `contributes` vs `src/` registration; three-site config contract |
+| `src/**/*.ts` (non-test) | direct | the file vs its owning-dir `CLAUDE.md` / `README.md` + any contract it belongs to |
+| `src/**/{CLAUDE,README}.md` | direct | the dir doc vs its code |
+| untracked files (e.g. `README.draft.md`) | direct | Orphan — is anything referencing it? |
+
+**On the qa/ volume worry:** diff-scoping is what keeps qa/ manageable. The sweep never reads the whole
+12-language qa/ tree — only the checklist(s) your change actually touches, plus the bounded cascade those
+edits propagate to. If your pile touches no qa-relevant behaviour, qa/ is skipped entirely.
+
+**Out of scope (explicit exclusions, by path):**
+- **Non-product paths** — `.claude/` (the audit tooling itself, **even now that it's tracked**), `process/`,
+ `.vscode/`, root build configs (`tsconfig.json`, `esbuild.js`, `eslint.config.mjs`, …), `dist/`, `out/`,
+ `node_modules/`. Excluded **by path, not by gitignore state**. *(So `.claude/`-internal drift is NOT covered
+ here — that's the whole-tree typed `/_rot`'s job; and auditing the audit-tooling with itself is
+ deliberately avoided.)*
+- **`src/test/**`** — no doc pairs; skipped as finder targets.
+- **Packaging / `.vscodeignore` / `vsce ls`** — owned by `release-align`; the sweep emits a one-line note
+ deferring to it and never runs `vsce`.
+
+## The five `/_rot` categories
+
+Every finding is exactly one of (definitions match the typed `/_rot` taxonomy and `spec-sync.js`):
+
+- **Stale** — a doc statement that is no longer true.
+- **Drift** — a doc and the code have diverged (especially the byte-exact cross-site contracts below).
+- **Gap** — the code has something the docs don't cover (a new command, setting, behaviour).
+- **Orphan** — a doc / table / link references a file / symbol / path that was renamed or deleted (or a new
+ file that nothing references).
+- **Broken invariant** — a documented invariant, count, or sync-contract is now violated (e.g. the "seven
+ commands" count, the dependency-direction rule, the "only barrel is `commands/index.ts`" rule).
+
+## Cross-site contracts (checked only when implicated)
+
+The four documented multi-site contracts (root `CLAUDE.md` → "Cross-cutting sync rules"). A contract finder
+runs **only if the uncommitted set intersects that contract's member files** — diff-triggered, but it then
+reads *all* member sites (committed ones as witnesses) so a half-finished edit is caught:
+
+| Contract | Member files (intersection triggers the finder) |
+|---|---|
+| Four-site extension sync | `src/types/file-extension.ts`, `src/constants/extensions.ts`, `src/snippets/dispatch.ts`, `src/snippets/variants.ts` |
+| Three-site config sync | `package.json`, `src/snippets/_styles.ts`, `src/snippets/languages/*.ts` |
+| Two-site button-label sync | `src/editor/notification.ts`, `src/commands/copy-file-path.ts`, `src/commands/reset-import-styles.ts` |
+| Runtime-type mirror | `src/constants/extensions.ts`, `src/types/file-extension.ts` |
+
+## CHANGELOG — accurate Unreleased check
+
+The changelog is a Tier-1 target with its own finder. It does NOT just diff prose — it triages the
+**user-facing** changes in your pile (new/changed commands, settings, behaviours; fixes) and checks them
+against the `## [Unreleased]` section:
+- **Gap** — a user-facing change in the pile that Unreleased doesn't mention → propose the exact
+ Added / Changed / Fixed bullet.
+- **Stale** — an Unreleased bullet that misstates what the code now does → propose the correction.
+- Never touches published version sections; the version number + date are a publish-time concern.
+
+Report-only: it *proposes* the accurate entries; you (or the assistant, afterward) apply them. rot-sweep
+cannot reuse `release-align`'s `changelog-drafter` agent (it is self-contained), so this is a lightweight,
+pile-scoped reimplementation — not the full two-state release drafter.
+
+## Diff → bucket routing
+
+The enumerate phase buckets each uncommitted path (in plain JS, no agent) so the right finder is dispatched:
+- a root product doc → root-doc finder
+- `package.json` → package finder (and marks the three-site contract implicated)
+- under `docs/` → docs finder; under `qa/` → qa finder
+- a non-test `.ts` under `src/` → per-file finder against its **owning doc-dir** (nearest ancestor holding
+ both `CLAUDE.md` + `README.md`; `src/snippets/languages/*` maps up to `src/snippets/`; files directly in
+ `src/` map to `src/`)
+- a `CLAUDE.md` / `README.md` under `src/` → dir-doc finder
+- an added / deleted / renamed path → also feeds the interconnection finder (registration tables +
+ cross-doc links) and the Orphan check
+- a member of any contract above → marks that contract implicated
+- a **behavioural** `src/` code change → also implicates the **Tier-1 partner docs** (`SPEC.md`,
+ `README.md`, the relevant `docs/` criteria, the relevant `qa/` checklist) and the **CHANGELOG Unreleased**
+ finder — read as witnesses even when those files are unchanged
+
+## Deliberately self-contained (do not "fix" this)
+
+`rot-sweep` **reimplements** the finder → verify → critic shape inline. It does **not** dispatch or call any
+local agent or workflow (`doc-sync-auditor`, `doc-sync-full`, `release-align`, `qa-doc-sync`, the typed
+`/_rot`). This is a **deliberate exception** to this repo's "reuse, don't reimplement" convention
+(`specs/CLAUDE.md`), made by owner directive for hermeticity — the sweep must behave identically regardless
+of how those other tools evolve. **Do not refactor it to reuse them.** (It does use the built-in `Explore`
+agent type — a harness primitive, not a local agent.) Consequence to accept: the four contract definitions
+above are restated here and must be updated by hand if a contract changes.
+
+## Output & mode
+
+- **Report-only — the only mode.** There is no `fix` path; the workflow has no writer agent and edits
+ nothing. (So the "args mishap silently edits files" failure that motivated the house report-default
+ cannot occur here at all.)
+- **It writes no file.** Per the house convention the workflow *returns* the structured report; the caller
+ persists it. Findings are grouped **by `/_rot` category** and **by surface / file**, each with severity,
+ `file:line` for both the doc claim and the code reality, a described `proposedFix` (never applied), and
+ triage tags (`mechanical` | `behavioral`, `uncommitted` | `committed`).
+- After the run the assistant relays a chat summary; only if you ask for a persisted file does it write one
+ to the **gitignored** `.claude/rot-sweep.audit.md` (so the report never pollutes your pile).
+- **Coverage manifest (honesty, not hedging).** The report always carries a factual manifest from the
+ completeness critic — which files were *read* (diff subjects + pulled-in witnesses), which were *checked*,
+ which in-scope surfaces were *skipped* and why — plus **one** line: "diff-scoped; for a completeness
+ guarantee run the full-tree `/_rot` before publish." No speculation about unread files, no generic
+ disclaimer — just this run's actual coverage, so a clean report is never mistaken for an exhaustive one.
+- A one-line `note` records that packaging / `.vscodeignore` is out of scope (see `release-align`).
+
+## Workflow phases (implementation shape for `rot-sweep.js`)
+
+All fan-out runs through `runBatched(thunks, 6)` (throttle-avoidance — a whole-repo surface is many
+agents). Every agent is `agentType:'Explore'` (read-only; there is no writer phase) and `model:'opus'`.
+
+1. **Enumerate** (1 agent) — `git status --porcelain` (+ untracked) → the uncommitted path set; bucket each
+ path (root-doc / package.json / docs / qa / src-code / src-doc / orphan-candidate) and mark implicated
+ contracts. Read-only git.
+2. **Detect** (fan out, `runBatched(6)`) — one finder per bucketed item: per changed `src/` file (vs its
+ owning doc-dir); per changed root-doc / `docs/` / `qa/` surface; per **implicated** contract (reads all
+ its member sites); one **Tier-1 partner** finder each for `SPEC.md` / `README.md` / the relevant `docs/`
+ criteria / the relevant `qa/` checklist — **claim-by-claim** — whenever a behavioural code change
+ implicates them even if unchanged; one **CHANGELOG** finder (triage the pile's user-facing changes vs the
+ Unreleased section); plus one interconnection finder when the set adds / deletes / renames a path. Each
+ finding: `category, severity, surface, location, claim, reality, proposedFix, tags`. Dedupe in JS.
+3. **Verify** (fan out, `runBatched(6)`) — one **single-lens, refute-by-default** agent per deduped finding
+ (re-read the actual doc location AND the actual code; default `real=false` unless the evidence clearly
+ holds). Keep only `real===true`.
+4. **Complete** (1 agent) — completeness critic: which uncommitted path or implicated contract wasn't
+ represented? Emits the **coverage manifest** (read / checked / in-scope-skipped) the report carries —
+ facts about this run's coverage, **not** speculation about unread files (recall safety net).
+5. **Assemble** — `return { mode:'report', findings (byCategory + bySurface), coverage, note }`. No writes.
+
+## Decisions / toggles (current defaults — flip by editing this doc)
+
+- **Mode:** `report` only. *There is no `fix` mode, by design.*
+- **Scope:** strict uncommitted working tree (`git status`) ∩ an explicit product allowlist (`src/`,
+ `docs/`, `qa/`, root product docs, `package.json`). *Currently: product-allowlist — NOT gitignore-derived,
+ so an ignore-rule edit can't move scope; `.claude/` is excluded by path.*
+- **Verification depth:** single-lens, refute-by-default, per finding. *Currently: single-lens — the report
+ is human-reviewed, so budget goes to finder recall + the critic, not 3-lens voting. Raise here for a
+ higher-precision report.*
+- **Fan-out batching:** `runBatched(size=6)`. *Currently: 6 (throttle-safe).*
+- **Models:** every agent `model:'opus'` + `agentType:'Explore'`. *Currently: all-opus.*
+- **Self-contained:** no reuse of local agents / workflows. *Currently: standalone (deliberate exception).*
+- **Recall posture:** report carries a factual coverage manifest + a one-line pointer to the full-tree
+ `/_rot`; no speculation about unread files, no rules derived from audited docs. *Currently: manifest-only
+ — completeness is the full-tree pass's job, not this tool's.*
+- **Tier-1 priority targets:** `SPEC.md`, `README.md`, `docs/**`, relevant `qa/**`, `CHANGELOG.md` —
+ claim-by-claim + checked as standing partners against behavioural code changes even when unchanged.
+ *(Add/remove targets here.)*
+- **Packaging audit (`vsce ls` / `.vscodeignore`):** excluded. *Currently: no — owned by `release-align`.*
+
+## Maintenance
+
+Edit this doc, then regenerate `rot-sweep.js` from it. The `.js` should contain no policy the doc doesn't
+state; the doc should describe no behaviour the `.js` doesn't implement. If they drift, this doc wins.
diff --git a/.claude/workflows/specs/spec-sync.md b/.claude/workflows/specs/spec-sync.md
new file mode 100644
index 0000000..26c43ac
--- /dev/null
+++ b/.claude/workflows/specs/spec-sync.md
@@ -0,0 +1,86 @@
+# Spec Sync Workflow — Spec
+
+> **This Markdown is the source of truth.** The implementation lives in `spec-sync.js`
+> (generated from this doc). To change the workflow's behaviour, edit *this file first*, then
+> regenerate the script from it. Both files are under `.claude/` → local-only (gitignored).
+
+## Purpose
+
+The deep, **spec-only** counterpart to `release-align`'s broad release sweep: audit the existing
+`SPEC.md` against the code and report (or, in fix mode, apply) the gaps and corrections —
+keeping everything already right. **It is not a rebuild** — the current `SPEC.md` is preserved and
+improved in place. **Heavy / rare** — run when you doubt the spec's completeness, not per-commit.
+**Report/draft by default**; `args {mode:"fix"}` edits `SPEC.md` only.
+
+When to run: when in doubt about `SPEC.md`, before a release, or after a big refactor. Broad
+per-release file alignment is `release-align`'s job; per-commit doc upkeep is `doc-sync-auditor`'s.
+
+## Ground truth
+
+The **code** under `src/` (non-test), plus **`package.json`** (its `contributes`: commands,
+keybindings, activationEvents, configuration), is the source of truth. The current `SPEC.md` is the
+**audited document**, not ground truth — every SPEC claim is checked against the code, and every code
+behavior is checked for SPEC coverage. Same posture as `doc-sync-full`, with `SPEC.md` as the doc.
+
+## What it audits (the `/_rot` taxonomy, spec-flavoured)
+
+- **Gap** — a behavior / command / keybinding / setting / gating clause that exists in code but
+ `SPEC.md` documents nowhere.
+- **Stale** — a `SPEC.md` claim no longer true.
+- **Drift** — `SPEC.md` and code diverged, especially byte-exact contract values.
+- **Broken invariant** — a documented invariant/contract violated in code (reported, never code-edited).
+
+## Coverage — exhaustive
+
+Denominator = every non-test `.ts` under `src/` (the caller passes `args.sourceFiles` from
+`git ls-files`) + `package.json`. Every target gets one **full-read** agent; the verified inventory is
+the ground truth the spec is then checked against. The four cross-file contracts + gating + layering
+get dedicated agents.
+
+## Output & mode
+
+**Report/draft by default** — returns confirmed findings (Gap/Stale/Drift/Broken-invariant, each with
+SPEC section + location + reality + `proposedFix`), the full inventories, and a reconciliation. The
+caller writes a throwaway `SPEC.audit.md` from the findings; `SPEC.md` is untouched. **`args
+{mode:"fix"}`** runs a **single writer** that edits **only `SPEC.md`**, applying confirmed findings.
+
+## Workflow phases
+
+1. **Extract → Verify** — full-read Opus agent per source file (+ `package.json`); the schema forces
+ complete inventories; an independent verify stage corrects/augments. Runs in **small sequential batches**
+ (`EXTRACT_BATCH`), like Audit/Verify (see Decisions).
+2. **Contracts** — one agent per cross-file contract (four-site, three-site, two-site, runtime-mirror,
+ gating, layering).
+3. **Audit** — per-file + per-contract agents read the live `./SPEC.md` and check it against the
+ verified inventory → findings; deduped. Extract, Audit, and Verify all run in **small sequential
+ batches**, not wide `parallel()`/`pipeline()` fan-outs (see Decisions).
+4. **Verify** — adversarial refute-by-default per finding; only survivors are confirmed.
+5. **Apply** *(fix mode only)* — single writer edits `SPEC.md`.
+
+## Decisions / toggles
+
+- **Full-read default subagent, NOT `Explore` — deliberate deviation from this directory's convention.**
+ `Explore` reads excerpts; exhaustive whole-file reading is the point. Write-safety is preserved by the
+ report-mode default + the single writer existing only in fix mode (and touching only `SPEC.md`).
+- **Tests excluded from ground truth.** The spec describes *shipped* behavior; tests verify it, they
+ don't add it — so extraction and verify read `src/` (non-test) + `package.json` only. (The verify
+ stage could consult a test as a tiebreaker via a one-line toggle, off by default.)
+- **Opus across all agents.** ~36 files + the manifest; cost acceptable, model-tier doubt removed.
+ Levers: `sonnet` (or drop) the per-file verify; 3-lens the finding-verify for extra rigour.
+- **Extract, Audit & Verify all run in small sequential batches (`EXTRACT_BATCH` / `AUDIT_BATCH` / `VERIFY_BATCH`, default 6), not wide `parallel()` / `pipeline()` fan-outs.** By the time Audit runs (~80 prior agents, >1M tokens), firing all ~42 audit
+ + all verify *schema* agents as one barrier throttles the API and agents return prose instead of calling
+ `StructuredOutput` — observed 2026-05-31: 25/43 audit agents (incl. **all 6 contract audits**) mass-failed.
+ Small waves keep the instantaneous rate under the throttle — **PROVEN 2026-05-31**: a batched re-run
+ completed **43/43 audits, `droppedAudits: []`**, 38 raw → 26 confirmed → 24 applied. **Extract was the last un-batched phase (a bare `pipeline()`) until 2026-06-04, when a transient throttle dropped its tail — `pipeline[21..36]` = 14/37 targets (incl. `package.json` + all 9 `languages/` builders); it now uses the same `runBatched` waves.** Slower wall-clock,
+ reliably complete. Reconciliation reports `droppedAudits`/`auditCompleted` (+ dropped targets) so any residual gap is visible.
+ Levers: raise the batch sizes when rate-comfortable, or make the audit agents free-text.
+- **Audit agents read the live `./SPEC.md`, not an embedded `args.specText` snapshot.** Avoids passing a
+ ~25 KB arg and any transcription/staleness skew; safe because the fix-mode writer edits only at the very
+ end, after every audit/verify agent has finished. `args.specText` is now optional and ignored.
+- Rough scale: ~90 opus agents, ~1–1.3M tokens, one-shot.
+
+## Maintenance
+
+Edit this doc → regenerate `spec-sync.js`; on drift, the doc wins. No cheap agent counterpart yet
+(standalone heavy audit); complements `release-align` (broad release sweep). If a daily "is `SPEC.md`
+accurate" driver is added later, record it in `.claude/workflows/CLAUDE.md`'s pairing table.
diff --git a/.codex/config.toml b/.codex/config.toml
new file mode 100644
index 0000000..52883ec
--- /dev/null
+++ b/.codex/config.toml
@@ -0,0 +1,2 @@
+project_doc_fallback_filenames = ["CLAUDE.md"]
+project_doc_max_bytes = 65536
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..db738ed
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,8 @@
+# Enforce LF for all text files — prevents the CRLF spurious-diff problem
+* text=auto eol=lf
+
+# Binary assets
+*.png binary
+*.gif binary
+*.vsix binary
+*.mov binary
diff --git a/.gitignore b/.gitignore
index b0d1c23..25a5a17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,33 @@
+# Build output
out
dist
+coverage
+*.tsbuildinfo
+
+# Dependencies
node_modules
+
+# VS Code extension testing & packaging
.vscode-test/
*.vsix
-/assets/test
-/process/*
+
+# Logs
+*.log
+npm-debug.log*
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Project-private assets & notes
+process
+
+# Claude Code
+.claude/*
+!.claude/agents/
+!.claude/skills/
+!.claude/workflows/
+.claude/agents/state/
+
+# Large local media (screen-recording tests — not shipped)
+assets/*.mov
diff --git a/.vscode-test.mjs b/.vscode-test.mjs
index b62ba25..9d1422d 100644
--- a/.vscode-test.mjs
+++ b/.vscode-test.mjs
@@ -1,5 +1,32 @@
import { defineConfig } from '@vscode/test-cli';
+import os from 'node:os';
+import path from 'node:path';
+// VS Code's default user-data-dir lives inside the project (.vscode-test/user-data). On deep project
+// paths that pushes the Extension Host IPC socket past macOS's ~103-char unix-socket limit
+// (listen EINVAL: ...-main.sock). Relocate it to a short temp path so `npm test` runs anywhere.
+const userDataDir = path.join(os.tmpdir(), 'auto-import-vscode-test');
+
+// Coverage is read ONLY from the `{ tests, coverage }` global form — a single-object
+// config silently drops it (see @vscode/test-cli config loader). Coverage is opt-in via the
+// `--coverage` flag (npm run test:coverage); a plain `npm test` ignores this block.
export default defineConfig({
- files: 'out/test/**/*.test.js',
+ tests: [
+ {
+ files: 'out/test/**/*.test.js',
+ mocha: {
+ ui: 'bdd',
+ },
+ launchArgs: [ '--user-data-dir', userDataDir ],
+ },
+ ],
+ coverage: {
+ // Count every source file, not just those imported at runtime, so files with zero
+ // tests surface as 0% instead of vanishing from the denominator.
+ includeAll: true,
+ // src/types/** is type-only (unions, no runtime statements) — including it reports a
+ // phantom 0% that understates real coverage. The compiler is its test.
+ exclude: [ '**/test/**', '**/*.test.*', '**/types/**' ],
+ reporter: [ 'text', 'html' ],
+ },
});
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index dd01eb3..d7a3ca1 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,5 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
- "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "ms-vscode.extension-test-runner"]
+ "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 16a5c02..6ff5875 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -9,5 +9,5 @@
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
- "typescript.tsc.autoDetect": "off"
+ "typescript.tsc.autoDetect": "off",
}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c2ab68a..3cf99c3 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -4,19 +4,43 @@
"version": "2.0.0",
"tasks": [
{
- "type": "npm",
- "script": "watch",
- "problemMatcher": "$ts-webpack-watch",
- "isBackground": true,
- "presentation": {
- "reveal": "never",
- "group": "watchers"
- },
- "group": {
- "kind": "build",
- "isDefault": true
- }
- },
+ "label": "watch",
+ "dependsOn": [
+ "npm: watch:tsc",
+ "npm: watch:esbuild"
+ ],
+ "presentation": {
+ "reveal": "never"
+ },
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ }
+ },
+ {
+ "type": "npm",
+ "script": "watch:esbuild",
+ "group": "build",
+ "problemMatcher": "$esbuild-watch",
+ "isBackground": true,
+ "label": "npm: watch:esbuild",
+ "presentation": {
+ "group": "watch",
+ "reveal": "never"
+ }
+ },
+ {
+ "type": "npm",
+ "script": "watch:tsc",
+ "group": "build",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "label": "npm: watch:tsc",
+ "presentation": {
+ "group": "watch",
+ "reveal": "never"
+ }
+ },
{
"type": "npm",
"script": "watch-tests",
diff --git a/.vscodeignore b/.vscodeignore
index d255964..b90e8de 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -4,11 +4,24 @@ out/**
node_modules/**
src/**
.gitignore
-.yarnrc
-webpack.config.js
-vsc-extension-quickstart.md
+.gitattributes
+esbuild.js
**/tsconfig.json
**/eslint.config.mjs
**/*.map
**/*.ts
**/.vscode-test.*
+qa/**
+coverage/**
+CLAUDE.md
+SPEC.md
+SUPPORT.md
+ROADMAP.md
+package-lock.json
+process/**
+.claude/**
+.codex/**
+docs/**
+assets/*.gif
+assets/*.mov
+**/.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72631ee..495e72b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,299 +1,671 @@
# Changelog
-## [0.6.1] - 2025-03-28
+## v1.0.0 (2026-06-28)
-### Fixed
-- **Relative Path Bug:** Prepend './' to relative paths for same-directory imports. This critical bug fix ensures that module imports are correctly resolved when files reside in the same directory.
+### Breaking Changes
+- **Minimum VS Code version is `^1.97.0`.** The drag-and-drop import provider needs the drop-edit APIs (`DocumentDropOrPasteEditKind`, 3-argument `DocumentDropEdit`) finalized in VS Code 1.97 (February 2025), so the extension requires VS Code 1.97 or newer and won't load on builds older than 1.97. (This is the empirical floor for those APIs — and a slight *widening* versus the 0.6.x line, which required `^1.98.0`.) Recent Cursor, VSCodium, and Code Server builds that track the VS Code API at 1.97+ remain supported.
+- **TypeScript import styles reshuffled.** The "aliased default" shape (`import { $1 as $2 } from '…'`) has been removed. Three new shapes added: type-only import, mixed value + type import (TS 4.5+), and dynamic `await import()`. Net change: 5 → 7 entries — **style indices have shifted**; users with a non-default `typescriptImportStyle` should re-select their preferred shape in Settings.
+- **JavaScript import styles reshuffled.** Four legacy shapes removed: `import { default as name }`, `var = require()`, `var = import()`, `const = import()`. Two new shapes added: mixed default + named import, and `const = await import()`. Net change: 9 → 7 entries — **style indices have shifted**; users with a non-default `javascriptImportStyle` should re-select.
+- **SCSS default flipped from `@import` to `@use`.** The `@import url(…)` shape has been dropped entirely. Two new shapes added: `@use '…' as name` and `@forward '…'` (`@use '…' as *` was already available). Users who relied on `@import` as the default should update their `scssImportStyle` setting.
+- **HTML script default flipped to modern minimal.** `` → ``. Three new shapes added: `defer`, `type="module"`, and `async`. Users who need the legacy `type` attribute should select it explicitly.
+- **Markdown image default reordered.** Two new shapes added (bare inline, HTML `` embed), the reference-style `![alt-text][image]` shape dropped, and the default position rotated. Users with a non-default `markdownImageImportStyle` should verify their selection.
+- **Markdown link default changed from image syntax to link syntax.** `markdownImportStyle` now defaults to `[text](path)` instead of ``. Image embeds belong under the dedicated `markdownImageImportStyle` setting.
-## [0.6.0] - 2025-03-28
+### Added
+- **Five new destination languages.** Vue (`.vue`), Svelte (`.svelte`), Astro (`.astro`), MDX (`.mdx`), and **LaTeX** (`.tex`) are now supported as import destinations — bringing the total to **13 destination file types**.
+- **LaTeX (`.tex`) — figures, file includes, and bibliographies.** Drag or paste a file into a `.tex` document and get the right LaTeX command, via a dedicated `auto-import.importStatement.latex.*` settings namespace. Three source relationships, each with its own configurable style:
+ - **Graphics** (`.pdf`, `.png`, `.jpg`, `.jpeg`, `.eps`) → a `figure` float by default (`\begin{figure}[htbp] … \includegraphics[width=0.5\textwidth]{…} … \caption{} \label{fig:} … \end{figure}`, with `\caption` before `\label` for correct `\ref` numbering), or a bare / sized `\includegraphics`. The accepted graphics set is **engine-renderable only** — `.svg` / `.gif` / `.webp` / `.avif` are rejected (`pdflatex` can't render them). `preserveGraphicsFileExtension` (default *on* — keep) governs the path, the inverse of the script/stylesheet preserve toggles.
+ - **`.tex`** → `\input{…}` (default) or `\include{…}`; the `.tex` extension is dropped (`\include` requires it omitted).
+ - **`.bib`** → `\addbibresource{…bib}` (modern biblatex, default) or `\bibliography{…}` (legacy BibTeX).
+ - Activates via `onLanguage:latex` + `workspaceContains:**/*.tex`; the drop provider matches `.tex` by file pattern (LaTeX has no guaranteed VS Code language ID), the same way `.mdx` is handled.
+- **Media and document source types.** Video (`.mp4`, `.webm`, `.mov`), audio (`.mp3`, `.ogg`, `.wav`, `.m4a`), text-track (`.vtt`), and additional asset types (`.svg`, `.avif`, `.pdf`) can now be imported into JSX, TSX, MDX, and HTML destinations. Together with the LaTeX `.tex` / `.bib` / `.eps`, this brings the total to **38 supported file extensions across 18 categories**.
+- **Five new commands.**
+ - **Auto Import: Paste as Import (Pick Style)** — opens a QuickPick listing every applicable import shape for the current source/destination pair. Picker rows are labelled by the source file's basename; the full relative path is still what gets inserted. Reachable from the Command Palette and from the "Paste with Style" button on the copy-success toast. Single-shape destinations insert directly without showing the picker.
+ - **Auto Import: Set Default Import Style** — same picker, but persists the chosen shape to User (Global) settings instead of inserting a snippet. The current default is marked with a checkmark icon and appears first in the list.
+ - **Auto Import: Set Import Placement** — a QuickPick of Top / Bottom / Cursor that persists where imports are inserted (`auto-import.preferences.importStatementPlacement`); the current choice is marked with a checkmark. Command Palette only.
+ - **Auto Import: Toggle Preserve Script File Extension** — flips `preserveScriptFileExtension` and shows the new state (On / Off) in a toast. Command Palette only.
+ - **Auto Import: Reset All Import Styles to Defaults** — clears every customized import-style override (the **twelve** configurable styles, incl. the three `latex.*` styles) back to its `package.json` default, with an **Undo** action on the confirmation toast; shows an info toast when nothing is customized. Command Palette only.
+- **Drag-and-drop import from Explorer.** Drag any supported source file from the Explorer sidebar into an open editor — the extension generates the same import snippet as the paste commands and inserts it at the drop position. Registered as a `DocumentDropEditProvider` for all 13 supported destination languages. No keybinding needed; no new settings — uses the same styles and configuration as the paste commands. Unsupported pairs show the same "Cannot import" warning as paste commands and insert nothing — the provider suppresses VS Code's default raw-path drop.
+- **`onLanguage` activation events.** The extension now activates when any supported language file is opened (13 `onLanguage:*` entries in `package.json`, plus `workspaceContains:**/*.mdx` and `**/*.tex` for the two pattern-matched destinations), ensuring the drop provider is registered before the user's first drag.
+- **Untrusted-workspace and virtual-workspace support.** The manifest now declares `capabilities.untrustedWorkspaces` (supported) and `virtualWorkspaces` (true), so the extension loads in Restricted Mode and in virtual workspaces (e.g. github.dev, vscode.dev) — it only reads file paths and inserts snippets, with no workspace-trust requirement.
+- **Smart placement for component files.** Astro imports land inside `---` frontmatter fences. Vue and Svelte imports land inside `` tag at the cursor, driven by the new `importStatements.html.htmlScriptSupport` setting.
+- **Stylesheet link for `.css → .html`.** A `.css` source pasted into an HTML file emits a `` tag, driven by the new `importStatements.html.htmlStylesheetSupport` setting.
+- **Two new HTML settings.** `importStatements.html.htmlScriptSupport` and `importStatements.html.htmlStylesheetSupport` register in `package.json` and are read live via `config-retrival.ts` (`HTMLScriptEnum` / `HTMLStylesheetEnum` in `config-enum.ts`); `extension.ts` watches both through `onDidChangeConfiguration` so style changes apply without reload.
+- **HTML demo asset and docs.** Added `images/html.gif` plus an "HTML Support" section in `DEMO.md` and `README.md`. HTML moved out of the README "TODO" list into a documented feature, and the settings reference gained an `#### HTML` subsection.
### Changed
-- Updated **.gitignore**.
-## [0.1.14] - 2020-03-21
+- **Refreshed the settings-preview screenshot.** README now points at `images/settings.gif` (replacing the removed `images/preview.gif`).
+- **Keyed snippet dispatch on the active file type.** `import-text.ts` now records the destination file's extension (`activeExtname`) and routes `.js` / `.css` sources to the HTML builders when the target is `.html`, while the existing JS/TS/CSS/SCSS/LESS/Markdown/image builders are now guarded to fire only for non-HTML destinations.
+- **Version bumped to `0.2.2`.** `package.json` and `package-lock.json` updated from `0.2.1`.
-### Changed
-- Updated **README.md**.
+### Fixed
+
+- **Markdown image-support description typo.** README setting description corrected from "iamge" to "image".
-## [0.1.13] - 2020-03-20
+## v0.2.1 (2020-03-22)
+
+### Fixed
+
+- **Broken Markdown demo links in the README.** The three `DEMO.md` deep links (`[markdown support]`, `[markdown image import]`, `[markdown import]`) used PascalCase anchors (`#Markdown-support`, `#Import-image-to-markdown`, `#Import-markdown`) that did not match GitHub's lowercased heading slugs, so they resolved to nothing — they now point to the correct lowercase fragments (`#markdown-support`, `#import-image-to-markdown`, `#import-markdown`). README whitespace was also tidied. Docs-only change; the `0.2.1` version bump in `package.json` carries no code changes.
+
+## v0.2.0 (2020-03-22)
+
+### Added
+
+- **Markdown file support.** `.md` is now a supported file type: copying a relative path between two Markdown files inserts a Markdown link via the new `extMD` builder in `src/import-text.ts`, controlled by the new `importStatements.markdown.markdownSupport` setting (style ``).
+- **Image-into-Markdown imports.** Copying an image (`.gif`, `.jpeg`, `.jpg`, `.png`) while a `.md` file is active inserts a Markdown image, driven by the new `extImgMD` builder and the `importStatements.markdown.markdownImageSupport` setting, which offers an inline style (``) and a reference style (`![alt-text][image]` with a trailing `[image]: path "Hover text"` definition).
+- **New configuration plumbing for Markdown.** Added `markdownEnum` / `markdownImageEnum` to `src/config-enum.ts`, the `markdownSupport` / `markdownImageSupport` fields to the `Config` interface, `configEnum` keys, the `param` map, and two getters in `src/config-retrival.ts`, plus the matching `onDidChangeConfiguration` observers in `src/extension.ts` so the settings apply live.
### Changed
-- Updated **README.md**.
-## [0.1.12] - 2020-03-20
+- **Relaxed cross-extension validation for images.** `extension.autoImportPaste` previously required the clipboard and active file to share an extension; it now also accepts an image source dropped into a Markdown file (`imageMDSupport = isMarkdownActive && isImageClipboard`), with `.gif` / `.jpeg` / `.jpg` / `.png` and `.md` added to `validTypes` in `src/extension.ts`.
+- **Markdown imports always insert at the cursor.** In `src/import-position.ts`, `pasteImport()` now routes any `.md`/Markdown import to `importToCursor()` regardless of the configured Top/Bottom/Cursor placement, using an internal `markdown` marker that is stripped before insertion.
+- **Tidied `.gitignore` and workspace settings.** `.gitignore` gained section headers and a `/images/test` entry, and `.vscode/settings.json` was trimmed to a single `files.exclude` rule.
+- **Docs and demo refresh.** Added `DEMO.md` and the `images/markdown.gif` / `images/markdown-image.gif` walkthroughs, and reworked `README.md` (link references, a Markdown section, and a TODO note for HTML support); these are documentation-only and ship no behavior change.
+
+## v0.1.14 (2020-03-21)
### Changed
-- Updated **README.md**.
-## [0.1.11] - [0.1.10] - 2020-03-19
+- **README copy polish.** Minor punctuation cleanup in `README.md` — added trailing periods to the "Select Install Extensions" install step and to the "More extensions of mine" link label (and its matching link definition). No code, command, setting, or behavior changes; the `package.json` version was bumped `0.1.13` → `0.1.14`.
+
+## v0.1.13 (2020-03-20)
### Fixed
-- Corrected internal linking issues in **README.md**.
-## [0.1.9] - 2020-03-19
+- **`README.md` keybinding typo.** Corrected the documented shortcut for copying a relative path from `Ctrl+Shit+A` to `Ctrl+Shift+A`; docs-only, no behavior change (version bumped to `0.1.13` in `package.json`).
+
+## v0.1.12 (2020-03-20)
### Fixed
-- Updated settings preview.
-## [0.1.8] - 2020-03-19
+- **Corrected the keybinding typo in the README Commands table.** The `Copy path` shortcut was misspelled `Ctrl+Shit+A`; it now reads `Ctrl+Shift+A`, matching the actual binding. The surrounding markdown table separators and column padding in `README.md` were tidied up at the same time. Docs-only patch release — the version bump in `package.json` and `package-lock.json` (`0.1.11` → `0.1.12`) carries no source, command, setting, language, or dependency changes.
+
+## v0.1.11 (2020-03-19)
+
+### Fixed
+
+- **Broken in-page README navigation.** Corrected the "Copy and paste like import" table-of-contents link in `README.md` (`#Heres-my-solution` → `#heres-my-solution-`) so it resolves to the rendered heading anchor instead of dead-ending.
+
+## v0.1.10 (2020-03-19)
### Changed
-- Updated settings preview GIF file.
-## [0.1.7] - 2020-03-19
+- **Renamed the README settings heading.** `## Extension Settings` became `## Configuration Settings`, and a couple of trailing-whitespace trims were applied to the supported-file-types line and the "Import to cursor" demo heading.
-### Added
-- Features and demo section in **README.md**.
+### Fixed
+
+- **`README.md` in-page navigation links.** The "Copy and paste like import" entry in the table of contents pointed at an anchor (`#Here's-my-solution`) whose apostrophe didn't match the generated heading slug, so the link didn't jump; corrected to `#Heres-my-solution`.
+
+## v0.1.9 (2020-03-19)
### Changed
-- Removed newline (`\n`) in the "import to cursor" functionality.
-## [0.1.6] - 2020-03-16
+- **Broadened the workspace ignore globs.** `.vscode/settings.json` `files.exclude` now hides `**/out`, `**/node_modules`, `**/.vscode-test/`, and `**/*.vsix` from the editor (developer-only config; no effect on the published extension).
+
+### Fixed
+
+- **Stale settings-preview GIF.** The "Settings Preview" image in `README.md` was showing the old animation; the asset was refreshed and renamed `images/settings.gif` → `images/preview.gif`, with the README reference re-pointed to it.
+
+## v0.1.8 (2020-03-19)
### Changed
-- Modified extension description in **package.json**.
-- Removed unused command `closeAllNotif`.
-- Updated **README.md**.
-- Changed display name and repository name.
-- Added author information in **package.json**.
-#### Todo
-- Rename extension.
+- **Refreshed the settings preview animation.** Regenerated `images/settings.gif` (the README/Marketplace demo) so it reflects the current settings UI; no functional changes ship in this release.
-## [0.1.5] - 2020-03-16
+## v0.1.7 (2020-03-19)
### Changed
-- Renamed extension display name from *Auto Import* to *Auto Import Relative Path*.
-#### Todo
-- Rename package from *Auto Import* to *Auto Import Relative Path*.
- - See:
- 1. [How to rename my theme extension](https://github.com/Microsoft/vscode/issues/25988)
- 2. [Possible to change package name?](https://github.com/Binaryify/OneDark-Pro/issues/54)
+- **Expanded the README with Features and Demo sections.** Added a `## Features` list (linking to import-styles config, copy-paste import, and the three placements) and a `## Demo` section embedding three new screencasts — `images/cursor.gif`, `images/bottom.gif`, and `images/top.gif` — for import-to-cursor, import-to-bottom, and import-to-top. The settings reference was also re-leveled (`Import statements > X` headings flattened to `#### X` subsections under a single `### Import statements`).
-## [0.1.4] - 2020-03-16
+### Fixed
+
+- **No blank line on import-to-cursor.** `importToCursor()` in `src/import-position.ts` now inserts `this.importText.trim()`, stripping the trailing newline so an "Import to cursor" no longer leaves an empty line above the snippet. The top and bottom insert paths are unchanged.
+
+## v0.1.6 (2020-03-18)
### Changed
-- Preparations for renaming the extension.
-## [0.1.3] - 2020-03-16
+- **Notifications now disabled by default.** The `disableNotifs` setting default flipped from `false` to `true` in `package.json`, so notifications on file drop to the active pane are off out of the box.
+- **Clearer extension description.** The `package.json` `description` now reads "Auto import relative path without typing long and tedious import statements and file paths." (was "Auto import without...").
+- **Author metadata added.** Added an `author` block (`John James Ermitaño`) to `package.json`.
### Fixed
-- Corrected "copy relative on-focus" behavior in the text editor.
-## [0.1.2] - 2020-03-15
+- **Documentation refresh.** Updated `README.md` usage steps, capitalization, and Marketplace badges (added an installs badge, normalized the badge URLs), and pointed the changelog link at the GitHub `CHANGELOG.md` instead of the Marketplace page.
+
+### Removed
+
+- **`closeAllNotif` setting.** Dropped the "Close all active notifications on Escape keydown" configuration entirely — removed from the `package.json` contribution, the `Config` interface, the `configEnum.CLOSEALLNOTIF` key and getter in `src/config-retrival.ts`, and the change observer in `src/extension.ts`.
+
+## v0.1.5 (2020-03-16)
### Changed
-- Updated configuration name.
-## [0.1.1] - 2020-03-15
+- **Renamed the extension to "Auto Import Relative Path".** The Marketplace `displayName` changed from `Auto Import Path` to `Auto Import Relative Path`, and the settings `configuration.title` from `Auto Import` to `Auto Import Relative Path`, to better describe what the extension does. The underlying package `name` (`auto-import`) and `publisher` (`ElecTreeFrying`) were left unchanged — a full package rename is still pending.
+- **Moved the repository to `auto-import-relative-path`.** The `homepage`, `repository.url`, and `bugs.url` in `package.json` (and the GitHub Issues link in `README.md`) were repointed from the old `ElecTreeFrying/auto-import` repo to `ElecTreeFrying/auto-import-relative-path`.
+- **Expanded the search keywords.** Added `relative` and `path` to the `keywords` array in `package.json` (alongside the existing `auto`, `import`, `javascript`, `css`).
+- **Rebranded the README.** The title, Marketplace badge labels, and tagline in `README.md` were updated to the new "Auto Import Relative Path" name.
+
+## v0.1.4 (2020-03-16)
+
+### Changed
+
+- **Renamed the extension to "Auto Import Path."** The Marketplace `displayName` in `package.json` and the `README.md` title were changed from "Auto Import" to "Auto Import Path"; the internal package `name` (`auto-import`), `publisher`, and `description` were left untouched. Recorded in `CHANGELOG.md` as an interim "Change extension name" note (the final name was still being decided).
+- **Version bump only.** `package.json` and `package-lock.json` bumped `0.1.3` → `0.1.4`; no code, command, setting, supported-language, or dependency changes shipped in this release.
+
+## v0.1.3 (2020-03-16)
### Fixed
-- Corrected table formatting in **README.md**.
-- Fixed issue with settings GIF not showing.
+
+- **`Auto Import: Copy` no longer steals focus to the Explorer.** The `extension.autoImportCopy` command dropped its `workbench.files.action.focusFilesExplorer` call, so copying a file path now respects the file you are focused on in the editor instead of forcing the Files Explorer to the front before running `copyFilePath`.
+
+## v0.1.2 (2020-03-15)
+
+### Changed
+
+- **Renamed the settings-page title to "Auto Import".** The extension's `configuration.title` in `package.json` changed from `auto-import` to `Auto Import`, so the group now reads as a proper display name in the VS Code Settings UI. No commands, behavior, supported languages, or dependencies changed in this release.
+
+## v0.1.1 (2020-03-15)
### Changed
-- Changed icon from square to circle.
-## [0.1.0] - 2020-03-15
+- **New extension icon.** Replaced `images/github.png` (the icon referenced by `package.json`) with an updated graphic.
+- **Version bump.** Bumped `version` to `0.1.1` in `package.json` and `package-lock.json`.
+
+### Fixed
+
+- **Commands table now renders on the Marketplace.** Rewrote the Commands table in `README.md` as a proper pipe-delimited Markdown table (with header separator and outer borders) so it displays correctly instead of as plain text.
+- **Version badge fixed.** Switched the Marketplace version badge in `README.md` from the `version-short` to the full `version` `vsmarketplacebadge` URL.
+
+## v0.1.0 (2020-03-15)
### Added
-- `Auto Import: Copy path` command.
-- Notification for the `Auto Import: Copy path` command.
+
+- **New `Auto Import: Copy path` command.** A dedicated `extension.autoImportCopy` command, bound to `cmd/ctrl+shift+a` and active in both the editor and the Explorer (`editorTextFocus || filesExplorerFocus`). It focuses the Files Explorer, runs VS Code's built-in `copyFilePath`, and validates the result against the supported `scripts`/`styles` extensions before writing it to the clipboard.
+- **Copy confirmation toast.** A successful copy now shows a `Copied: ` information message so it's clear which file's path was placed on the clipboard.
+- **`onCommand` activation for the new command.** `onCommand:extension.autoImportCopy` added to `activationEvents` in `package.json`.
### Changed
-- Removed unnecessary notification pop-ups.
-- Renamed `Auto Import` command to `Auto Import: Paste relative`.
-- Updated demo GIF in **Usage**.
-- Added settings preview GIF.
-- Added badges to **README.md**.
-## [0.0.6] - 2020-03-15
+- **Renamed `Auto Import` to `Auto Import: Paste relative`.** The paste command id changed from `extension.autoImport` to `extension.autoImportPaste` (still bound to `cmd/ctrl+i`), with its command title and `onCommand` activation event updated to match.
+- **Moved path math into `ImportText`.** The relative-path resolution, `./` same-directory prefixing, and `extname` extraction were lifted out of `extension.ts`'s `setup()` and into the `ImportText` constructor, whose signature changed to `(param, toPath, fromPath)`. `extension.ts` no longer imports the `relative` module directly.
+- **Tightened notification handling.** Both commands now call `notifications.clearAll` before running to clear stale popups, and the `notify()` invalid-import branch was rewritten around explicit "both" / "either" source/destination validity checks.
+- **README overhaul.** Marketplace version/downloads/ratings badges, a Key Binding / Command / Description table, sectioned settings headings, a refreshed usage demo (`images/playback.gif`), a new settings-preview clip (`images/settings.gif`), and Changelog / Contributing / Related sections.
+
+## v0.0.6 (2020-03-15)
### Fixed
-- Resolved `TypeError: camelcase_1.default is not a function`.
-## [0.0.5] - 2020-03-15
+- **`TypeError: camelcase_1.default is not a function` on every import.** `src/import-text.ts` imported the `camelcase` package as a default import (`import camelcase from 'camelcase'`), which transpiled to a `camelcase_1.default(...)` call that was `undefined` at runtime — so the PascalCase import-name derivation (`camelcase(importName, { pascalCase: true })`) crashed whenever the `extension.autoImport` command ran. Switched to a namespace import (`import * as camelcase from 'camelcase'`) so the CommonJS callable resolves correctly.
+- **`camelcase` missing from `dependencies`.** The package was imported in source but never declared as a runtime dependency; added `camelcase` `^5.3.1` to `dependencies` (and `@types/camelcase` `^5.2.0` to `devDependencies`) so a clean install ships the module.
+
+### Removed
+
+- **Stray `console.clear()` on each run.** Dropped the `console.clear()` call at the top of the `extension.autoImport` handler in `src/extension.ts`, which wiped console output every time the command was invoked.
+
+## v0.0.5 (2020-03-15)
### Added
-- Pasting import directly on the selected line at the top or bottom of the import list.
-## [0.0.4] - 2020-03-14
+- **`importType` setting to control where the import is pasted.** New string setting (enum `Top` / `Buttom` / `Cursor`) chooses whether the generated statement lands at the top of the import list, at the bottom of it, or on the currently selected line.
+- **`ImportPosition` insertion engine.** New `src/import-position.ts` houses an `ImportPosition` class with three strategies dispatched by `pasteImport()`: `importToTop()` inserts at line 0, `importToCursor()` inserts at the selection's anchor line, and `importToBottom()` scans the document for import/`require`/`import()`/`@import`/`@use` indicator lines and inserts after the last one found.
+
+### Changed
+
+- **Auto Import now delegates insertion to `ImportPosition`.** `setup()` in `src/extension.ts` no longer reopens the document via `showTextDocument` and hardcodes the statement at position `(0,0)`; it constructs an `ImportPosition` and calls `pasteImport()`, so placement honors the new `importType` setting. The setting is also wired into the live `onDidChangeConfiguration` handler (via the `IMPORTTYPE` key) so changes apply without reload.
+- **Plumbed `importType` through the config layer.** Added `importTypeEnum` in `src/config-enum.ts`, the `importType` field on the `Config` interface, the `IMPORTTYPE` key, and an `importType` getter in `src/config-retrival.ts`.
+- **Config-retrieval cleanup.** Removed the unused `*DefaultValue` getters across `src/config-retrival.ts` and dropped the commented-out `importPosition` / `importCursor` scaffolding.
### Fixed
-- Corrected version inconsistencies.
-## [0.0.3] - 2020-03-14
+- **Console cleared per run.** The `extension.autoImport` command now calls `console.clear()` at the start of each invocation.
+
+## v0.0.4 (2020-03-14)
+
+### Fixed
+
+- **Version-numbering consistency.** Bumped the package `version` to `0.0.4` in `package.json` and `package-lock.json` and corrected the `CHANGELOG.md` history, where a prior release had been mislabeled `v0.0.2` (it was `v0.0.3`) and the original `v0.0.2` entry carried the wrong date. No code or behavior changes.
+
+## v0.0.3 (2020-03-14)
+
+### Added
+
+- **`bugs` field in `package.json`.** Declared a contact email and the GitHub issues URL (`https://github.com/ElecTreeFrying/auto-import/issues`) so the Marketplace listing surfaces a report-an-issue link.
### Changed
-- Updated **README.md**.
-- Revised export name description.
-## [0.0.2] - 2020-03-13
+- **Tightened every setting description.** Reworded all `importStatements.*` and top-level setting descriptions in `package.json` and `README.md` to be terser (e.g. "Select **.js** import style", "Toggle semicolon...", and `addExportName` now reads "(Angular/.tsx)" instead of the prior HTML-laden "(Angular) *same behaviour applies in .tsx files.*").
+- **README polish.** Renamed the title from "Auto-import" to "Auto Import", restructured the headings (Supported file types / Usage / Extension Settings), and clarified the copy-then-paste workflow; supported file types remain JS, JSX, TS, TSX, CSS, SCSS, SASS, LESS.
+- **Source formatting pass.** Added block braces to single-line `if`/`return` statements and reordered the field assignments in `ImportText`'s constructor in `src/extension.ts` and `src/import-text.ts`; no behavior change.
+
+## v0.0.2 (2020-03-13)
### Added
-- "Homepage" field added in **package.json**.
-## [0.0.1] - 2020-03-13
+- **First functional release.** Replaced the prior `0.0.1` Yeoman hello-world scaffold (`extension.helloWorld`, placeholder metadata) with a working auto-import extension and published it to the Marketplace.
+- **Auto Import command.** A single `extension.autoImport` command ("Auto Import") that inserts a relative-path import for a previously copied file. Workflow: copy a file's path from the Explorer tree view (`Shift+Alt+C`), then run the command in the destination editor — the import statement is generated and inserted at the top of the file (`Position(0,0)`).
+- **Keybinding and Command Palette entry.** Bound to `ctrl+i` / `cmd+i` (active only when `editorTextFocus`), and also reachable via the Command Palette as *Auto Import*.
+- **Eight supported file types.** Both source and destination may be `.js`, `.jsx`, `.ts`, `.tsx`, `.css`, `.scss`, `.sass`, or `.less`. The command only fires when both files are a supported type, share the same extension, and are not the same file.
+- **Relative-path computation.** Uses the newly added `relative` dependency to compute the path between destination and source, normalizes Windows `\` separators to `/`, and prefixes `./` for same-directory siblings. The file extension is stripped from the emitted path by default.
+- **Per-language import-style settings.** `importStatements.javascript.jsSupport` / `jsxSupport` (15 shapes each, from `import … from ''` through `require()` and dynamic `import()` variants), `importStatements.typescript.tsSupport` / `tsxSupport` (5 shapes each), and `importStatements.stylesheet.cssSupport` (`@import` / `@import url()`), `scssSupport` (adds `@use`), and `lessSupport` (`@import` / `@import ()`). SCSS/SASS share the SCSS builder; `.sass` routes to the same shapes as `.scss`, and leading `_` is stripped from SCSS partial paths.
+- **Formatting and Angular settings.** `quoteStyle` (single vs. double quotes), `addSemicolon` (trailing `;`), the `withExtnameJS` / `withExtnameTS` / `withExtnameCSS` toggles (append the file extension to the path), and `addExportName` (include a PascalCase component name in TS/TSX imports, for Angular). The component name is derived from the filename via `camelcase`.
+- **Notification settings.** `disableNotifs` and `closeAllNotif` configuration keys, plus contextual warning/error toasts for *No active pane*, *Same file path*, and *Invalid import*.
+- **Live configuration reload.** A `workspace.onDidChangeConfiguration` listener updates the in-memory settings whenever any of the extension's configuration keys change, so edits take effect without a reload.
+- **Activation events.** `onCommand:extension.autoImport` plus `onLanguage:*` events for all eight supported languages.
+- **Marketplace metadata and assets.** Publisher `ElecTreeFrying`, display name "Auto Import", description, `MIT` license, extension icon, keywords, gallery banner, and repository URL added to `package.json`; `LICENSE.md`, a README rewrite, and `images/github.png` + `images/playback.gif` demo assets added.
+
+### Changed
+
+- **Version bump and homepage.** Bumped `version` from `0.0.1` to `0.0.2` in `package.json` / `package-lock.json`, and added a `homepage` field pointing at the GitHub README.
+
+### Removed
+
+- **Scaffold artifacts.** Dropped the generator's `vsc-extension-quickstart.md` and the placeholder `extension.helloWorld` command / "auto-import" + "test" metadata from the original scaffold.
+
+## v0.0.1 (2020-03-05)
+
+### Added
-- Published extension on the marketplace.
+- **Initial project scaffold.** First commit ("add: working base") establishes the extension from the standard VS Code generator template — not yet the relative-path import tool it would later become.
+- **`extension.helloWorld` command.** The sole contributed command ("Hello World"), activated via `onCommand:extension.helloWorld`; `src/extension.ts` logs an activation message and shows a "Hello World!" information message.
+- **TypeScript build pipeline.** `tsc -p ./` compiles `src/` to `out/`, with `compile`, `watch`, `pretest`, and `vscode:prepublish` npm scripts; `tsconfig.json` and `main` point at `./out/extension.js`.
+- **Lint setup.** ESLint with `@typescript-eslint/parser` and `@typescript-eslint/eslint-plugin` via `.eslintrc.json` and the `lint` script (`eslint src --ext ts`).
+- **Test harness.** Mocha + `vscode-test` integration scaffolding — `src/test/runTest.ts`, `src/test/suite/index.ts`, and a placeholder `src/test/suite/extension.test.ts` — wired to the `test` script.
+- **Editor and packaging config.** `.vscode/` files (`launch.json`, `tasks.json`, `settings.json`, `extensions.json`), `.vscodeignore`, `.gitignore`, baseline `package.json` (`auto-import`, engine `vscode ^1.42.0`, category `Other`), and a generated `package-lock.json`.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..123c88f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,95 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commit conventions
+
+Do NOT append a `Co-Authored-By: Claude ...` trailer (or any other Claude/AI attribution) to commit messages. Write commits as if authored solely by the user.
+
+## Project
+
+VS Code extension that generates relative-path import statements for JS/TS/JSX/TSX/MDX/CSS/SCSS/HTML/Markdown/Vue/Svelte/Astro/LaTeX. Two input gestures: **copy-paste** (eight commands) and **drag-and-drop** (a `DocumentDropEditProvider` for all 13 destination languages). The eight commands (`extension.copyFilePath`, `extension.pasteImport`, `extension.copyPaste`, `extension.pasteImportWithStyle`, `extension.setDefaultImportStyle`, `extension.setImportPlacement`, `extension.togglePreserveScriptExtension`, `extension.resetImportStyles`) are registered in `src/extension.ts`. The first three are bound to keybindings in `package.json` (`cmd/ctrl+shift+a`, `cmd/ctrl+i`, and `alt+d` in the explorer respectively); the latter five are reachable via the Command Palette (and `pasteImportWithStyle` also via the `copy-success` toast button). The drop provider is registered alongside the commands in `activate()` and uses the same snippet pipeline.
+
+## Subdirectory guides
+
+Each directory under `src/` has its own pair of nested guides. Read the directory's `CLAUDE.md` first when editing files inside it; read its `README.md` when navigating or onboarding.
+
+| Directory | Scope | Guides |
+|-----------|-------|--------|
+| `src/` | Source-tree overview, dependency layering, naming conventions | [`src/README.md`](src/README.md), [`src/CLAUDE.md`](src/CLAUDE.md) |
+| `src/commands/` | The eight commands (five paste/copy + three settings); clipboard data channel, twelve-clause gating (paste/copy only) | [`src/commands/README.md`](src/commands/README.md), [`src/commands/CLAUDE.md`](src/commands/CLAUDE.md) |
+| `src/drop/` | DocumentDropEditProvider; drag-from-Explorer import generation | [`src/drop/README.md`](src/drop/README.md), [`src/drop/CLAUDE.md`](src/drop/CLAUDE.md) |
+| `src/editor/` | VS Code-API helpers (clipboard, snippet insertion, notifications) | [`src/editor/README.md`](src/editor/README.md), [`src/editor/CLAUDE.md`](src/editor/CLAUDE.md) |
+| `src/snippets/` | Per-language snippet builders + dispatch; style sync rules; JSX/TSX/MDX shared algorithm | [`src/snippets/README.md`](src/snippets/README.md), [`src/snippets/CLAUDE.md`](src/snippets/CLAUDE.md) |
+| `src/snippets/languages/` | The ten per-language leaf builders; config/pure split, intra-directory delegation, source-classification routing | [`src/snippets/languages/README.md`](src/snippets/languages/README.md), [`src/snippets/languages/CLAUDE.md`](src/snippets/languages/CLAUDE.md) |
+| `src/path/` | Pure path math (no `vscode` import); `./` prefix rule | [`src/path/README.md`](src/path/README.md), [`src/path/CLAUDE.md`](src/path/CLAUDE.md) |
+| `src/config/` | Workspace-config access; three-site sync rule | [`src/config/README.md`](src/config/README.md), [`src/config/CLAUDE.md`](src/config/CLAUDE.md) |
+| `src/constants/` | Runtime gating tables; runtime mirror of `types/file-extension.ts` | [`src/constants/README.md`](src/constants/README.md), [`src/constants/CLAUDE.md`](src/constants/CLAUDE.md) |
+| `src/types/` | Cross-cutting type unions (no enums) | [`src/types/README.md`](src/types/README.md), [`src/types/CLAUDE.md`](src/types/CLAUDE.md) |
+| `src/test/` | Mocha BDD tests; runs from `out/`, not `dist/` | [`src/test/README.md`](src/test/README.md), [`src/test/CLAUDE.md`](src/test/CLAUDE.md) |
+| `qa/` | Per-language manual-QA checklists + matching by-language fixture workspaces, the `_authoring/` checklist-codegen pipeline (RECIPE + frozen-IR PROFILE), and a standalone framework demo-workspace | [`qa/README.md`](qa/README.md), [`qa/CLAUDE.md`](qa/CLAUDE.md), [`qa/_authoring/README.md`](qa/_authoring/README.md), [`qa/checklists/README.md`](qa/checklists/README.md), [`qa/checklists/CLAUDE.md`](qa/checklists/CLAUDE.md), [`qa/workspace/README.md`](qa/workspace/README.md), [`qa/workspace/CLAUDE.md`](qa/workspace/CLAUDE.md) |
+| `docs/` | Design library (the *why*): the import-statements design tree (criteria, decisions, rejection ledgers) + a reader-facing QA checklist-codegen overview. The product spec `SPEC.md` stays in root, paired with `README.md`. | [`docs/CLAUDE.md`](docs/CLAUDE.md), [`docs/import-statements/CLAUDE.md`](docs/import-statements/CLAUDE.md) |
+
+## Commands
+
+```bash
+npm run compile # type-check + lint + esbuild bundle (dev) → dist/extension.js
+npm run watch # parallel esbuild watch + tsc --noEmit watch (via npm-run-all)
+npm run package # production bundle (used by vscode:prepublish)
+npm run check-types # tsc --noEmit (no bundling)
+npm run compile-tests # tsc → out/ (separate from esbuild; required before npm test)
+npm run watch-tests # tsc watch for the test build
+npm run lint # eslint src
+npm test # @vscode/test-cli; pretest runs compile-tests + compile + lint
+npm run test:coverage # @vscode/test-cli with --coverage (opt-in; writes coverage/ + text report)
+```
+
+Run a single test by filtering Mocha via `npm test -- --grep ""` (the filter is forwarded to `@vscode/test-cli`). Tests must be compiled first — `out/test/**/*.test.js` is what `.vscode-test.mjs` picks up.
+
+Press **F5** inside VS Code to launch an Extension Development Host with the extension loaded (`.vscode/launch.json`); the default build task (`npm: watch`) runs first.
+
+Publishing flow lives in `process/workflow.md` (gitignored): `vsce publish patch|minor|major` after `npm run vscode:prepublish`.
+
+## Architecture
+
+The source tree is layered by responsibility, with strict directional dependencies:
+
+```
+src/
+├── extension.ts # activate/deactivate; registers 8 commands + drop provider
+├── gating.ts # shared isPairSupported() — ten-clause extension-pair check
+├── commands/ # public command surface (one file per command)
+├── drop/ # DocumentDropEditProvider (drag-from-Explorer imports)
+├── editor/ # VS Code-API touching helpers (clipboard, snippets, notifications)
+├── snippets/ # per-language snippet builders + dispatch
+├── path/ # pure path math (no `vscode` import; Node-testable)
+├── config/ # workspace-config access (getAutoImportSetting)
+├── constants/ # cross-import gating tables
+├── types/ # cross-cutting type unions
+└── test/ # Mocha BDD tests (runs from out/, not dist/)
+```
+
+Allowed dependency direction: `commands → gating, editor, snippets, config, constants, types`; `drop → gating, editor, snippets, constants, types`; `gating → editor, constants, types`; `snippets → config, path, editor, types, constants`; `editor → config, path, constants, types`; `path → types`. Lower layers never import from higher layers. Internal-only sibling modules in `snippets/` are prefixed with `_` (`_styles.ts`, `_react.ts`, `_class-name.ts`).
+
+## Cross-cutting sync rules
+
+These multi-site contracts silently break on drift. The linked guides have the full rules.
+
+- **Four-site extension sync** — adding a file extension requires updates in `types/file-extension.ts` → `constants/extensions.ts` → `snippets/dispatch.ts` → `snippets/variants.ts` (non-script asset sources into JSX/TSX/MDX route through `snippets/_react.ts:buildAssetImportStatement` instead of `dispatch.ts`; of those, only JSX/TSX/MDX-exclusive sources like fonts skip the `constants` gating table — images, media, documents, and components also target gated destinations and keep their `constants` entries). See [`src/types/CLAUDE.md`](src/types/CLAUDE.md).
+- **Three-site config sync** — setting enum strings must be byte-identical across `package.json` → `snippets/_styles.ts` → per-language `switch`. Four dormant single-shape keys (`cssImage`, `scssImage`, `htmlStyleSheet`, `markdown`) are the exception — kept in `package.json` for backward compatibility but not style-synced at runtime. See [`src/config/CLAUDE.md`](src/config/CLAUDE.md).
+- **Two-site button-label sync** — toast action-button labels in `editor/notification.ts` must match the `switch` cases in the dispatching command character-for-character, at two sites: the `copy-success` buttons ↔ `commands/copy-file-path.ts`, and the `styles-reset` **Undo** ↔ `commands/reset-import-styles.ts`. See [`src/commands/CLAUDE.md`](src/commands/CLAUDE.md).
+- **Runtime-type mirror sync** — `IMAGE_FILE_EXTENSIONS` (mirrors `ImageFileExtension`) and `TEXT_TRACK_FILE_EXTENSIONS` (mirrors `TextTrackFileExtension`) in `constants/extensions.ts` track their type unions; `MEDIA_FILE_EXTENSIONS` holds video + audio only (`.vtt` lives in `TEXT_TRACK_FILE_EXTENSIONS`, and both are spread together into the destination lists). See [`src/constants/CLAUDE.md`](src/constants/CLAUDE.md).
+
+## Build/test layout quirks
+
+- Two independent TS pipelines: **esbuild** (bundles `src/extension.ts` → `dist/extension.js` as CommonJS with `vscode` external; sourcemaps in dev, minified with `--production`; see `esbuild.js`) and **tsc** (`compile-tests` emits `src/**` → `out/` for the test runner). The two outputs are independent — don't try to share them.
+- `tsconfig.json` is `module: Node16`, `target: ES2022`, `strict: false`, `rootDir: src`, `sourceMap: true`, and `types: ["node", "mocha"]`. New source files belong under `src/`. (`sourceMap` is on so `test:coverage` maps `out/` back to `src/`.)
+- Mocha tests are written in BDD style (`describe`/`it`); the runner UI is set to `bdd` in `.vscode-test.mjs`. Tests use Node's built-in `assert` (Chai/Sinon were dropped in commit `f06101f`). The test runner glob is `out/test/**/*.test.js` — only files emitted by `compile-tests` get picked up.
+- **Coverage is opt-in:** `npm run test:coverage` runs the same suite with `vscode-test --coverage` (V8/c8, ~96% lines). `.vscode-test.mjs` uses the `{ tests, coverage }` form — the `coverage` block (`includeAll`, `exclude` of `test`/`*.test.*`/`types`, `text`+`html` reporters) is silently ignored unless `--coverage` is passed. Report lands in the git-ignored `coverage/`.
+- `process/` is gitignored — private publishing notes + access tokens; never commit it. Under `.claude/`, the shared tooling (`agents/`, `skills/`, `workflows/`) is tracked; `agents/state/` and everything else under `.claude/` stays gitignored.
+
+## Naming conventions
+
+- Files use noun-only kebab-case: `relative-path.ts`, `file-path-info.ts`. Do not reintroduce suffixes like `.command.ts`, `.util.ts`, `-fn.ts`, `.types.ts`, `.enums.ts`, `.interface.ts` — the parent directory carries the kind signal.
+- Modules whose filename starts with `_` (e.g., `snippets/_styles.ts`, `snippets/_react.ts`, `snippets/_class-name.ts`) are internal to their parent subtree; importing them from outside `snippets/` is a smell. The `snippets/languages/` modules importing `../_styles`, `../_react`, and `../_class-name` is expected.
+- The only barrel file is `src/commands/index.ts`. Other directories use direct imports so dependency direction stays visible at the call site.
diff --git a/DEMO.md b/DEMO.md
deleted file mode 100644
index 56756ee..0000000
--- a/DEMO.md
+++ /dev/null
@@ -1,297 +0,0 @@
-# Auto Import Relative Path (DEMO)
-
-[![Version][version svg]][package] [![Installs][installs svg]][package] [![Downloads][downloads svg]][package] [![Ratings][ratings svg]][package]
-
-**Auto Import Relative Path** is a [Visual Studio Code][VS Code] extension that simplifies how you import files in your projects. No more memorizing or typing out long relative paths — let this extension do the heavy lifting for you. Whether you’re building a small side project or a complex application with hundreds of files, Auto Import Relative Path will help you streamline your workflow and keep your code clean.
-
-[VS Code]: https://code.visualstudio.com/
-
-[VS Code]: https://code.visualstudio.com/
-[extension]: https://marketplace.visualstudio.com/VSCode
-[version svg]: https://vsmarketplacebadges.dev/version-short/electreefrying.auto-import.png
-[installs svg]: https://vsmarketplacebadges.dev/installs/electreefrying.auto-import.png
-[downloads svg]: https://vsmarketplacebadges.dev/downloads/electreefrying.auto-import.png
-[ratings svg]: https://vsmarketplacebadges.dev/rating-short/ElecTreeFrying.auto-import.png
-[package]: https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.auto-import
-
----
-
-## Table of Contents
-
-- [Auto Import Relative Path (DEMO)](#auto-import-relative-path-demo)
- - [Table of Contents](#table-of-contents)
- - [Features](#features)
- - [Supported File Extensions](#supported-file-extensions)
- - [Commands](#commands)
- - [Import Statement Position](#import-statement-position)
- - [Append to Cursor](#append-to-cursor)
- - [Append to the Bottom of the Import List](#append-to-the-bottom-of-the-import-list)
- - [Append to the Top of the Import List](#append-to-the-top-of-the-import-list)
- - [Keybindings](#keybindings)
- - [Auto Import from Explorer](#auto-import-from-explorer)
- - [Single Keybinding Import](#single-keybinding-import)
- - [Auto Import Across Active Tabs](#auto-import-across-active-tabs)
- - [HTML Support](#html-support)
- - [Import Script and Stylesheet](#import-script-and-stylesheet)
- - [Markdown Support](#markdown-support)
- - [Import Image to Markdown](#import-image-to-markdown)
- - [Import Markdown](#import-markdown)
- - [Changelog](#changelog)
- - [Contributing](#contributing)
- - [Support](#support)
- - [Support the Project](#support-the-project)
- - [Related](#related)
-
----
-
-## Features
-
-- **Instant Relative Path Imports**: Quickly copy and paste a file’s path in your editor, or auto-import it in a single command.
-- **Flexible Placement**: Place new import statements at the top, the bottom, or even at your cursor’s current position.
-- **Highly Customizable**: Configure the style of import statements for JavaScript, TypeScript, React, CSS, SCSS, HTML, and Markdown.
-- **Time-Saving**: Ideal for large projects with complex directory structures.
-- **Keyboard Friendly**: Offers intuitive default shortcuts so you can keep your hands on the keyboard and stay in the flow.
-
----
-
-## Supported File Extensions
-
-| File Type | File Extension |
-| --------------- | ---------------------------- |
-| **Programming** | `.js`, `.jsx`, `.ts`, `.tsx` |
-| **Markup** | `.html`, `.md` |
-| **Stylesheet** | `.css`, `.scss` |
-
----
-
-## Commands
-
-
-| Command | Windows/Linux | macOS | Description | Demo |
-|-----------------------|----------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------|
-| **Auto Import: Copy** | Ctrl+Shift+A | Cmd+Shift+A | **Copy** the relative path of the selected file in Explorer. | [See it in action](#auto-import-from-explorer) |
-| **Auto Import: Paste**| Ctrl+I | Cmd+I | **Paste** the import statement into your active text editor. | [See it in action](#auto-import-from-explorer) |
-| **Auto Import: Auto** | Alt+D | Option+D | **Auto** copy and paste the import statement from Explorer to your active text editor in one step (copy + paste). | [Single keybinding import](#single-keybinding-import) |
-
----
-
-## Import Statement Position
-
-You can configure where the import statement goes in your code. Choose any of the following placement options:
-
----
-
-### Append to Cursor
-
-**Windows/Linux:**
-1. Press Ctrl+Shift+A on a file in Explorer.
-2. Switch to your active text editor and press Ctrl+I to paste.
-3. Or use Alt+D to do both in one step (copy + paste).
-
-**macOS:**
-1. Press Cmd+Shift+A on a file in Explorer.
-2. Switch to your active text editor and press Cmd+I to paste.
-3. Or use Option+D to do both in one step (copy + paste).
-
-![Cursor Demo][cursor]
-
-[cursor]: https://res.cloudinary.com/october7/image/upload/v1679982363/github/auto-import-relative-path/cursor.gif "import to cursor using ctrl+i command"
-
----
-
-### Append to the Bottom of the Import List
-
-**Windows/Linux:**
-1. Press Ctrl+Shift+A on a file in Explorer.
-2. Press Ctrl+I in your code editor to paste at the bottom.
-3. Or use Alt+D to copy and paste in one step.
-
-**macOS:**
-1. Press Cmd+Shift+A on a file in Explorer.
-2. Press Cmd+I in your code editor to paste at the bottom.
-3. Or use Option+D to copy and paste in one step.
-
-![Bottom Demo][bottom]
-
-[bottom]: https://res.cloudinary.com/october7/image/upload/v1679982363/github/auto-import-relative-path/bottom.gif "import to bottom using ctrl+i command"
-
----
-
-### Append to the Top of the Import List
-
-**Windows/Linux:**
-1. Press Ctrl+Shift+A on a file in Explorer.
-2. Press Ctrl+I in your code editor to paste at the top.
-3. Or use Alt+D to copy and paste in one step.
-
-**macOS:**
-1. Press Cmd+Shift+A on a file in Explorer.
-2. Press Cmd+I in your code editor to paste at the top.
-3. Or use Option+D to copy and paste in one step.
-
-![Top Demo][top]
-
-[top]: https://res.cloudinary.com/october7/image/upload/v1679982367/github/auto-import-relative-path/top.gif "import to top using ctrl+i command"
-
----
-
-## Keybindings
-
-### Auto Import from Explorer
-
-**Windows/Linux**
-1. In the Explorer, press Ctrl+Shift+A on a file.
-2. In the editor, press Ctrl+I to paste the import statement.
-
-**macOS**
-1. In the Explorer, press Cmd+Shift+A on a file.
-2. In the editor, press Cmd+I to paste the import statement.
-
-![Auto import from explorer demo][keybinding-copy-and-paste]
-
-[keybinding-copy-and-paste]: https://res.cloudinary.com/october7/image/upload/v1679982581/github/auto-import-relative-path/keybinding-copy-and-paste.gif "Auto import from explorer demo"
-
----
-
-### Single Keybinding Import
-
-**Windows/Linux**
-1. Press Alt+D on a file in the Explorer.
-2. The import statement is automatically inserted into your active editor.
-
-**macOS**
-1. Press Option+D on a file in the Explorer.
-2. The import statement is automatically inserted into your active editor.
-
-![Single keybinding import demo][keybinding-single]
-
-[keybinding-single]: https://res.cloudinary.com/october7/image/upload/v1679982581/github/auto-import-relative-path/keybinding-single.gif "Single keybinding import demo"
-
----
-
-### Auto Import Across Active Tabs
-
-**Windows/Linux**
-1. Press Ctrl+Shift+A on a file in the Explorer.
-2. Switch to any open tab and press Ctrl+I.
-3. The relative path import will be pasted where your cursor is.
-
-**macOS**
-1. Press Cmd+Shift+A on a file in the Explorer.
-2. Switch to any open tab and press Cmd+I.
-3. The relative path import will be pasted where your cursor is.
-
-![Auto import from text editor demo][keybinding-feature]
-
-[keybinding-feature]: https://res.cloudinary.com/october7/image/upload/v1679982581/github/auto-import-relative-path/keybinding-feature.gif "Auto import from text editor demo"
-
----
-
-## HTML Support
-
-### Import Script and Stylesheet
-
-**Windows/Linux**
-1. Press Ctrl+Shift+A on a `.js` or `.css` file in the Explorer.
-2. Press Ctrl+I in your HTML file, or use Alt+D for a single-step import.
-3. The extension automatically creates the appropriate `` | Modern minimal **(default)** |
+| 1 | `` | Deferred execution |
+| 2 | `` | ES module |
+| 3 | `` | Async execution |
+| 4 | `` | Legacy |
+
+**`` — 3 styles**
+
+Setting: `auto-import.importStatement.markup.htmlImageImportStyle`
+
+Used for image sources imported into `.html` destinations.
+
+| # | Snippet | Description |
+|---|---|---|
+| **0** | `` | Standard **(default)** |
+| 1 | `` | Lazy loading |
+| 2 | `` | Explicit dimensions (CLS prevention) |
+
+**`