diff --git a/.claude/skills/article-synthesis/SKILL.md b/.claude/skills/article-synthesis/SKILL.md new file mode 100644 index 0000000..2c824a0 --- /dev/null +++ b/.claude/skills/article-synthesis/SKILL.md @@ -0,0 +1,249 @@ +--- +name: article-synthesis +description: Draft an article for the documentation site by synthesizing the spec and research corpus into secondary literature. Use when the user asks to "write an article about X", "synthesize an article from the specs", "draft the article on the IR", or "turn R12 into an article". Enforces citation density, preserved reversals, and never overwriting a promoted article. +--- + +# Synthesize an article from the design record + +The specs under `docs/spec/` and the research notes under `docs/research/` are +primary sources. They are precise, cross-referenced, and written for reviewers +and agents. Nobody reads them for pleasure. + +This skill produces the secondary literature: an article-shaped page that tells +a higher-level story and cites the primary sources underneath it. Published +articles live in `docs/articles/` and appear high in the site's sidebar, above +the collapsed design record. + +**This is the only content on the site with no mechanical backstop.** The CLI +reference has a drift gate, examples have tests, links have a checker, prose +has Vale. An article has none of that, and no check can detect that it +misrepresented a source. The three constraints below exist because of that, and +they are not negotiable. + +## The three constraints + +### 1. Citation density + +Every non-obvious claim links to the spec or note it came from. + +The purpose is reviewability, not scholarly manners. With citations, a reviewer +checks "does this sentence match the cited paragraph". Without them, the only +available check is "does this match my memory of the corpus", which is not a +check. Assume the reviewer will spot-check three sentences at random and will +be annoyed if any of them is unsupported. + +Cite inline, as a normal Markdown link on the phrase making the claim: + +```markdown +The IR keeps a `raw` escape hatch for storage-format constructs Markdown has no +equivalent for, so a roundtrip preserves them byte-for-byte +([S28](../spec/S28-document-ir-foundation.md)). +``` + +Not as a bibliography at the bottom. A reader who wants to check one sentence +should not have to guess which of eight entries covers it. + +What does not need a citation: things a reader can verify by running a command, +definitions given earlier in the same article, and general background that is +not specific to this project. + +**Density is a floor on coverage, not a target for frequency.** Applied +literally, this rule produces prose nobody can read — a link on every clause, +often landing mid-phrase on a verb, so the reader's eye leaves the sentence +several times per sentence. That has already happened once. Three limits: + +- **At most one citation per sentence.** Where consecutive sentences draw on + the same source, cite once and move on. +- **Put the link at the end of the sentence or clause it supports**, never on a + verb or an arbitrary fragment inside it. A trailing + `([S31](../spec/S31-ir-normalization-and-whitespace.md))` reads better than a + link buried on the words "deliberately refuses". +- **Read a paragraph aloud.** If you stumble over where the links sit, move + them. + +A reviewer can check a claim that is cited at the end of its sentence just as +easily. Nothing is lost. + +### 2. Preserve reversals + +Dead ends, rejected options, and superseded recommendations are the payload. + +A model synthesizing a corpus produces, by default, a tidy narrative in which +the right answer was reached directly. That narrative erases the most +interesting content in the corpus and yields prose with no opinions in it. It +is also false: the corpus is full of decisions that were made, tested, and +reversed. + +Before you write a word, go looking for the reversals: + +```bash +grep -rn -i -E "rejected|reversed|superseded|instead of|we tried|abandoned|does not work|turned out" docs/spec/ docs/research/ +grep -rn "~~" docs/spec/ # struck-through open questions, i.e. resolved ones +git log --oneline -- docs/spec/.md +``` + +The resolved open questions at the bottom of a spec are the densest source: +each one is a fork where a decision was taken and the losing option is written +down. The research notes for a spike series (`R09` through `R13`, for example) +record what was measured and what failed. + +An article that mentions no rejected option is almost certainly wrong. If you +genuinely find none, say so in your report and explain why. + +### 3. Never overwrite + +Drafts go to `site/src/content/docs/_drafts/_.md`. Note the underscore on +**both** the directory and the filename. `docsLoader()`'s glob is +`**/[^_]*.{md,mdx}`, and the `[^_]` applies to the filename, not to any +directory above it — so a file at `_drafts/foo.md` is still loaded, still fails +content-schema validation for having no `title`, and still breaks the build. +The underscore on the filename is what actually excludes it. The `_drafts/` +directory is for tidiness. + +Drafts are tracked in git, so they can be reviewed in a pull request like +anything else. + +Promotion is a `git mv`, done by a human, which drops both underscores: + +```bash +git mv site/src/content/docs/_drafts/_.md docs/articles/.md +``` + +**Never write to `docs/articles/` directly.** If the user asks you to revise an +article that is already promoted, read it, write your revision to +`site/src/content/docs/_drafts/.md`, and show the user the diff: + +```bash +diff -u docs/articles/.md site/src/content/docs/_drafts/.md +``` + +Then stop and let them decide. Propose; do not apply. + +## Step 1 — Pick the story, not the spec + +An article is organized by story. A spec-shaped article is a spec. + +Ask what question a reader arrives with, and answer that. "Why does `mdd` have +its own document IR" is a story; "S28 explained" is not. Good articles usually +draw on several specs and notes, and often the interesting one is a research +note that a spec deliberately does not link to. + +If the user named a topic, use it. If they asked for "the next article", +propose two or three candidates with a one-line pitch each and let them choose +before you write. + +## Step 2 — Read the primary sources in full + +Read every spec and note you intend to cite, end to end. Do not cite from a +grep hit or from a summary; the citation is a promise that the cited paragraph +says what you claim. + +Read the ones you will not cite too, if they are adjacent — knowing what a +neighboring spec decided is often what stops an article from overstating. + +Then check the code. Specs describe intent at the time of writing; `src/mdd/` +is what ships. Where they disagree, the code wins for any claim about current +behaviour, and the disagreement itself is worth a sentence. + +## Step 3 — Write + +Format and conventions: + +- Plain `.md`. No YAML frontmatter — a build script derives the title from the + single level-1 `# Heading` at the top and strips it from the body, so the + file starts with exactly one `# ` heading. +- **Write every relative link as if the file already lived in + `docs/articles/`** — `../spec/S28-document-ir-foundation.md`, + `../research/R12-confluence-ir-comparison.md`, + `../guide/03-concepts.md`. Those links are broken while the draft sits in + `_drafts/`; that is expected, and nothing checks a draft. They resolve the + moment it is promoted, and `scripts/sync-docs.py` fails the build on one that + does not. +- Callouts use GitHub alert syntax (`> [!NOTE]`, `> [!WARNING]`, and so on), + which renders on GitHub and maps to Starlight asides. Not `:::note`. +- No MDX components. +- Length: 800 to 2000 words. Below that it is a guide page; above it, nobody + finishes it. + +Voice — this repository has one writer with an established voice: + +- Direct, specific, unhedged. Read a few specs and `README.md` to absorb it. +- Banned words: "simply", "seamlessly", "powerful", "robust", "easily", "just", + "of course", "delve", "leverage". +- Much of the audience reads English as a second language. Prefer active voice + and avoid idiom; Vale flags both. +- American spelling. Vale's dictionary is American English. +- No summary paragraph restating the article at the end. Stop when you are + done. + +Content rules: + +- **Show something before you explain it.** An article about a conversion + shows the input and the output; an article about a failure mode shows the + failure. Use real bytes from a committed file — `tests/corpus/` and the + fixtures under `tests/` exist for this — never an invented illustration. + Three short blocks, not three screens. +- **Do not open in notation.** Metric codes, phase numbers and internal + shorthand (`M1`, `R3`, `P03 phase 5`) are how the corpus talks to itself. A + reader has not learned them and should not have to. If a number matters, say + what it measures in words. One piece of shorthand, introduced once, is the + most an article should ask for. +- **Lead with what is true now.** Superseded measurements belong in the + reversal that explains them, in a sentence, not in a table the article then + admits is historical. +- Never assert a fact a generator could state. An article does not enumerate + flags or list command output; it explains why the design is the way it is. +- Do not invent numbers. If you quote a benchmark or a fidelity percentage, it + comes from a note that recorded it, and you cite that note. +- If you are not sure, say what is uncertain rather than smoothing it over. + +## Step 4 — Self-check before reporting + +First, the question the other checks cannot ask: **would somebody who does not +already know this understand it?** Read the draft as an operator who runs +`mdd` and has never opened a spec. Where does it assume the corpus's +vocabulary? Where does it assert something it could have shown? An article +that is accurate, well cited, and impossible to follow has failed — this has +happened, and the author of the system being described was the one who could +not follow it. + +Then read it against the three constraints, in this order: + +1. **Reversals.** Which decisions in this article had a losing option? Is each + one named? If the article reads as a straight line from problem to solution, + go back to the corpus. +2. **Citations.** Take three claims at random. Open the cited file. Does the + cited paragraph actually say that? Fix or drop anything that does not + survive. +3. **Overwrite.** `git status --short` shows changes only under + `site/src/content/docs/_drafts/`. Nothing under `docs/articles/`. + +Then run the prose gate, which covers `_drafts/`: + +```bash +vale site/src/content/docs/_drafts/.md +``` + +Vale runs at warning level, so its findings are advice, not a gate. Read them +and act where they are right. + +## Step 5 — Report, do not commit + +Report to the user: + +- The draft path. +- Which specs and notes you cited, and which you read but did not cite. +- **The reversals you preserved**, listed explicitly. This is the part a + reviewer checks first. +- Anything you could not verify, and any place the code and the specs disagree. + +Do not commit and do not promote. Promotion is the human's `git mv`, and it is +the point at which somebody has read the thing. + +## Where this skill does NOT apply + +- Operator documentation — that is `docs/guide/`, written from the code and the + commands rather than synthesized from the design record. +- Writing or extending a spec — use `/spec-extension`. +- Summarizing a spec for a reviewer in chat. This skill produces a published + page; a summary is just a summary. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..72596ad --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,74 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +# one Pages deployment at a time; don't cancel a production deploy mid-flight +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # Starlight's `lastUpdated` derives each page's date from git + # history; a shallow clone would show the deploy date on every page. + fetch-depth: 0 + + - name: Set up mise + uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3 + with: + install: true + cache: true + + - name: Cache bun install cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('site/bun.lock') }} + restore-keys: ${{ runner.os }}-bun- + + - name: Install site dependencies + run: mise run docs-install + + # `docs-build` depends on `docs-sync`, which shells out to + # `uv run python scripts/sync-docs.py` - the Python env needs to exist + # too, not just the Node one. + - name: Install Python dependencies + run: mise run install + + - name: Build + run: mise run docs-build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 + with: + path: site/dist + + deploy: + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + steps: + - name: Setup GitHub Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..2cd026d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,56 @@ +name: Docs + +# A site build must fail when the change is proposed, not when it is +# deployed (S06 requirement 9). Path-filtered so a Python-only pull request +# doesn't pay for a Node toolchain; `mise run ci` covers the code-coupled +# documentation checks (the `mdd …` string checker) instead. +on: + pull_request: + branches: [main] + paths: + - 'docs/**' + - 'site/**' + - '.vale.ini' + - 'scripts/sync-docs.py' + - '.github/workflows/docs.yml' + +permissions: + contents: read + +jobs: + docs-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up mise + uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3 + with: + install: true + cache: true + + - name: Cache bun install cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('site/bun.lock') }} + restore-keys: ${{ runner.os }}-bun- + + # `docs-astro-check` and `docs-build` both depend on `docs-sync`, which + # shells out to `uv run python scripts/sync-docs.py`. + - name: Install Python dependencies + run: mise run install + + - name: Install site dependencies + run: mise run docs-install + + # The synced `write-good` style is gitignored (S06's Vale implementation + # note); fetch it before linting. + - name: Fetch Vale styles + run: mise run docs-vale-sync + + - name: Prose lint, astro check, site build + run: mise run docs-check diff --git a/.gitignore b/.gitignore index 4df3a72..147dbf5 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,24 @@ src/mdd/_version.py # Legacy sibling clone — corpus is now vendored under tests/corpus/. # Listed so a still-existing local checkout doesn't show up as untracked. /test-confluence/ + +# Documentation site (site/, Astro + Starlight) +site/node_modules/ +site/dist/ +site/.astro/ +# Synced from docs/ by scripts/sync-docs.py — build input, not source. +site/src/content/docs/guide/ +site/src/content/docs/articles/ +site/src/content/docs/design-record/ +site/src/content/docs/get-involved/ +site/src/content/docs/reference/ +site/src/content/docs/spec/ +site/src/content/docs/research/ +# Vale styles fetched by `vale sync` from pinned release URLs. +.vale/styles/write-good/ +# Raw Markdown twins (site/public/.md), also written by sync-docs.py. +site/public/guide/ +site/public/articles/ +site/public/reference/ +site/public/design-record/ +site/public/get-involved/ diff --git a/.mise.toml b/.mise.toml index 4872ad5..8c23bd0 100644 --- a/.mise.toml +++ b/.mise.toml @@ -7,6 +7,10 @@ [tools] python = "3.14" uv = "latest" +# Documentation site only. `mise run ci` never touches these, so a contributor +# working on Python pays nothing for them beyond `mise install`. +bun = "1.3" +vale = "3" [env] _.file = { path = ".env", optional = true } @@ -61,6 +65,14 @@ description = "pip-audit (OSV) over the dependency closure" # logs are not a terminal, so the spinner goes too. run = "uv run pip-audit --progress-spinner off" +[tasks.check-mdd-commands] +description = "Verify every `mdd …` string in user-facing prose is a real command" +run = "uv run python scripts/check-mdd-commands.py" + +[tasks.check-mdd-commands-all] +description = "Same check, extended over the design record. Advisory: never fails." +run = "uv run python scripts/check-mdd-commands.py --all" + [tasks.ci] description = "Full CI gate" depends = [ @@ -72,6 +84,7 @@ depends = [ "test", "ir-roundtrip", "ir-coverage", + "check-mdd-commands", ] [tasks.version] @@ -125,3 +138,90 @@ run = "uv run python -m pytest tests/ir/test_benchmarks.py --no-cov -v" [tasks.ir-coverage] description = "Corpus coverage matrix gate. Writes build/ir-coverage.json." run = "uv run python -m pytest tests/ir/test_corpus_coverage.py --no-cov -q" + +# --- Documentation site (site/, Astro + Starlight) --------------------------- +# +# None of these are part of `mise run ci`: that gate stays Node-free so a +# contributor changing Python is never asked to install bun. The code-coupled +# documentation checks (`check-mdd-commands` above) are pure Python and do run +# in `ci`, because a docs-only workflow would not fire on a Python change. + +[tasks.docs-install] +description = "Install the site dependencies (bun)" +dir = "site" +run = "bun install" + +[tasks.docs-sync] +description = "Copy docs/**.md into site/src/content/docs/ with Starlight frontmatter" +run = "uv run python scripts/sync-docs.py" + +[tasks.docs-dev] +description = "Start the live-reloading dev server" +depends = ["docs-sync"] +dir = "site" +run = "bun run dev" + +[tasks.docs-build] +description = "Build the static site into site/dist" +depends = ["docs-astro-check"] +dir = "site" +# Remove the previous build first. Astro leaves orphaned output behind when a +# page's slug changes, and the link check reads whatever is in dist/ — a stale +# page reports broken links that no longer exist in the source. +run = ["rm -rf dist", "bun run build"] + +[tasks.docs-preview] +description = "Preview the production build" +dir = "site" +run = "bun run preview" + +[tasks.docs-astro-check] +description = "Astro type/content check" +depends = ["docs-sync"] +dir = "site" +run = "bunx astro check" + +# `check` and `build` both write under site/.astro/, so they must not run +# concurrently. mise runs `depends` in parallel, so the order is forced by +# chaining rather than by listing both under docs-check. + +[tasks.docs-vale-sync] +description = "Fetch the pinned Vale style packages (needs the network)" +run = "vale sync" + +[tasks.docs-vale] +description = "Vale prose lint over hand-written prose. Errors gate; warnings advise." +# Hand-written prose only. Not the design record, which is written for a +# different reader in a different register — and not the whole of +# site/src/content/docs/, because once docs-sync has run that directory +# contains the synced specs and research notes too. +# +# Two passes. Vale's exit code covers everything at or above MinAlertLevel, +# with no way to print a class of finding without failing on it, so the first +# pass shows every warning and the second decides the outcome. Only errors — +# misspellings and wrong-cased terms — block. Passive voice is a judgement +# call a reviewer makes, not something that should stop a build. +run = [ + "vale --no-exit docs/guide docs/articles site/src/content/docs/index.mdx site/src/content/docs/_drafts", + "vale --minAlertLevel=error docs/guide docs/articles site/src/content/docs/index.mdx site/src/content/docs/_drafts", +] + +[tasks.docs-links] +description = "Check every internal link in the built site resolves to a served page" +# The sync step checks that a link's target file exists in the repository. +# This checks that the URL it emitted is one the site actually serves — a +# different question, and the only one a reader experiences. +depends = ["docs-build"] +run = "uv run python scripts/check-site-links.py" + +[tasks.docs-check] +description = "Full documentation gate — prose lint, astro check, site build, link check" +depends = ["docs-vale", "docs-links"] + +[tasks.docs-clean] +description = "Remove site build artefacts and synced content" +run = "rm -rf site/dist site/.astro site/src/content/docs/{guide,articles,reference,spec,research}" + +[tasks.derive-dark-logo] +description = "Regenerate site/src/assets/mdd-logo-dark.png from assets/mdd-logo.png" +run = "uv run python scripts/derive-dark-logo.py" diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000..b2e10ff --- /dev/null +++ b/.vale.ini @@ -0,0 +1,21 @@ +StylesPath = .vale/styles +MinAlertLevel = warning +Vocab = mdd + +# Pinned release, not `@latest` - see S06's Vale implementation note. +Packages = https://github.com/errata-ai/write-good/releases/download/v0.4.1/write-good.zip + +# `filter_mode = added` (only lint new content) does not exist as a +# `.vale.ini` key in Vale 3, and `vale --help`'s `--filter` flag filters +# *which rules* run, not *which lines* they run over - there is no built-in +# "only new content" mode to configure here. Achieving that would mean +# diffing against `main` outside Vale (e.g. in the calling `mise` task or +# workflow) and passing only changed files/line ranges to `vale`. Left +# unimplemented for now; `MinAlertLevel = warning` is the whole gate. +[*.md] +BasedOnStyles = Vale + +# Named individually rather than adding the whole `write-good` style, which +# enables it without pulling in the package's other seven rules. +write-good.Passive = YES +write-good.Cliches = YES diff --git a/.vale/styles/config/vocabularies/mdd/accept.txt b/.vale/styles/config/vocabularies/mdd/accept.txt new file mode 100644 index 0000000..588b2d1 --- /dev/null +++ b/.vale/styles/config/vocabularies/mdd/accept.txt @@ -0,0 +1,44 @@ +Confluence +SharePoint +Atlassian +Atlassian's +OneDrive +Quarto +Docling +Lua +Starlight +Astro +Pagefind +Vale +argparse +basedpyright +ripgrep +librsvg +pathspec +complexipy +Astral's +mdd +Markdown +PPTX +DOCX +[Ff]rontmatter +[Rr]oundtrips? +mddignore +namespaced +passthrough +allowlist +subcommands? +subtrees? +unarchive +basename +callout +config +lossy +unmerged +reimplemented +Rasterizing +[Mm]irror(ed|ing)? +auditable +reframed +else's +[Pp]andoc diff --git a/AGENTS.md b/AGENTS.md index 4b3dafe..e771d73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ The installed `mdd` may be a wrapped or aliased command, so instead use `uv run `.claude/skills/` holds repeatable workflows for the tooling this repo already ships. Invoke one with a `/` prefix when the task matches: +* `/article-synthesis` — draft a `docs/articles/` page from the spec and research corpus, with citation density and preserved reversals enforced * `/complexipy-refactor` — refactor a function off the `complexipy-snapshot.json` watermark, with the score as a check rather than a target * `/refactor-module` — scoped single-module pass: run the gates over one path, judge which findings are worth acting on, apply one transformation at a time * `/spec-extension` — draft or extend a `docs/spec/SNN-*.md` diff --git a/README.md b/README.md index de0fdd8..c4d1a63 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Bidirectional sync between Markdown-in-git and Confluence/SharePoint. remote document management systems. Engineers and AI agents can work with Markdown while everyone else can continue working with the tools they know. +📖 **Documentation: ** + ## ⚠️ Beta software - use with care `mdd` is written almost entirely by AI agents under human review. There @@ -97,13 +99,12 @@ example. Especially handy for deep-linking Confluence and Markdown. ## Documentation -Design docs live in [docs/spec/](docs/spec/); start from -[docs/spec/000-specs.md](docs/spec/000-specs.md). +The documentation site is at . It has +the install guide, an offline quickstart, what can destroy data, and the +bring-your-own-tenant how-tos for Confluence and SharePoint. -The research notes behind those designs — surveys, spikes, and -measurements — live in [docs/research/](docs/research/); start from -[docs/research/000-research.md](docs/research/000-research.md). They -are working documents, not maintained design records. +All of it is Markdown in this repository, so it reads on GitHub too: +[docs/README.md](docs/README.md) is the map. ## Contributing diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b98bac5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,69 @@ +# Documentation + +Everything here is Markdown checked into the repository. It renders on GitHub, +and it is also published as a site at . + +The site is the better way to read it — it has search, cross-links that +resolve, and the design record tucked out of the way. This file is the map for +anyone reading the repository directly. + +## What is where + +| Directory | What it holds | Written for | +|---|---|---| +| [guide/](guide/) | Install, quickstart, concepts, safety, configuration, and the first-sync how-tos | operators | +| [articles/](articles/) | Longer pieces on why the design is the way it is, citing the specs underneath | anyone deciding whether to trust the tool | +| [design-record/](design-record/) | Introduction to the two directories below | contributors | +| [spec/](spec/) | The durable design record, one file per feature — start at [000-specs.md](spec/000-specs.md) | contributors, AI agents | +| [research/](research/) | Surveys, spikes and measurements, recorded when the work happened — start at [000-research.md](research/000-research.md) | contributors, AI agents | + +Specs and research notes describe **intent at the time of writing**. They are +not maintained as the code moves, and a research note may recommend an approach +that was later abandoned. The guide describes what `mdd` does now; a command's +own `--help` is more current still. + +Guide pages carry a numeric filename prefix (`01-install.md`) that orders them +in the sidebar. The prefix is dropped from the published URL. + +## How the site is built + +The Astro + Starlight application lives in [`site/`](../site/), not in this +directory. `mdd` is a Markdown-mirror tool and `docs/` is the tree it would +plausibly be pointed at, so build artefacts are kept out of it. + +`scripts/sync-docs.py` runs before every build. It copies this directory into +the site's content tree, deriving the Starlight frontmatter these files do not +carry from each file's first heading, demoting the design record, and rewriting +repo-relative links so that links to published pages resolve on the site while +links to source files resolve on GitHub. It fails the build on a link that +resolves to nothing. + +The synced copies are gitignored. They are build input, not source — edit the +files here. + +```bash +mise run docs-install # once: install the site dependencies +mise run docs-dev # live-reloading server on http://localhost:4321/mdd/ +mise run docs-check # prose lint, type check, build, link check +``` + +The site deploys to GitHub Pages on every merge to `main`. + +Two things live outside this directory and are published anyway. +`site/src/content/docs/index.mdx` is the hand-authored landing page, since a +hero and a card grid are presentation rather than documentation. And +`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md` and `LICENSE` are read +from the repository root, because GitHub finds those by path and moving them +would break its pull-request and advisory flows. + +## Adding to it + +- A new guide page: add `docs/guide/NN-.md` with a single `# ` heading. + Callouts use GitHub alert syntax (`> [!WARNING]`), which renders here and + maps to a Starlight aside on the site. +- A new spec or research note: see [spec/000-specs.md](spec/000-specs.md) and + [research/000-research.md](research/000-research.md); both have a scaffold + task in `mise tasks`. +- A new article: drafted by the `article-synthesis` skill into + `site/src/content/docs/_drafts/`, and promoted into `articles/` by hand once + a person has read it. diff --git a/docs/articles/.gitkeep b/docs/articles/.gitkeep new file mode 100644 index 0000000..7a679ff --- /dev/null +++ b/docs/articles/.gitkeep @@ -0,0 +1,2 @@ +# Promoted articles land here. +# Drafts live in site/src/content/docs/_drafts/ until a human promotes them. diff --git a/docs/articles/near-lossless.md b/docs/articles/near-lossless.md new file mode 100644 index 0000000..fc6a826 --- /dev/null +++ b/docs/articles/near-lossless.md @@ -0,0 +1,204 @@ +# What "near-lossless" actually means + +The README promises "near-lossless roundtrips" between Markdown in git and +Confluence. Here is what that looks like on a real page. The fragment below is +Confluence's storage XHTML for a macro that `mdd` does not recognize, taken +from a captured test page committed at +`tests/corpus/confluence/_snapshots/1147246/storage.xhtml`: + +```xml +onetwo +

The body is rich text inside an unrecognised macro. The + reader does not introspect the children; the writer + re-emits them byte-for-byte from the captured Origin.

+
+``` + +When `mdd` pulls that page, this is the Markdown it writes: + +```markdown +:::confluence-macro {alpha="one" beta="two" name="bogus-macro"} +The body is rich text inside an unrecognised macro. The + reader does not introspect the children; the writer + re-emits them byte-for-byte from the captured Origin. + +::: +``` + +(The page text describes itself; it is a test fixture.) Notice what +the Markdown does not contain: the `ac:macro-id` GUID and +`ac:schema-version="1"` are gone, because Markdown has no place to put them. +And yet, when you push the file back unedited, the storage `mdd` reconstructs +is the first block again, byte for byte, GUID included — the diff comes out +empty and no update is sent. A CI gate asserts exactly that round trip, +storage to Markdown and back, for every one of the 82 captured pages in the +corpus ([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). + +That example carries the whole promise: **unmodified content round-trips +without loss; edited content and a documented list of constructs do not, and +the "near" is that list.** The rest of this article walks the list. For the +story of why the machinery underneath exists at all, read +[why mdd has its own IR](why-mdd-has-its-own-ir.md). + +## Why "lossless" is not on the table + +Confluence stores a page as namespaced XHTML in which macros, layout cells, +and links carry identity attributes — `ac:local-id`, `ac:macro-id`, +`ac:schema-version` — that Confluence itself needs back +([S28](../spec/S28-document-ir-foundation.md)). Markdown has nowhere to hold +them, and `mdd` refuses to invent a place — no `data-mdd-*` attributes, no +HTML comment trails — because the Markdown file is what humans read and edit +([S30](../spec/S30-markdown-ir-conversion.md)). Every converter faces this +fork: pollute the Markdown with metadata, or carry the metadata somewhere +else. `mdd` carries it somewhere else, and "near-lossless" is the measured +result of that choice. + +## What survives, and by what mechanism + +Three mechanisms produced the example above. + +**Nothing is dropped silently.** A reader that meets a shape it does not +recognize must keep the source rather than discard it; the fallback contract +states this as "MUST NOT silently drop content" +([S28](../spec/S28-document-ir-foundation.md)). That is why the bogus macro +became a `:::confluence-macro` block instead of vanishing: its parameters and +body ride through opaquely, present in the file and visible in every diff. +Storage constructs with no macro shape at all surface as a fenced code block +tagged `confluence-xml` with the raw XHTML inside. Every fallback also raises +an event, and CI fails when one fires on a fixture not explicitly tagged as a +fallback case ([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)) — +falling back is allowed; falling back unexpectedly is a build failure. + +**Identity is grafted back.** Since the Markdown leg drops identity +attributes, the push path re-fetches the remote storage, parses it, and +reattaches the cached tree's identity attributes and reader-only fields onto +the tree parsed from your Markdown — "fresh wins, cached fills" +([S28](../spec/S28-document-ir-foundation.md)). That is where the GUID in the +example comes from. This is production behavior, not a test-harness trick: +`mdd confluence update-page` reuses the storage it already fetched for the +diff display, and an integration test asserts that pushing an unedited export +issues no PUT at all +([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). An earlier design +kept the cached tree in a `.confluence.json` sidecar next to each file; +the sidecar was retired because page metadata already lives in the Markdown's +frontmatter and the lookup happens in-process from the fetched storage +([S31](../spec/S31-ir-normalization-and-whitespace.md)). + +**Source form is recorded.** In preserving mode every node carries an +`Origin`: the exact source bytes, the surrounding whitespace, and the entity +form of each character ([S31](../spec/S31-ir-normalization-and-whitespace.md)). +A page that spells `…` as `…` gets its `…` back instead of a +normalized Unicode character, and a CDATA section keeps its trailing newline. + +## What is known not to survive + +The carve-outs are written down, and each one is a decision with a rejected +alternative behind it. + +- **Backslash escapes.** Write `\*` in Markdown and the round-tripped file + comes back with a plain `*` — storage holds the literal character, and + restoring the backslash needs a context-sensitive re-escape policy that is + deferred ([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). + Fixtures exercising this are expected failures on the byte-perfect gate. +- **Empty paragraphs carrying an anchor id.** Confluence's editor records the + cursor position as a trailing empty `

` with a `local-id`; after a round + trip, that invisible paragraph is gone. `mdd` considered and rejected + inventing Markdown syntax for it, because an HTML-comment marker or an empty + fence would either be noise in human-edited files or get stripped by other + Markdown tooling — a silent loss anyway + ([S30](../spec/S30-markdown-ir-conversion.md)). Corpus pages that exhibited + the shape were corrected at the source instead of taught to the converter. +- **Inline `` and ``.** Wrap a key name in `` and the tags + disappear, leaving the text behind: an allowlist gap in the Markdown + reader's raw-HTML handling + ([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). +- **Text nodes over 256 KiB.** Recorded source bytes are capped per node; + past the cap the writer falls back to its canonical render, so a huge + embedded blob may come back reformatted + ([S31](../spec/S31-ir-normalization-and-whitespace.md)). The alternative — + unbounded source capture — could turn a 5 MB page into a ~10 MB tree, and + graceful degradation on that long tail won. +- **Merged-cell tables in the Markdown surface.** A table with `colspan` or + `rowspan` shows up in your file as a raw HTML ``, not a pipe table, + because pipe syntax cannot express the merge + ([S30](../spec/S30-markdown-ir-conversion.md)). Content survives; the clean + Markdown surface does not. +- **Origin across structural edits.** Split a paragraph in two and the second + half's recorded source form is meaningless, so the edited page re-renders + canonically — entities normalized, whitespace canonical — rather than + replaying stale bytes + ([S31](../spec/S31-ir-normalization-and-whitespace.md)). + +Separately, some page aspects are outside the model entirely rather than lossy +within it: comments, page restrictions, and page properties are not synced, +and labels are read but not pushed back. The +[concepts page](../guide/03-concepts.md) lists these, and +[Safety](../guide/04-safety.md) covers what they mean for a push. + +## Losing things on purpose + +One class of loss is a feature. The converter runs in two modes: a preserving +mode used internally on the push leg so Confluence sees its own bytes back, +and a default normalizing mode for the files written to disk, which collapses +soft breaks, drops empty paragraphs, converts `…` to `…`, and trims +redundant attributes ([S31](../spec/S31-ir-normalization-and-whitespace.md)). +The goals conflict — a byte-faithful export is full of entity clutter and +trailing blank paragraphs no human wants to read — and the spec's answer is +that a single mode cannot serve both. When someone says the Markdown on disk +is not byte-identical to what a raw export would give: correct, and +intentional. + +## Attachments are a different promise + +None of the above applies to attachments. On pull, `mdd` downloads the binary +and, for Office formats and PDF, writes a converted `.md` sibling next to it +([S16](../spec/S16-confluence-attachment-conversion.md)). The binary is the +fidelity copy; the sibling is a convenience for grep and diff in the mirror, +and page links keep pointing at the binary. The research behind the Office +story is blunt about the direction of truth: renders between Markdown and +`.docx` are one-way authoritative, and the design never claims a byte-stable +round trip for them — divergence is detected and surfaced to a human instead +of merged ([R02](../research/R02-attachments.md)). + +## Where the claim came from, and how it is defended + +"Near-lossless" started as a measurement, not a slogan. In May 2026 four +candidate pipelines ran against a 35-fixture corpus; the comparison note first +recommended pandoc with a Lua writer, then reversed itself when a pure-Python +spike beat pandoc on every fidelity metric +([R12](../research/R12-confluence-ir-comparison.md)). The sibling article +[why mdd has its own IR](why-mdd-has-its-own-ir.md) tells that story. +The fidelity percentages from those spikes are history now, superseded by the +binary gate the example at the top demonstrates. + +What holds today: the corpus has grown to 82 captured Confluence pages plus +Markdown-first fixtures for every CommonMark 0.31 construct, the GFM +additions, and each mdd extension +([S32](../spec/S32-ir-test-corpus-expansion.md)). The byte-perfect gate for +preserving mode and similarity floors for normalizing mode run on every +commit, and locally: `mise run ir-roundtrip` runs the round-trip suites, +`mise run ir-coverage` writes the coverage matrix to `build/ir-coverage.json`, +and both run inside `mise run ci`. + +The discipline around the gates has two properties worth knowing. Known +failures are quarantined, not tolerated: a page that stops round-tripping gets +a reproducer snapshot in a strict expected-failure tier +(`tests/corpus/confluence/_xfail_snapshots/`), so the moment a fix lands the +test unexpectedly passes, CI fails, and the snapshot must move into the main +byte-perfect gate. That tier is empty at the time of writing. And thresholds +only ratchet one way: if a benchmark gate flakes, the documented response is +to raise the ceiling, never to disable the gate +([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). One gate was +dropped rather than ratcheted — a hard limit keeping the JSON form of the +parsed tree within 10× of the source size could not be made meaningful on +small fixtures, where structural overhead produces ~30× ratios on 200-byte +pages, so it was demoted to a design goal reviewers eyeball during corpus +growth. + +The conversion pipeline did not become the default on green tests alone: +promotion required the gates green for two consecutive weeks, a whole-space +sync round-tripping clean against a live tenant, and a two-week A/B against +the legacy converters with zero unexpected diffs +([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). That is the +standard "near-lossless" is held to, and the reason the word "near" comes with +a list instead of a shrug. diff --git a/docs/articles/publishing-without-clobbering.md b/docs/articles/publishing-without-clobbering.md new file mode 100644 index 0000000..dc92e88 --- /dev/null +++ b/docs/articles/publishing-without-clobbering.md @@ -0,0 +1,223 @@ +# Publishing to Confluence without clobbering it + +Say your team's space is called DOCS. Most pages are yours, but a +Sphinx pipeline republishes one subtree every night, the platform team +edits a few pages by hand, and somebody renamed a page in the Confluence +UI last week. Now you point `mdd confluence sync-space` at it, so the +space and a git repository mirror each other. + +Every push in that setup replaces the entire live page body with a +render of your local Markdown, with no merge step in between +([Safety](../guide/04-safety.md)). This article covers the mechanisms +that keep that from destroying anything: what each one protects, what +was rejected on the way, and where the gaps are. For +procedures, see [Safety](../guide/04-safety.md) and +[Your first Confluence sync](../guide/07-confluence-first-sync.md). + +## The pages that are not yours + +Push a body edit to a page in the Sphinx subtree and the update refuses +before any write: + +``` +This page is managed by 'sphinx-pipeline'. Edit at the source; do not +update via mdd. +``` + +The wording is the default refusal in `src/mdd/confluence/update.py`; +the publisher name comes from your shared config, which can also supply +a custom message. Pushing to a page owned by other automation is worse +than a normal clobber: the upstream system republishes on its next run, +your edit vanishes, and the two tools oscillate forever +([R05](../research/R05-managed-elsewhere.md)). Detection is a cascade of +checks — managed space, managed subtree, publisher account id, body +marker, page restrictions — and a hit blocks every write while pull +keeps working ([S26](../spec/S26-managed-elsewhere.md)). The exported +file gets a header naming the publisher, built in +`src/mdd/confluence/managed/headers.py`: "Edit there; this mirror is +read-only." + +The research proposed a `--force-managed` flag and a per-page +frontmatter override for migration cases +([R05](../research/R05-managed-elsewhere.md)). The spec killed both: a +detected managed page cannot be pushed via `mdd`, full stop, and a wrong +detection is fixed by editing the shared fingerprint config, which +leaves a reviewable diff in git +([S26](../spec/S26-managed-elsewhere.md)). The reasoning: the only +stable way to coexist with upstream automation is to never write to its +pages, and an override flag is a standing invitation to do exactly that +under deadline pressure. + +The cascade also inverted between research and spec. The research put +the page-restrictions check first; the spec runs it last, after the +cheap config-driven checks, and gives it a separate +`READ_ONLY` reason because "you lack permission" means something +different from "another system owns this" +([S26](../spec/S26-managed-elsewhere.md)). The shipped code adds a +wrinkle the spec does not mention: when the restrictions API call fails, +`classify_page` in `src/mdd/confluence/managed/classify.py` fails +*open* and assumes you may write, on the stated grounds that the other +cascade layers are stronger and Confluence will reject an actually +forbidden write server-side. A reasonable trade, but it makes the last +cascade layer advisory, not a guarantee. + +## Nothing tells you what changed + +The renamed page is a different problem: Confluence Cloud has no change +feed. No endpoint reports renames, moves, archives, or deletes as +events since a timestamp, and per-page version history only covers body +edits +([S14](../spec/S14-confluence-sync.md)). The research that shaped sync +tried the obvious shortcut, a CQL `lastmodified` query as an incremental +filter, and found it unreliable for exactly the events that restructure +a space: moves and archives +([R01](../research/R01-confluence-renames.md)). A sync that trusted it +would re-export a renamed page to a new file and leave the old one, +silently forking the mirror. + +What killed that approach also dictated the replacement. The one field +Confluence keeps stable across rename, move, archive, and unarchive is +the page `id`; every other field is mutable +([R01](../research/R01-confluence-renames.md)). So sync does a full +reconciliation on every run: fetch the whole page tree, diff it against +the local mirror keyed by the `page_id` in each file's frontmatter, and +classify every difference +([S14](../spec/S14-confluence-sync.md)). An early option kept a side-car +state file mapping page ids to paths instead; it lost to frontmatter +because a second source of truth drifts the moment anyone edits a file +outside `mdd`. + +Two smaller forks from the same note settled the destructive edges +([R01](../research/R01-confluence-renames.md)). Archiving could have +been modeled as relocation to an `_archived/` subtree or as deletion; +both lost to an in-place frontmatter status flip, because relocation +churns paths on every archive cycle and deletion loses content the user +wanted kept. And a cross-space move could have been handed +off to the other space's mirror; that was rejected as inter-repo +coordination nobody wanted, in favor of deleting locally and warning. + +## Structural changes flow one way + +The diff model only detects Confluence-side structure changes, which +surprises people. Rename a file locally with `git mv` +and the Confluence page is not renamed; the spec's own assessment is +that the next sync "at best produces a confusing diff and at worst a +duplicate page" +([S27](../spec/S27-confluence-page-rename-move-archive.md)). Setting +`confluence.status: ARCHIVED` in frontmatter does not archive the page +either — the next sync sees the mismatch and reverts your edit. + +The considered fix, inferring intent from the working tree so a +`git mv` becomes a Confluence rename, was explicitly rejected: guessing +at structural intent from file state is how a mirror ends up renaming +pages nobody meant to rename. Instead, imperative commands +(`rename-page`, `move-page`, `archive-page`, `unarchive-page`) make the +Confluence API call first and refresh the local file from the response, +after the same guards as a body push: managed-page classification, a +dirty-tree refusal, and a version-drift check. There is deliberately no +`delete-page`: archiving is the safe substitute, and the Confluence +client issues no HTTP DELETE at all +([S27](../spec/S27-confluence-page-rename-move-archive.md)). + +## When both sides changed + +The version-drift check is the guard you will meet most often. Your +frontmatter says the page was at version 5 when you last pulled; a +colleague has since saved twice; your push stops with: + +``` +Conflict: remote version 7 is newer than local version 5. +Re-export the page to get the latest version, reconcile manually, +then re-run update. +``` + +That wording lives in `src/mdd/confluence/version.py` and is shared by +the body push and the structural commands. When sync finds a page where +the remote advanced *and* the local body also changed, it skips the page +in both directions — no push that would erase someone else's edit, no +pull that would erase yours — and records a conflict for the operator +([S14](../spec/S14-confluence-sync.md)). + +The prior art is instructive because it is bad: Quarto's Confluence +publisher, the closest existing system, is last-writer-wins — it fetches +the version number, increments it, and replaces the body wholesale, and +its documentation states plainly that Confluence-side edits are +overwritten on publish +([R06](../research/R06-confluence-bidirectional-sync-ir.md)). `mdd` +refuses to be that. But the skip is a punt, not a merge. The research +note that opened the merge problem calls it "fine +for an MVP, useless as a working model," because a space that humans +actively edit produces conflicts faster than an operator resolves them +([R06](../research/R06-confluence-bidirectional-sync-ir.md)). That note +launched a measured comparison of document-model foundations: four +pipelines built and scored against the same corpus, with the original +Pandoc recommendation reversed in favor of a pure-Python one +([R12](../research/R12-confluence-ir-comparison.md)). That foundation +shipped ([S28](../spec/S28-document-ir-foundation.md)); the merge engine +built on top of it has not. What ships today is still skip-and-report, +and resolving a conflict is still manual work. + +## The crude guards + +Two last-resort checks watch the body itself. Push a file whose content +somehow collapsed — a bad merge, a truncated write — and you get: + +``` +local body (403 chars) is less than 10 % of the remote body +(61240 chars). This looks like accidental content loss. Re-export +and reconcile, or pass --allow-shrink to override. +``` + +Its companion refuses an empty body outright ("Refusing to wipe the +remote page") unless you pass `--allow-empty`; both messages are in +`src/mdd/confluence/update.py`. The guards are honest about being crude: +both are length checks, and neither notices a push that deletes half a +page ([Safety](../guide/04-safety.md)). + +Around the body sit narrower rules. Office attachment publishing is a +separate write path with its own managed-page refusal and one ownership +rule: a user-uploaded attachment already holding the target filename is +a hard error, while an attachment `mdd` published itself is versioned +over on every re-render +([S17](../spec/S17-confluence-office-publishing.md)). Comments, page +restrictions, and page properties are not modeled at all: a push does +not destroy them, but the Markdown file that looks like the whole page +is not the whole page ([Safety](../guide/04-safety.md)). On the pull +side, `.mddignore` filters content before download with gitignore +semantics; adding a pattern never deletes already-synced files, cleanup +is a separate opt-in `--prune-ignored` flag, and combining it with +`--read-only` is rejected as contradictory +([S39](../spec/S39-mddignore.md)). + +## The gate the spec describes and the code does not have + +One mechanism sits on the far side of the pipeline: a committed +confidentiality blacklist listing spaces and sites whose content must +never be pushed to a git remote, with `--force` explicitly not an +override ([S07](../spec/S07-data-protection.md)). Here the design record +and the shipped code part ways, and the code is what counts. The spec +says every path that can publish to a remote consults the blacklist +first; the sync spec says the mirror push applies it +([S14](../spec/S14-confluence-sync.md)). + +> [!WARNING] +> In this distribution, no Confluence code path calls the blacklist +> check, and the built-in generic git backend states in its +> own source, `src/mdd/mirror/git.py`, that it applies "no host +> allow-list and no data-protection gate". The SharePoint half is +> enforced; the Confluence half is a helper that only `mdd search` +> reads. + +The backend protocol reserves a place for the gate, and a deployment +that supplies its own mirror backend can enforce it there — but if you +run the open-source core against a sensitive space, the spec describes a +protection you do not have ([Safety](../guide/04-safety.md)). + +And all of it — the diff model, the cascade, the guards — is beta +software written almost entirely by AI agents under human review, with +no independent security review ([SECURITY.md](../../SECURITY.md)). The +protections described here exist and are tested, but tested is not +audited. The design compensates the honest way: keep the mirror in git +so every sync is one commit you can revert, lean on Confluence's own +version history for the remote side, and prefer refusing to guessing +everywhere the two conflict. diff --git a/docs/articles/spec-driven-development-with-agents.md b/docs/articles/spec-driven-development-with-agents.md new file mode 100644 index 0000000..7881e9a --- /dev/null +++ b/docs/articles/spec-driven-development-with-agents.md @@ -0,0 +1,224 @@ +# Spec-driven development with AI agents + +Almost every design document in this repository ends with a list of resolved +questions. This one comes from +[the documentation-site spec](../spec/S06-documentation-site.md): + +> 3. ~~Do generated reference pages get committed and drift-gated, or generated +> at build time and never stored?~~ **Resolved.** Committed to +> `docs/reference/`, drift-gated in `mise run ci`. See "Generated reference +> pages are committed". + +The struck-through text is the mechanism. Each question marks a fork where +two designs were possible; the resolution names the winner; and the losing +option stays on the page, crossed out but readable. Multiply that by +thirty-seven specs and fifteen research notes and you have the design record +published on this site. It is not documentation of `mdd`'s code — it is how the code got +written, largely by AI agents under human review +([CONTRIBUTING.md](../../CONTRIBUTING.md)). This article describes the +method as one project practices it: the gates, the rules added after +something went wrong, and what it costs. + +## Spec before code + +The rule is old by this project's standards: new features get a structured +spec before implementation, focused on design rationale rather than +implementation detail ([S01](../spec/S01-spec-based-development.md)). Each +spec states a purpose, the requirements, the approach chosen, and the options +rejected along the way. A status line tracks it from draft to implemented or +superseded, and [the spec index](../spec/000-specs.md) holds the conventions. + +The specs are themselves gated. A build script fails CI on a broken link +between specs, a missing status line, or an `Implemented` status without a +plain date ([the spec index](../spec/000-specs.md)). The design record gets +the same treatment as the code because the agents that write the code read +the specs first, and a rotten record produces rotten code. + +Working documents live somewhere else. Research notes — surveys, spikes, +measurements — go in `docs/research/`, and they are deliberately not kept up +to date as the code moves on +([the research index](../research/000-research.md)). +Specs must not link out to them; anything a spec depends on gets copied in, so +the durable record reads on its own. The split lets a note be honestly wrong +later without anything needing repair. The best example: the note comparing +Confluence conversion approaches recommended building on Pandoc with a custom +Lua writer ([R12](../research/R12-confluence-ir-comparison.md)). A fourth +spike overturned that within days — a first-party typed document model beat +Pandoc on every fidelity measure at roughly 94 times less wall-clock +([R13](../research/R13-confluence-ir-spike-pure-python.md)). The note keeps +the overturned recommendation intact under a "Revision" heading, because the +losing analysis is what makes the final decision auditable. + +## The gates: a harness agents do not have + +One command, `mise run ci`, is the whole quality gate, and the CI pipeline +runs the same tasks a contributor runs locally +([CONTRIBUTING.md](../../CONTRIBUTING.md)). From `.mise.toml`: + +```toml +[tasks.ci] +description = "Full CI gate" +depends = [ + "verify-clean", + "lint", + "typecheck", + "complexipy", + "audit", + "test", + "ir-roundtrip", + "ir-coverage", + "check-mdd-commands", +] +``` + +In words: a git-conflict-marker check, ruff lint and format, basedpyright in +strict mode, a cognitive-complexity gate, a dependency audit, the test suite +with coverage, round-trip suites for the document model, and a check that +every `mdd …` command string in user-facing prose names a real command. + +The spec that added the structural rules cites published measurements of +repositories where agents work with little supervision — cognitive complexity +up roughly 39% in a few months — and draws the conclusion: "Agents have none +of the implicit human harness — no aesthetic disgust at a 300-line function, +no organisational memory — so the gate has to be made explicit and +machine-checkable" ([S34](../spec/S34-code-quality-gates.md)). + +Two decisions in that spec had a losing option worth naming. + +**Grandfathering beat the big-bang cleanup.** Enabling the new rules exposed +921 existing violations; fixing them all before any other work could land +would have blocked the project for days. Instead, every existing violation got +a targeted marker on the offending line — never a config-level ignore, never a +bare `# noqa`. This one is still in `src/mdd/ir/serialize.py`, suppressing the +too-many-return-statements rule: + +```python +def to_dict(node: object) -> JsonValue: # noqa: PLR0911 +``` + +The debt is visible at the function definition, and the gate is fully active +for new code from day one. A grandfathered function may not grow: adding +statements to it means refactoring it enough to drop the marker. + +**Rejections are sticky, and one was reversed.** The spec keeps a table of +rules considered and not enabled, each with a reason, under the instruction +"do not re-add without reopening this spec." One entry has since been struck +through: the security-lint rules were rejected as noisy — "security review is +a separate concern" — then adopted a month later by amendment, with a curated +ignore list, once the packaging work reframed source-level security smells as +a standing concern for a tool that shells out constantly +([S34](../spec/S34-code-quality-gates.md)). The wrong call and the correction +sit next to each other in the table, in that order. + +## The watermark: stopping an agent from making things worse + +The cognitive-complexity gate is the most interesting one, because it answers +a question every agent-heavy project hits: how do you hold new code to a +standard the existing code does not meet, without freezing all work until +someone clears the backlog? + +`complexipy`, the scanner, has no per-function suppression syntax, so the +marker convention could not carry over; patching one in would have meant +maintaining a fork, which the spec rejects +([S37](../spec/S37-complexipy-cognitive-complexity.md)). Instead the +repository commits a snapshot file, `complexipy-snapshot.json`, recording each +over-threshold function's current score. One entry: + +```json + { + "path": "src/mdd/ai/rewrite.py", + "file_name": "rewrite.py", + "functions": [ + { + "name": "rewrite_file", + "complexity": 17 + } + ] + }, +``` + +The recorded score is a watermark. This function passes CI at complexity 17 or +below; if a change pushes it to 18, the gate fails. New functions are held to +a threshold of 15 — a measured choice, not a default accepted blindly. Reusing +10, the threshold of the complexity gate already in place, would have produced +135 violations against 75 at 15, gating too aggressively for the same shape of +function ([S37](../spec/S37-complexipy-cognitive-complexity.md)). + +The spec states the trade-off plainly: a watermark is less visible in a diff +than an inline marker, but more accurate, because the exact baseline is stored +rather than "this function is grandfathered at some level." And the +file only moves in one direction by policy — regenerating the snapshot "MUST +NOT be run as a routine 'make CI green' reflex"; it exists for deliberate +refactor pull requests that lower watermarks. The mechanism visibly works: the +snapshot listed every function above threshold at adoption, and at the time of +writing it is down to thirteen files. A companion spec keeps file *length* a +heuristic rather than a gate — a target of 300 lines, a soft ceiling of 500, +and an explicit decision **not** to add a file-length lint rule, because +per-function metrics are the machine-checked surface +([S36](../spec/S36-module-structure.md)). + +## Rules written after something went wrong + +A spec about method tends to read as though the method arrived fully formed. +The git history says otherwise. + +The clearest case is the no-cross-references-from-code rule. For most of the +project's life, code cited the specs back: spec numbers in comments, spike +markers in test names, issue numbers in error text. A run of commits in July +2026 stripped every such citation from `src/` and `tests/`, and the rule that +landed with them states why: "The reference is one-directional: specs point at +code, code does not point back" ([AGENTS.md](../../AGENTS.md)). A reader of +the code may not have the spec, and the old issue numbers predate the +open-source cut, so they resolve to nothing — or worse, to an unrelated GitHub +issue that happens to share the number. Removing the citations was only half +the job: the repository still modeled the pattern in places an agent reads +right before editing code, so the rule had to be written where the agents +look. One latent tension remains: the module-structure spec asks oversized +files to carry an `# S36-exception:` comment, which is itself a spec number in +a code comment. Nothing currently carries the marker, so the two rules have +not yet collided. + +The command-line interface is a rebuilt dead end. The original spec described +a home-grown command registry, and over time a bespoke flag-parsing +mini-framework grew alongside it — three coexisting parsing styles, each +module maintaining its own usage strings. A rewrite deleted all of it in favor +of a single argparse tree ([S35](../spec/S35-argparse-cli-parsing.md)). The +original spec survives, marked superseded, as the record of the approach that +did not work ([S02](../spec/S02-mdd-cli-tool.md)). + +Smaller reversals hide inside individual specs. The skills bundle — the +mechanism that ships agent workflow descriptions with `mdd` so agents know +when to reach for the tool — originally computed its skill index at import +time; that turned out to be incompatible with letting a wrapping distribution +register its own skills, so discovery became lazy +([S23](../spec/S23-skills-bundle.md)). The resolved questions at the bottom of +most specs — like the one this article opened with — record smaller forks the +same way. + +## What it costs + +Spec discipline is overhead, and pretending otherwise would make this article +worthless. Every non-trivial feature pays for a design document before any +code, plus the hygiene work of keeping the index, the status lines, and the +cross-references valid. The gates add their own tax: a change that trips a +structural threshold gets refactored rather than suppressed, which is slower +than shipping it. + +The record also drifts, by design, and the project pays to keep that honest +rather than to prevent it. Running the command-string checker over the design +record surfaced 95 mismatches — commands renamed since a spec was written, and +commands proposed and rejected in exactly the sections that make the record +worth keeping. Every one was correct, so the check runs over specs in an +advisory mode that never fails: rewriting a spec to match today's command tree +would falsify the record ([S06](../spec/S06-documentation-site.md)). + +Not everything stated is practiced. The founding spec asks for specs "under +100 lines when possible"; the specs that carry the most weight run well past +200 ([S01](../spec/S01-spec-based-development.md)). Its three-step status +ladder has quietly grown into five states in the conventions document. The +method's own record shows the method being renegotiated, which is consistent +with everything above. + +For the conventions themselves, start at +[the spec index](../spec/000-specs.md). For how the design record relates to +the rest of this site, see [the design record](../design-record/index.md). diff --git a/docs/articles/why-mdd-has-its-own-ir.md b/docs/articles/why-mdd-has-its-own-ir.md new file mode 100644 index 0000000..fa2985a --- /dev/null +++ b/docs/articles/why-mdd-has-its-own-ir.md @@ -0,0 +1,206 @@ +# Why mdd has its own document IR + +Take one page from `mdd`'s test corpus. In the Confluence editor it reads "The +current status is On track. A problematic status would be Blocked" — 80 +characters of visible text, each status drawn as a small colored lozenge. In +storage format, each lozenge is a macro. This is the first one, copied from +the committed snapshot +`tests/corpus/confluence/_snapshots/164105/storage.xhtml`: + +```xml +On trackGreen +``` + +Before `mdd` had a document IR, exporting this page produced the Markdown +below — committed in +`tests/corpus/confluence/fixtures/niche-macros/status.md`, exported on +2026-05-11 by the converter `mdd` shipped at the time: + +```markdown +The current status is `On trackGreen`{=confluence} . +``` + +The 237-character macro became a 350-character inline code span of raw XML, +`xmlns` declarations included — semantically intact, but a blob no person +would want to edit. Round-tripping back to storage made it worse: the page's +80 characters of visible text came back as 750, and a sibling fixture holding +two same-space page links grew from 146 characters to 666 +([R09](../research/R09-confluence-ir-spike-status-quo.md)). + +Numbers like these are why `mdd` now converts between Confluence storage XHTML +and Markdown through its own intermediate representation: a tree of typed +Python `dataclass` nodes in `src/mdd/ir/` +([S28](../spec/S28-document-ir-foundation.md)). Building a document model from +scratch deserves suspicion — Pandoc alone has handled this problem space for +two decades. So the decision was made by +measurement: four candidate pipelines, scored against the same corpus of real +Confluence pages, and a winner that was not on the original shortlist. + +## Sync needs untouched content to survive + +The first version of Confluence sync punted on merge: when both Confluence and +the local Markdown changed between sync points, it reported a conflict and +skipped the page ([R06](../research/R06-confluence-bidirectional-sync-ir.md)). +Fixing that requires real bidirectional merge, and merge has a hard +prerequisite: every storage element the user did *not* touch must round-trip +unchanged — macro IDs intact, attribute order preserved, whitespace the way +Confluence emitted it. Otherwise every push silently rewrites half the page. + +The status-macro example shows why the old code could not deliver that. The +two converters, one per direction, shared no vocabulary: every Confluence +shape was encoded twice, with raw-XML spans like the one above as the only +escape hatch, and there was nothing either converter could attach identity or +provenance *to* ([S28](../spec/S28-document-ir-foundation.md)). The natural +fix is an intermediate representation with identity per node, so that merge +becomes a tree diff and untouched nodes re-render to their original bytes. + +That much was agreed early. Which IR to use was not, and the project settled +it empirically: a corpus of real Confluence pages, a harness that scores any +candidate pipeline the same way, and one spike per candidate. The design had +already corrected itself once before the spikes started. +An early draft spoke of "byte-for-byte" preservation, which the note itself +calls wrong: diff and merge belong at the level of the parsed tree, not at +byte offsets, where the first non-ASCII character breaks the arithmetic +([R06](../research/R06-confluence-bidirectional-sync-ir.md)). + +## Numbers, not vibes + +The harness ran every candidate against 35 snapshot pages from a live +Confluence instance, scoring each round trip on six measurements: how much +visible text survives, whether the block structure survives, whether macro IDs +survive, whether each Markdown block can be traced back to a storage element, +whitespace drift, and code size +([R08](../research/R08-confluence-ir-experiment-harness.md)). Its stated job +was to keep the comparison "grounded in numbers, not vibes". Each fixture +carried shape tags — callout, page link, status macro — so failures pointed +at specific Confluence constructs rather than disappearing into an average. + +Three candidates were shortlisted. All three lost. + +## The baseline: correct on prose, broken where it matters + +The first spike wrapped the existing converter pair unchanged, to give the +others a baseline to beat +([R09](../research/R09-confluence-ir-spike-status-quo.md)). On plain prose it +was fine: eleven of the 35 fixtures came through perfect on both fidelity +scores. It collapsed on the Confluence-namespaced shapes, exactly as the +example above shows — the status-macro page scored 0.11 on text fidelity, an +attachment link 0.13, a same-space page link 0.22, each ballooning into raw +XML because the Markdown-to-storage direction did not recognize the spans the +other direction emitted. The comparison's verdict: correct on most shapes, +brittle on exactly the ones merge cares about, no identity, no provenance +([R12](../research/R12-confluence-ir-comparison.md)). + +## Docling: good text, wrong structure + +Docling was attractive on paper: Python-native, in-process, already a +transitive dependency of `mdd`, MIT-licensed, with identity and provenance +slots built into its document model +([R06](../research/R06-confluence-bidirectional-sync-ir.md)). The research +flagged its gap before the spike ran: no equivalent of a raw block, no idiom +for "carry these bytes through verbatim, I don't know what they mean." + +The measurement sharpened that gap into a rejection. Docling scored well on +text fidelity — 0.94, where the baseline scored 0.86 — but worst of any +candidate on structure, at 0.41 +([R11](../research/R11-confluence-ir-spike-docling.md)). Its Markdown export +hard-wraps prose and its re-parse treats each wrapped line as a new paragraph, +so a four-paragraph fixture came back as twelve; Confluence-namespaced +elements it does not recognize are silently dropped. The comparison ruled it +out because the fixes were "upstream-shaped, not in our control" +([R12](../research/R12-confluence-ir-comparison.md)). + +One Docling claim had been withdrawn even earlier: that adopting it would +unify the document model across `mdd`'s PDF, DOCX, PPTX, and Confluence paths. +The note records that as "an overreach" — no writer exists from Docling's +model back to DOCX or PPTX, and the question was only ever about storage XHTML +and Markdown ([R06](../research/R06-confluence-bidirectional-sync-ir.md)). + +## Pandoc: the recommendation that lasted a day + +Pandoc was the reasonable bet. Its document model is twenty years old, +carrying unknown bytes through verbatim is a first-class concept there, and +the exact architecture — Pandoc plus a custom Lua writer emitting Confluence +storage — ships in production inside Quarto's Confluence publisher, which the +research studied line by line +([R06](../research/R06-confluence-bidirectional-sync-ir.md)). The spike +delivered: Pandoc won every fidelity score then measurable — 0.97 on text, +0.77 on structure, 0.75 on whitespace — in about 200 lines of adaptor and Lua +([R10](../research/R10-confluence-ir-spike-pandoc-lua.md)). + +On that evidence the comparison note recommended it, with two follow-ups +before production: wire identity and provenance through the writer, and close +known gaps around code blocks and merged table cells +([R12](../research/R12-confluence-ir-comparison.md)). + +The recommendation lasted less than a day, and its own text records the +tension that undid it. The fidelity margin over the baseline was real but +modest, while the costs were concrete: roughly 125 times the per-page latency, +a 7 MB GPL-licensed binary, Lua as a second implementation language — and, +most telling, the identity and provenance channels that motivated the whole +exercise still not wired up, deferred to follow-up work +([R13](../research/R13-confluence-ir-spike-pure-python.md)). + +## The late entrant that won + +The design principle had been on paper from the start — an IR with identity on +every node, diffed as a tree — yet every shortlisted candidate delegated the +IR to a third party. A fourth spike, run after the recommendation was written, +implemented the principle directly: a pure-Python typed IR, with identity and +provenance carried on the nodes themselves rather than bolted on afterward +([R13](../research/R13-confluence-ir-spike-pure-python.md)). + +It swept the board. Text fidelity 0.9988, structure 0.9838, whitespace 0.9830, +and identity preservation and provenance coverage both complete — it was the +only candidate that wired identity at all. Twenty-seven of the 35 fixtures +came through perfect on every measurement simultaneously, and the whole corpus +round-tripped in 31 ms against Pandoc's 2895 ms +([R13](../research/R13-confluence-ir-spike-pure-python.md)). The comparison +note was revised in place, keeping the Pandoc analysis under an explicit +"original recommendation" heading so the trade-off reasoning stays on the +record ([R12](../research/R12-confluence-ir-comparison.md)). + +## What it cost + +The one measurement the pure-Python IR lost was code size: 2249 lines, against +78 for the Docling adaptor and 202 for Pandoc +([R12](../research/R12-confluence-ir-comparison.md)). Reading that number is +the crux of the build-versus-adopt call. The 78-line Docling adaptor sits on +top of a large pre-1.0 library; the 202 Pandoc lines sit on a GPL binary plus +a Lua writer with no upstream; the 2249 lines are the only option whose entire +surface lives in this repository, with no library to track, version, or +migrate. `mdd` did not build an IR because building is fun. It bought +identity, provenance, and fidelity whose remaining gaps were a bounded list of +five fixes, about 50 lines in total, at the price of owning the code +([R13](../research/R13-confluence-ir-spike-pure-python.md)). + +## What shipped, and what got reversed after + +The spike was promoted into the production tree as three specs: the IR types +and identity contract ([S28](../spec/S28-document-ir-foundation.md)), the +Confluence storage converters +([S29](../spec/S29-confluence-ir-conversion.md)), and the Markdown converters +([S30](../spec/S30-markdown-ir-conversion.md)). The five fixes are recorded +there as shipped, and the old converter pair was deleted once the IR +converters took over, rather than kept indefinitely as wrappers +([S29](../spec/S29-confluence-ir-conversion.md)). + +Even the winning plan lost a piece after promotion. The spike and the +comparison both sketched an on-disk sidecar — a `.confluence.json` next +to each Markdown file — as the production home for cached identity +([R12](../research/R12-confluence-ir-comparison.md)). That design was retired: +page metadata lives in the Markdown file's YAML frontmatter, the cached-IR +lookup happens in-process from the remote-storage parse, and no sidecar file +is written during normal runs +([S31](../spec/S31-ir-normalization-and-whitespace.md)). + +What the IR guarantees today — which shapes round-trip byte-perfect, and where +the documented trade-offs sit — is a story of its own, covered in +[what near-lossless means](near-lossless.md). + +The method outlasts the outcome. Any of the four candidates could have been +argued for persuasively in prose; the corpus and harness made the argument +unnecessary. A written recommendation was overturned within a day, at the cost +of one more spike, because the harness made a late entrant directly comparable +to three incumbents ([R12](../research/R12-confluence-ir-comparison.md)). That +is the cheapest a reversal ever gets. diff --git a/docs/design-record/index.md b/docs/design-record/index.md new file mode 100644 index 0000000..a04f31f --- /dev/null +++ b/docs/design-record/index.md @@ -0,0 +1,34 @@ +# The design record + +The specs and research notes below are published because they are useful, not +because they are documentation. They are not written for you unless you are +working on `mdd` itself. + +## What is in here + +**Specs** (`SNN-.md`) are the durable design record. Each one states the +purpose of a feature, the requirements it has to meet, the approach chosen, and +the options rejected along the way. Non-trivial features start with a spec +before any code is written. [000-specs](../spec/000-specs.md) is the index and +the conventions. + +**Research notes** (`RNN-.md`) are working documents: surveys, +measurements, and spikes recorded at the moment that work happened. They are +not kept up to date as the code moves on, and specs deliberately do not link +back to them. [000-research](../research/000-research.md) is the index. + +## What it is not + +Every page here describes **intent at the time of writing**. A spec marked +"Implemented" was implemented once; the code has moved since, and the spec was +not always updated to match. A research note may compare three approaches and +recommend the one that was later abandoned. + +That is the point rather than a defect. Reversals, dead ends and superseded +recommendations are the most useful content in the corpus, and rewriting them +into a tidy narrative would destroy the reason to keep them. + +So: the guide describes what `mdd` does. The design record describes why it +does it that way, and what else was tried. When the two disagree about +behavior, the guide is closer to right, and the command's own `--help` is +closer still. diff --git a/docs/guide/01-install.md b/docs/guide/01-install.md new file mode 100644 index 0000000..2689bb0 --- /dev/null +++ b/docs/guide/01-install.md @@ -0,0 +1,140 @@ +# Install + +`mdd` is one command-line tool. The install is a single line. Everything else +on this page is optional and matters only for the features that need it. + +> [!WARNING] +> `mdd` is beta software. It is written largely by AI agents under human +> review, and it has had no independent security review. Several commands write +> to production document systems. Read [Safety](04-safety.md) before you point +> `mdd` at content you care about. + +## Prerequisites + +`mdd` needs Python 3.14. You do not have to install Python yourself. The +supported installer is [`uv`](https://docs.astral.sh/uv/), which fetches a +matching interpreter and keeps the tool in its own environment, isolated from +anything else on your machine. + +Install `uv` first if you do not already have it. Follow +[Astral's installation instructions](https://docs.astral.sh/uv/getting-started/installation/). + +## Install `mdd` + +```bash +uv tool install git+https://github.com/schubergphilis/mdd +``` + +There is no PyPI package and no stable release. That command builds from the +tip of `main`, so what you get is whatever was merged most recently. Two +consequences: the tool can change under you between installs, and you should +read the [Safety](04-safety.md) page rather than assume a version number means +anything. + +## Verify + +```bash +mdd --version +``` + +``` +mdd 0.1.1.dev17+gf5812ba +``` + +The version string comes from git rather than from a release process. +`0.1.1.dev17+gf5812ba` means seventeen commits past the `v0.1.0` tag, at commit +`f5812ba`. Quote that string when you report a problem; it identifies the exact +build. + +Then get your bearings: + +```bash +mdd help +``` + +## Optional tools + +`mdd` runs without any of these. Each one turns on a specific feature, and the +command that needs it says so when it is missing. + +| Tool | Needed for | +|---|---| +| [Quarto](https://quarto.org) | Rendering a `mdd new` project into `.pptx` and `.docx` | +| Microsoft Office on macOS | `mdd pdf`, `mdd pdf-pptx`, `mdd pdf-docx` | +| [ripgrep](https://ripgrep.org) (`rg`) | `mdd search` | +| [librsvg](https://gitlab.gnome.org/GNOME/librsvg) (`rsvg-convert`) | Rasterizing `.svg` files into `.svg.png` siblings during a sync | +| [1Password CLI](https://www.1password.dev/cli) (`op`) | Resolving `op://` references in configuration | + +Two notes on the first two rows. + +PDF export drives Word and PowerPoint through AppleScript, so it works on macOS +with Microsoft Office installed and nowhere else. There is no fallback. + +Quarto is only needed to *render* a project. `mdd new` scaffolds one without +it. + +> [!NOTE] +> The first `mdd convert` downloads Docling's machine-learning models, around +> 500 MB, into `~/.cache/docling/`. Later runs reuse the cache. Budget for that +> download the first time, and expect it to fail behind a proxy that blocks +> model downloads. + +`mdd ai` needs an API token for a LiteLLM gateway. See +[Configuration and secrets](05-configuration.md). + +## Upgrade + +```bash +uv tool upgrade mdd +``` + +The install points at a git URL rather than a version, and `main` moves. If +`uv` decides there is nothing to do, force a rebuild from the current tip: + +```bash +uv tool install --force git+https://github.com/schubergphilis/mdd +``` + +Re-run `mdd --version` afterwards and check that the commit changed. + +## Uninstall + +```bash +uv tool uninstall mdd +``` + +That removes the tool and its environment. It leaves four things behind, on +purpose: + +- configuration under `~/.config/mdd/` +- the Docling model cache under `~/.cache/docling/` +- any mirror repository you cloned or created +- skill symlinks under `~/.claude/skills/` + +Remove the skill symlinks with `mdd skills uninstall` *before* you uninstall +the tool. Delete the rest by hand if you want them gone. + +## Install from a clone + +Contributors, and anyone who wants to run an unmerged branch, work from a +clone instead: + +```bash +git clone https://github.com/schubergphilis/mdd +cd mdd +mise install +mise run install +uv run mdd --version +``` + +Inside a clone, run `uv run mdd` rather than `mdd`. A bare `mdd` resolves to +whatever is on your `PATH`, which is usually the tool install and not the code +you are editing. + +[CONTRIBUTING.md](../../CONTRIBUTING.md) covers the development setup, the +quality gate, and how to propose a change. + +## Next + +[Quickstart](02-quickstart.md) walks through a first run that needs no +Confluence or SharePoint access. diff --git a/docs/guide/02-quickstart.md b/docs/guide/02-quickstart.md new file mode 100644 index 0000000..14f46b1 --- /dev/null +++ b/docs/guide/02-quickstart.md @@ -0,0 +1,291 @@ +# Quickstart + +This is a tour of the local half of `mdd`: scaffold a document, render it, +convert it back to Markdown, and search the result. Nothing here touches +Confluence or SharePoint, and nothing here needs an account anywhere. Allow +about ten minutes, plus a one-time model download in step 6. + +Before you start, [install `mdd`](01-install.md). Three steps need a tool you +may not have: Quarto, Microsoft Office, and ripgrep. Each of those steps says +so at the top. + +> [!NOTE] +> `mdd` is quiet by default: it logs at warning level, so a successful command +> often prints nothing at all. Pass `-v` before the subcommand to see what it +> is doing. This page uses `-v` throughout. + +## 1. Make a scratch directory + +Work somewhere disposable. You will delete it at the end. + +```bash +mkdir ~/mdd-quickstart +cd ~/mdd-quickstart +``` + +## 2. Scaffold a document project + +`mdd new` creates a [Quarto](https://quarto.org) project that produces a +PowerPoint deck and a Word document from one Markdown source. + +```bash +mdd -v new mdd-tour +``` + +``` +INFO mdd.utils.scaffolding: Created: mdd-tour/mdd-tour.qmd +INFO mdd.utils.scaffolding: Output: Both PowerPoint (.pptx) and Word (.docx) +INFO mdd.utils.scaffolding: Hint: Run 'cd mdd-tour && ./render.sh' to generate the output +``` + +## 3. Look at what it produced + +```bash +cd mdd-tour +ls -1F +``` + +``` +mdd-tour.qmd +render.sh* +simple-document.docx@ +simple-presentation.pptx@ +``` + +Four entries, and each one is worth a moment. + +`mdd-tour.qmd` is the source: YAML frontmatter naming the two output formats, +then ordinary Markdown. + +```yaml +--- +title: "mdd-tour" +subtitle: "Your subtitle here" +author: "Your Name" +format: + pptx: + reference-doc: simple-presentation.pptx + docx: + reference-doc: simple-document.docx + toc: true +--- +``` + +`render.sh` is a convenience wrapper that calls `quarto render` and then +`mdd pdf`. + +The two Office files are **symlinks**, not copies. They point into the bundled +templates inside your `mdd` installation, and they supply the fonts, colors and +slide masters that Quarto renders into. Two things follow: the project is not +self-contained, and moving it elsewhere breaks the links. If you want a +portable project, replace the symlinks with real copies. + +`mdd new-pptx` and `mdd new-docx` scaffold a single-format project instead, and +`mdd new-pptx --compact` uses a smaller type scale for dense slides. + +## 4. Render it + +This step needs Quarto. If you do not have Quarto, skip to step 6 and use any +`.docx`, `.pptx` or `.pdf` file you already have in place of `mdd-tour.docx`. + +```bash +quarto render mdd-tour.qmd +``` + +Quarto prints its full Pandoc configuration for each format. The lines that +matter are at the end of each block: + +``` +pandoc + to: pptx + output-file: mdd-tour.pptx + reference-doc: simple-presentation.pptx +... +Output created: mdd-tour.pptx +``` + +You now have `mdd-tour.pptx` and `mdd-tour.docx` alongside the source. + +## 5. Export to PDF (optional) + +`mdd pdf` drives Word and PowerPoint through AppleScript, so this step works on +macOS with Microsoft Office installed and nowhere else. Skip it otherwise; +nothing later depends on it. + +```bash +mdd -v pdf . +``` + +``` +INFO mdd.commands.pdf: === Exporting PowerPoint files === +INFO mdd.utils.pdf_export: Found 1 file(s) to export: +INFO mdd.utils.pdf_export: - mdd-tour.pptx +INFO mdd.utils.pdf_export: Exporting: mdd-tour.pptx -> mdd-tour.pptx.pdf +INFO mdd.utils.pdf_export: Exported 1 file(s), skipped 0 file(s) +INFO mdd.commands.pdf: === Exporting Word documents === +INFO mdd.utils.pdf_export: Found 1 file(s) to export: +INFO mdd.utils.pdf_export: - mdd-tour.docx +INFO mdd.utils.pdf_export: Exporting: mdd-tour.docx -> mdd-tour.docx.pdf +INFO mdd.utils.pdf_export: Exported 1 file(s), skipped 0 file(s) +INFO mdd.commands.pdf: All exports completed successfully +``` + +Word and PowerPoint open while this runs. The output keeps the source +extension: `mdd-tour.docx.pdf`, not `mdd-tour.pdf`. Every derived file in `mdd` +is named that way, so you can always tell what a file came from. Export is also +incremental — it skips a PDF that is newer than its source unless you change +the source. + +## 6. Convert an Office file back to Markdown + +This is the direction `mdd` uses when it pulls documents out of a remote +system. `mdd convert` reads `.docx`, `.pptx` and `.pdf` and writes a Markdown +sibling. + +> [!IMPORTANT] +> The first run downloads around 500 MB of Docling models into +> `~/.cache/docling/`. This is the slow step. Later runs reuse the cache. + +Convert the one file rather than the directory: + +```bash +mdd -v convert mdd-tour.docx +``` + +``` +INFO mdd.commands.convert: [1/1] Converting: mdd-tour.docx +``` + +That writes `mdd-tour.docx.md`. Pointing `mdd convert` at the directory instead +would also convert the two template symlinks, which is rarely what you want. + +Read the result: + +```bash +cat mdd-tour.docx.md +``` + +```markdown +# mdd-tour + +# mdd-tour + +**Your subtitle here** + +**Your Name** + + +### 1 Introduction + +This is a sample document that outputs to both PowerPoint and Word formats. + +### 2 Key Points + +- Edit this .qmd file with your content +- Run ./render.sh to generate both .pptx and .docx files +- The unified mdd pdf command will export both to PDF + +### 3 Code Example + +def hello(): + print("Hello, world!") + +### 4 Conclusion + +- Quarto makes it easy to create multiple output formats +- Single source, multiple outputs +``` + +Compare that against `mdd-tour.qmd` and you have the honest version of what +"near-lossless" means. The prose, the structure and the list items survive. The +title appears twice, `##` headings came back as `###` with Word's numbering +attached, inline code lost its backticks, and the Python block is no longer a +fenced code block. + +This is the round trip through a *rendered* Word file, which is the worst case: +Quarto turned Markdown into Word styling, and Docling read the styling back as +best it can. The Confluence path preserves far more, because `mdd` maps +Confluence storage format to its own document model rather than guessing from +visual styling. [Concepts](03-concepts.md) explains the difference. + +The practical rule: treat converted Markdown as a starting point for a human to +review, not as a faithful copy. + +## 7. Search it + +`mdd search` wraps [ripgrep](https://ripgrep.org), so it needs `rg` on your +`PATH`. It normally searches the mirror roots named in your configuration. You +have none yet, so it tells you so: + +```bash +mdd search "PowerPoint" +``` + +``` +No configured mirror roots found. Use --include to add a search path. +``` + +Add a root for this one run: + +```bash +mdd search --include . "PowerPoint" +``` + +``` +./mdd-tour.docx.md + Title: mdd-tour + + L12: This is a sample document that outputs to both PowerPoint and Word formats. +``` + +Results carry the document title as well as the path. The title comes from +`title:` in the frontmatter, or from the first `#` heading when there is no +frontmatter, as here. That is what makes results readable when the mirror is +thousands of pages deep. The default cap is ten matches in total, and `--limit` +changes it: + +```bash +mdd search --include . --limit 3 "output" +``` + +``` +./mdd-tour.docx.md + Title: mdd-tour + + L12: This is a sample document that outputs to both PowerPoint and Word formats. + L27: - Quarto makes it easy to create multiple output formats + L28: - Single source, multiple outputs +``` + +`--json` prints one record per match, which is the form to use from a script or +an agent. Once you configure a mirror, `--space` and `--site` narrow the search +to one Confluence space or one SharePoint site. + +## 8. Clean up + +```bash +cd ~ +rm -rf ~/mdd-quickstart +``` + +The Docling model cache in `~/.cache/docling/` stays. Delete it too if you do +not plan to run `mdd convert` again. + +## Next + +You have seen the offline half of `mdd`. The other half mirrors a remote system +into git, and that half can overwrite pages other people wrote. + +> [!CAUTION] +> Read [Safety](04-safety.md) before your first sync. `mdd confluence +> sync-space` and `mdd sharepoint sync-site` write to production systems by +> default. + +- [Concepts](03-concepts.md) — what a mirror is, and which way content flows. +- [Safety](04-safety.md) — what can destroy data, and how to hold it back. +- [Configuration and secrets](05-configuration.md) — config files and `op://` + references. +- [Your first Confluence sync](07-confluence-first-sync.md) — bring your own + tenant, starting read-only. +- [Your first SharePoint sync](08-sharepoint-first-sync.md) — the same, through + OneDrive. diff --git a/docs/guide/03-concepts.md b/docs/guide/03-concepts.md new file mode 100644 index 0000000..92da175 --- /dev/null +++ b/docs/guide/03-concepts.md @@ -0,0 +1,171 @@ +# Concepts + +Five ideas carry most of `mdd`: the mirror, sync direction, the document +IR, frontmatter, and `.mddignore`. This page explains what they are. The +how-to pages ([Your first Confluence sync](07-confluence-first-sync.md), +[Your first SharePoint sync](08-sharepoint-first-sync.md)) show you what to +run. + +## The mirror + +A mirror is a directory of Markdown files that corresponds to one remote +container: one Confluence space, or one SharePoint site. Keep it in git. +Several protections in `mdd` depend on git being there, and git history is +the only way to get a local file back after a sync overwrote it. + +For Confluence, the layout follows the page tree. Every page becomes one +`.md` file, named after the sanitized page title. A page that has children +also gets a directory of the same name, holding the children. Confluence +*folders* become directories with no `.md` beside them. Attachments and +embedded images land in `-attachments/`. + +``` +ENGINEERING/ +├── Onboarding.md a page +├── Onboarding/ its children +│ ├── Laptop setup.md +│ └── Laptop setup-attachments/ +│ └── dock.png +└── Runbooks/ a Confluence folder: no .md + └── Restart the gateway.md +``` + +For SharePoint the layout copies the source directory structure. Each +Office file gets a Markdown sibling in the output tree: `Foo.docx` produces +`Foo.docx.md`, with extracted images in `Foo.docx-attachments/`. A PDF +converts to `Foo.pdf.md`. Plain `.md` files are copied through. +Spreadsheets and standalone image files are skipped. + +Filenames come from titles, and titles change. `mdd` does not use the path +as identity — every Confluence file is keyed by the `page_id` in its +frontmatter, which survives rename, move, archive and unarchive. That is +why a rename in Confluence turns into a `git mv` in the mirror rather than +a delete and a create, and why the mirror's history stays readable. + +## Sync direction + +**Pull** means remote to mirror. **Push** means mirror to remote. +`mdd confluence sync-space` does both in a single run, and so does +`mdd sharepoint sync-site`. This is bidirectional sync, with everything +that implies: a run can change your local files, your remote system, or +both, and it decides which without asking. + +A pull overwrites the local file. For any page whose remote version number +advanced, `mdd` fetches the body again, re-renders it, and rewrites the +frontmatter. Local edits to that file are gone unless git has them. + +A push overwrites the remote page body with a render of the local file. +There is no merge. Confluence keeps the previous body as a version in page +history, so the old content is recoverable, but the live page is replaced. + +When both sides changed, `mdd` refuses rather than guessing. If a local +edit is queued for push and the remote version number has moved past the +version recorded in the local frontmatter, `mdd` skips both the push and +the pull for that page and reports it as a conflict. + +SharePoint is deliberately asymmetric. Office to Markdown always runs. +Markdown to Office runs only for pairs whose frontmatter carries +`update_office: true`, because the Markdown-to-Office render goes through +Quarto and loses shapes, styles and inline metadata. The default treats the +Office file as authoritative and ignores Markdown edits, with a warning +telling you how to opt in. + +Both sync commands accept `--read-only`, which suppresses every write to +the remote and leaves the pull half running, and `--dry-run`, which prints +the plan and writes nothing anywhere. + +## The document IR and "near-lossless" + +Confluence stores a page as storage-format XHTML, a namespaced dialect with +elements for macros, layouts and links that have no Markdown equivalent. +`mdd` does not translate that dialect to Markdown and back directly. +It parses it into a typed tree, the document IR, and every converter is a +walker over that shared shape. [S28](../spec/S28-document-ir-foundation.md) +describes the model. + +"Near-lossless" is a claim about a specific round-trip: storage to IR to +Markdown to IR to storage. For content you did not edit, that round-trip is +byte-for-byte identical, and CI fails when a corpus page stops +round-tripping ([S33](../spec/S33-ir-roundtrip-testing-and-benchmarks.md)). + +Two mechanisms hold it up. + +The first is that unrecognized content is never dropped. A Confluence macro +`mdd` has no model for rides through as a fenced block tagged +`confluence-xml`, holding the source XHTML verbatim: + +````markdown +```confluence-xml + + one + +``` +```` + +You can read around such a block and edit the prose next to it. Editing +inside it means editing storage XHTML by hand. + +The second is identity grafting. Confluence attaches identity attributes to +elements (`ac:local-id`, `ac:macro-id`, `ac:schema-version`) that Markdown +has nowhere to put. Before a push, `mdd` fetches the current remote storage +and grafts those attributes back onto the tree parsed from your Markdown, +so an unedited page pushes back as the same bytes rather than as a page +where every macro lost its id. + +What does not survive is documented rather than hidden. Backslash escapes +(`\*`, `\_`) are dropped by the Markdown reader per CommonMark and are not +restored on the way back. An empty paragraph carrying an anchor id has no +Markdown syntax and is not invented. Inline `` and `` raw HTML +falls outside the Markdown reader's allowlist. + +Separately from the round-trip, some things are not modeled at all. +Comments are neither read nor written. Page restrictions and page +properties are not synced. Labels are read into frontmatter but not pushed +back. [Safety](04-safety.md) covers what that means when you push. + +## Frontmatter + +The YAML block at the top of a mirrored file is the binding between the +local file and the remote object. It is not decoration, and hand-editing it +changes behavior. + +For Confluence, `confluence.page_id` is identity: it tells `mdd` which +remote page this file is. `confluence.version` is the conflict check: +`mdd` compares it against the live version number and refuses to push when +the remote has moved ahead. For SharePoint, the `sharepoint.sync` block +carries the hashes of both sides at the last sync, which is how divergence +is detected at all. + +Three states follow from what the block contains: + +- **Tracked.** A `confluence.page_id` is present. The file is one end of a + sync pair. +- **Publish candidate.** No `page_id`, but a `confluence.space_key` is + present. The next sync creates this as a new Confluence page. +- **Manually managed.** Anything else, including a file with no + frontmatter at all. Sync never deletes, moves or rewrites it. + +That third state is a deliberate safety property, and it means a plain +Markdown file you drop into a mirror is left alone. It also means a +frontmatter *typo* falls into it. Frontmatter is validated with unknown +keys rejected ([S40](../spec/S40-typed-frontmatter.md)), so writing +`spcae_key` makes the whole block unreadable, and the file silently stops +being part of the sync instead of silently syncing the wrong thing. + +## `.mddignore` + +`.mddignore` filters the source side before anything is downloaded or +converted. It uses gitignore pattern syntax, and it lives at the root of +the mirror directory. It exists because sync is otherwise all-or-nothing: +a SharePoint site with a 7 GB `Archive/` subtree costs you that 7 GB on +every run. + +It behaves like `.gitignore` in the way that matters: adding a pattern +blocks new pulls, and does nothing to files already on disk. Cleaning those +up is a separate, explicitly requested operation. `mdd` will not delete +local content because a pattern started matching it. + +`.mddignore` is not a confidentiality control. It runs on the pull side, to +avoid fetching content you do not want. The gate that stops content being +published is separate, and [Safety](04-safety.md) describes it. +[S39](../spec/S39-mddignore.md) has the pattern rules. diff --git a/docs/guide/04-safety.md b/docs/guide/04-safety.md new file mode 100644 index 0000000..be4f814 --- /dev/null +++ b/docs/guide/04-safety.md @@ -0,0 +1,243 @@ +# Safety + +> [!CAUTION] +> `mdd` is beta software that writes to production document systems. It is +> written almost entirely by AI agents under human review, it has had no +> independent security review, and it has no stable release. If you read +> one thing here, read this: point it at a scratch space first, run +> `--dry-run` before every unfamiliar operation, and keep the mirror in +> git so you have something to go back to. + +This page is organized by operation, because that is how damage happens. +Each section says what changes, what it overwrites, and what you can get +back. + +## Which commands write, and where + +| Command group | Writes to | +|---|---| +| `mdd convert`, `mdd new`, `mdd pdf` | local files only | +| `mdd search`, `mdd confluence whoami`, `mdd sharepoint list-sites` | nothing | +| `mdd confluence export-page` | local files only | +| `mdd skills install`, `mdd skills uninstall` | your skills directory | +| `mdd ai rewrite`, `mdd ai index` | local files, and only with `--apply` | +| `mdd ai review` | a local report file | +| `mdd confluence create-page`, `update-page`, `rename-page`, `move-page`, `archive-page`, `unarchive-page` | Confluence | +| `mdd confluence sync-space` | local files, Confluence, git | +| `mdd sharepoint sync-site`, `mdd sharepoint sync-folder` | local files, your OneDrive folder, git | + +Two entries deserve a second look. + +`mdd sharepoint` writes into the locally OneDrive-synced folder, not to a +SharePoint API. That is not a smaller blast radius. OneDrive uploads what +`mdd` writes, so an overwritten `.docx` in that folder becomes a new +version in SharePoint moments later, for everyone. + +The `mdd ai` commands send file content to whichever AI gateway you +configured. That is a third party receiving your documents. Whether that is +acceptable is a decision about your data, not about `mdd`. + +## What a Confluence push does + +A push replaces the live page body with a render of your local Markdown. +There is no merge and no three-way anything. The previous body becomes a +version in Confluence page history. + +Three specific behaviors surprise people: + +**The first `# H1` in the file sets the page title.** `mdd` reads the title +from the body, not from frontmatter, and sends it with every update. Edit +the H1 and the next push renames the Confluence page. If the file has no +H1, the filename stem is used. + +**`mdd` appends one footer line** to the rendered page, pointing at the +mirror. It replaces its own previous footer rather than stacking, and it is +skipped when no browse URL is available. It is the only element `mdd` +injects into a Confluence page. + +**Attachments are upload-only.** Local images referenced from the body are +uploaded, or re-uploaded as a new attachment version when their content +hash changed. Attachments already on the page that your file does not +reference are left alone. `mdd`'s Confluence client issues no HTTP DELETE +at all, so it cannot delete a page or an attachment. Archiving is a status +change, and `mdd confluence unarchive-page` reverses it. + +Two guards fire before any push and abort it: + +- An **empty body** after the export header is stripped. Overriding this + needs `--allow-empty`. +- A body **under 10% of the length** of the live page. Overriding this + needs `--allow-shrink`. + +Both exist to catch accidental content loss, and both are crude length +checks rather than anything semantic. They will not catch a push that +deletes half a page. + +## What a push does to a page that changed remotely + +`mdd` compares the `confluence.version` in your local frontmatter against +the live version number. + +If the remote has moved ahead, a single-page `mdd confluence update-page` +aborts with a version-drift error and pushes nothing. Inside +`mdd confluence sync-space`, the same page is recorded as a conflict and +skipped in **both** directions: not pushed, and not pulled either, so your +local edit is not overwritten while you deal with it. Resolve it by +re-exporting the page, reconciling the two versions by hand, then pushing. + +There is also a narrower race. If somebody saves the page between `mdd` +fetching it and `mdd` writing it, Confluence rejects the write with a 409 +and `mdd` reports a conflict. Nothing is written. + +## What `mdd` does not model + +Anything in this list is invisible to `mdd`. It is not read, not written, +and not carried across a round-trip: + +- **Comments.** Neither inline nor page comments. +- **Page restrictions.** `mdd` reads them only to decide that a page is + read-only for you, and never sets them. +- **Page properties** and content properties. +- **Labels on push.** Labels are exported into frontmatter, but a push does + not write them back. + +Those objects live in Confluence and are not touched by a body update, so a +push does not destroy them. The risk is different: `mdd` shows you a +Markdown file that looks like the whole page, and it is not the whole page. + +Inside the body, [Concepts](03-concepts.md) lists the constructs that do +not survive a round-trip byte-for-byte. Unrecognized macros survive as +opaque `confluence-xml` blocks rather than being dropped. + +## Pages managed by other automation + +If another system publishes a Confluence page — a docs pipeline, an +external sync job — `mdd` must not fight it. The "managed elsewhere" +mechanism detects those pages and blocks every push to them. + +Detection runs on every push attempt, in this order: the page's space is in +a configured managed list; an ancestor page is a configured managed +subtree root; the last author's account id belongs to a configured +publisher; the page body matches a publisher's marker pattern; or the +Confluence restrictions say you cannot update the page. + +A match is final. There is no `--force` for it, no frontmatter escape +hatch, and no per-page bypass. `mdd confluence update-page` exits with the +publisher's message; `mdd confluence sync-space` skips the page and reports +it in the run summary, and the run still exits 0 because that is expected +steady state. Pulling a managed page always works, and the exported file +carries a header saying who owns it and where to edit it. + +To bypass a wrong detection you edit `configs/external-publishers.yaml` to +remove the fingerprint, which leaves a diff in git. That friction is the +point. [S26](../spec/S26-managed-elsewhere.md) covers the details. + +## The confidentiality blacklist, and what it does not cover + +`configs/data-protection.yaml` lists Confluence space keys and SharePoint +site names that must not leave their source system. Matching is +case-insensitive, exact by default, with a trailing `*` for a prefix match +and no other wildcards. + +Be precise about what this protects in the open-source core: + +> [!WARNING] +> The SharePoint side is enforced. `mdd sharepoint sync-site` and +> `sync-folder` call the blacklist check before doing any work, so a +> blacklisted site name aborts the whole run. +> +> The Confluence side is **not** enforced in this distribution. No +> Confluence command consults the blacklist, and the built-in git mirror +> backend applies no data-protection gate — its own source says so. Only +> `mdd search --exclude-blacklisted` reads the Confluence list. A +> deployment that supplies its own mirror backend can add the gate; the +> core does not have one. + +Also note that the file shipped in this repository is a template. Its +`blacklisted_spaces` list is empty and its site list is illustrative. An +empty blacklist blocks nothing. Entries combine across the bundled file, +`~/.config/mdd/data-protection.yaml`, a `./configs/` copy, and any +`--blacklist` argument, so you extend the list rather than replacing it. + +The blacklist governs publishing. It is not a filter on what gets pulled +into the mirror in the first place; that is `.mddignore`, and the two do +not interact. + +## Dry runs, prompts, and where there are none + +`--dry-run` is available on both sync commands and on every single-page +Confluence mutation. It computes the full plan, prints it, and touches +nothing. Use it. + +Confirmation prompts are narrower than you might assume: + +| Operation | Prompts? | +|---|---| +| `mdd confluence update-page` | yes, after printing a diff; `--yes` skips it | +| `mdd confluence rename-page`, `move-page`, `archive-page`, `unarchive-page` | yes; `--yes` skips it | +| `mdd confluence create-page` | **no** | +| `mdd confluence sync-space` | **no**, for any of its writes | +| `mdd sharepoint sync-site`, `sync-folder` | **no** | + +`mdd confluence sync-space` calls the same push code as `update-page` with +confirmation already suppressed. It creates pages, pushes bodies and +publishes Office attachments without asking. The empty-body and shrink +guards still run, and managed pages are still skipped, but nothing stops +to check with you. This is the command most likely to surprise you, and +`--read-only` is how you take the write half off the table while keeping +the pull. + +Both sync commands refuse to run when the mirror working tree has +uncommitted changes, so a sync never mixes your edits into its own commit. +There is no override. That check asks git, so it does nothing if your +output directory is not a git repository — one more reason to make it one. + +`--prune-ignored` deletes local mirror files matching `.mddignore`. It logs +every deletion, it is per invocation and never sticky, and combining it +with `--read-only` is rejected outright. + +## Recovery + +What you can actually get back: + +**A Confluence page body.** Every push creates a new page version, and +`--message` sets the version comment. Confluence keeps previous versions +and you restore one from the page's version history in the Confluence UI. +`mdd` has no restore command. + +**A local mirror file.** Only from git. A pull overwrites the file in +place. Sync makes one commit per run, so `git log` on the mirror shows what +each run changed and `git revert` or `git checkout` gets a file back. If +the mirror is not in git, an overwritten file is gone. + +**A page deleted in Confluence.** Sync removes it from the mirror with +`git rm`, so it stays in git history. `--no-delete` keeps the local copy. +Restoring the page itself is a Confluence operation. + +**A `.docx` overwritten by a SharePoint sync.** `--backup` copies the prior +file to `.mdd-backups//-` before overwriting, and is +off by default. Beyond that you are relying on SharePoint's own version +history, which is a tenant setting rather than something `mdd` guarantees. +Check whether it is on for your library before you need it. + +When both sides of a SharePoint pair changed, `mdd` writes a candidate +render to `Foo.from-md.docx` and touches neither original. Port the changes +by hand in Word, delete the candidate, and re-run. + +## What has not been reviewed + +Read [SECURITY.md](../../SECURITY.md) before deploying this anywhere that +matters. In short: + +- There has been **no independent security review** of this codebase. +- It is written almost entirely by AI agents. The human review is real, and + it is not an audit. +- There is no 1.0. Breaking changes land between `0.x` minor versions. +- The data-protection support described above exists + ([S07](../spec/S07-data-protection.md)) and has not been independently + reviewed either, and as noted, its Confluence half is not wired up in + this distribution. +- `mdd ai` output comes from a model. It can be wrong. + +Report vulnerabilities privately. Do not include real customer data, live +credentials, or content from a private space in a report. diff --git a/docs/guide/05-configuration.md b/docs/guide/05-configuration.md new file mode 100644 index 0000000..cdb057e --- /dev/null +++ b/docs/guide/05-configuration.md @@ -0,0 +1,212 @@ +# Configuration and secrets + +`mdd` reads several small YAML files rather than one big one. Each file +covers one concern and has its own search path. Nothing is generated for +you: if a command needs a config file and cannot find one, it says so and +exits. + +## Where configuration lives + +Every file below is optional except where a command needs it. The general +shape is: an explicit path on the command line wins, then a `configs/` +directory relative to your **current working directory**, then +`~/.config/mdd/`. + +| File | Holds | Search path | +|---|---|---| +| `confluence.yaml` | Confluence URL, username, API token | `--config`, then `./configs/`, then `~/.config/mdd/` | +| `ai.yaml` | AI gateway URL, token, model aliases | `--config`, then `./configs/`, then `~/.config/mdd/` | +| `data-protection.yaml` | the confidentiality blacklist | all of them, combined (see below) | +| `external-publishers.yaml` | pages owned by other automation | bundled, then `~/.config/mdd/`, then `./configs/external-publishers.local.yaml` | +| `sharepoint-mapping.yaml` | site name to repository name | `--mapping`, then `./configs/`, then `~/.config/mdd/` | +| `config.yaml` | extra search roots under a `docs:` key | `./configs/`, then `~/.config/mdd/` | + +The cwd-relative `./configs/` entry is more load-bearing than it looks. It +means the directory you run from decides which tenant you talk to. The test +corpus in this repository uses exactly that property: it ships a +`configs/confluence.yaml` pointing at a throwaway instance, so commands run +from inside it cannot reach a production tenant by accident. Consider doing +the same for any mirror you keep locally. + +Two files behave differently on purpose. + +`data-protection.yaml` is **additive**. Every copy that exists is loaded +and the entries combine: the file bundled with the `mdd` install, then +`~/.config/mdd/data-protection.yaml`, then `./configs/data-protection.yaml`, +plus a file named with `--blacklist` on the commands that accept it +(`mdd search` today). You extend the list; you cannot +shrink it by shadowing the file. Removing an entry means editing the file +that contains it, which leaves a diff in git. + +`external-publishers.yaml` merges in the other direction: the bundled file +is the base, `~/.config/mdd/external-publishers.yaml` overrides it, and +`./configs/external-publishers.local.yaml` overrides that. Merging appends +managed spaces and subtrees, and extends the account-id list of a publisher +that already exists by name. + +## Confluence + +```yaml +# configs/confluence.yaml +confluence: + url: https://your-instance.atlassian.net + username: you@example.com + api_token: op://Employee/confluence-pat/token +``` + +`username` is your Atlassian account email and is not a secret. `url` is +the instance base URL; a page URL you pass on the command line must match +it, or `mdd` refuses the operation rather than risk hitting another tenant. + +Create the API token in Atlassian's account settings, not in Confluence +itself. `mdd confluence whoami` is the cheapest way to confirm the token +works: it prints the account it authenticated as. + +Note what is **not** here. `mdd confluence sync-space` does not read an +output directory out of this file. It uses `--output`, or, when the current +directory is a clone whose `origin` URL names the space, the current +directory. Configure `spaces:` if you want [`mdd search`](06-commands.md) +to find the mirror: + +```yaml +confluence: + url: https://your-instance.atlassian.net + username: you@example.com + api_token: op://Employee/confluence-pat/token + spaces: + ENGINEERING: + output_dir: ~/mirrors/ENGINEERING +``` + +## SharePoint + +SharePoint needs no credentials at all. `mdd` never calls a SharePoint API; +it reads the folder OneDrive has already synced to your disk, and your +OneDrive client holds the authentication. See +[Your first SharePoint sync](08-sharepoint-first-sync.md) for what that +changes. + +The sync root is discovered: `mdd` looks for a single +`~/Library/CloudStorage/OneDrive-SharedLibraries-*` directory. If you have +more than one tenant synced, the discovery is ambiguous and the command +stops and tells you so. In that case, point `mdd sharepoint sync-folder` at +the site folder directly by path. + +`configs/sharepoint-mapping.yaml` maps a SharePoint site name to a +repository name, because site names contain spaces and repository paths do +not. It is committed to git so everyone derives the same name: + +```yaml +# configs/sharepoint-mapping.yaml +sites: + "HR Documentation": + repo: HR-Documentation +``` + +Sites with no entry fall back to a default rule (whitespace and special +characters collapse to `-`), and `mdd sharepoint list-sites` warns about +each one so you can decide whether to pin it. + +For `mdd search` to see SharePoint mirrors, give them an `output_dir` under +`sharepoint.sites` in `configs/sharepoint.yaml`. + +## The AI gateway + +`mdd ai` talks to any OpenAI-compatible endpoint. There is no useful +default, so set one: + +```yaml +# configs/ai.yaml +ai: + base_url: https://your-gateway.example.com/v1 + api_token: op://Employee/ai-gateway/token + token_hint: >- + request a gateway token from the platform team, then store it in + 1Password under Employee/ai-gateway field token. + models: + default: claude-sonnet-4-5 + summarise: claude-haiku-4-5 + embed: text-embedding-3-large +``` + +`token_hint` is shown verbatim whenever the token is missing or rejected. +Fill it in with wherever your tokens actually come from; it is the one +piece of your deployment's operational knowledge `mdd` can repeat back to a +confused user. The `models` block maps the three task names `mdd` resolves +onto whatever your gateway serves. + +## Secrets + +The rule is short: **no secret ever goes into a config file, into the +mirror, or into git.** + +Every token field accepts a 1Password secret reference instead of a value. +`mdd` resolves it by shelling out to the `op` CLI at the moment it is +needed, caches the result in memory for that one command, and never writes +it to a file, a log, or an error message. Exceptions redact anything that +looks like a token or an `op://` reference before it can reach a stack trace. + +```yaml +api_token: op://Employee/confluence-pat/token +``` + +If you are signed in to several 1Password accounts and the vault lives in a +non-default one, pin the account: + +```yaml +api_token: + ref: op://Employee/confluence-pat/token + account: example +``` + +`account` is the shorthand from `op account list`, or the sign-in address. +Without it, `op` uses whichever account was active last and may report that +the vault does not exist. The same object form works for `ai.api_token`. +You can also set the `MDD_OP_ACCOUNT` environment variable, or `op`'s own +`OP_ACCOUNT`. + +This needs the 1Password CLI installed, signed in, and with desktop app +integration enabled. `mdd` bundles no 1Password libraries; it only knows +how to run `op`. When resolution fails, `mdd` distinguishes "not installed" +from "signed out" so the error tells you which one you have. + +> [!IMPORTANT] +> `mdd` does **not** interpolate environment variables into config files. +> There is no `${CONFLUENCE_TOKEN}` support, and no `.env` file loading. +> A value that is not an `op://` reference is used literally, so pasting a +> raw token into YAML does work — and it puts the token in a file, which is +> the thing this design exists to prevent. Do not do it. + +A handful of environment variables affect behavior, none of them secrets: +`MDD_OP_ACCOUNT` for the 1Password account, `MDD_LOG_LEVEL` for verbosity, +`NO_COLOR` and `FORCE_COLOR` for `mdd search` output, and `CLAUDE_HOME` for +where `mdd skills install` puts its symlinks. + +## Before your first sync + +Work through this list once. Most first-run failures are on it. + +1. **`op` is installed and unlocked.** Run any `mdd confluence` command; if + the token cannot resolve, `mdd` tells you whether `op` is missing or + locked. +2. **`mdd confluence whoami` succeeds** and prints the account you expect. + Wrong account here means every push is attributed to it. +3. **The account has the permissions you think it has**, in the space you + are about to sync, and no more. A token inherits everything its user can + do. +4. **You are running from the right directory.** `./configs/confluence.yaml` + beats `~/.config/mdd/confluence.yaml`. Check which file is winning + before you assume which tenant you are pointed at. +5. **The mirror directory is a git repository, and it is clean.** Sync + refuses to run on a dirty tree, and that check is the only thing between + your uncommitted edits and a sync commit. It silently passes if the + directory is not a repository at all. +6. **`configs/data-protection.yaml` says what you mean.** The copy shipped + in this repository is a template with an empty Confluence list. Read + [Safety](04-safety.md) for what the blacklist covers in this + distribution, which is less than you might assume. +7. **You have run `--dry-run` first.** Both sync commands support it and it + prints the whole plan. + +[S07](../spec/S07-data-protection.md) is the design record for the +credential and blacklist rules. diff --git a/docs/guide/06-commands.md b/docs/guide/06-commands.md new file mode 100644 index 0000000..c6c0709 --- /dev/null +++ b/docs/guide/06-commands.md @@ -0,0 +1,132 @@ +# Commands overview + +This page is a map, not a reference. It groups the command tree by what you are +trying to do and says when to reach for each group. It does not list flags. + +`mdd help` is the authority on what exists, and `mdd --help` on what +each one accepts. A generated per-command reference is planned but not +published yet, so where this page and `--help` disagree, believe `--help`. + +```bash +mdd help +mdd convert --help +mdd confluence sync-space --help +``` + +## Convert documents to Markdown + +`mdd convert` reads `.docx`, `.pptx` and `.pdf` and writes a Markdown sibling +next to each one, or into a separate tree. Reach for it when you have a folder +of Office files and want text you can diff, grep and hand to an agent. + +It is incremental: a file whose Markdown is already newer is skipped. The first +run downloads Docling's models, around 500 MB. + +Conversion is lossy in the direction of Markdown. See the round trip in the +[Quickstart](02-quickstart.md) for what that looks like in practice. + +## Author new documents + +`mdd new`, `mdd new-pptx` and `mdd new-docx` scaffold a Quarto project that +renders to Word, PowerPoint, or both. Reach for these when the deliverable has +to arrive as an Office file but you would rather write Markdown. + +`mdd pdf` exports the rendered Office files to PDF, and `mdd pdf-pptx` and +`mdd pdf-docx` do one format each. All three drive Microsoft Office through +AppleScript, so they need macOS with Office installed. + +## Mirror a Confluence space + +`mdd confluence` is the largest group and the one that can do the most damage. + +`mdd confluence sync-space` is the main verb: it reconciles a whole space with +a local git mirror in both directions. `mdd confluence export-page` pulls a +single page and writes nothing back, which makes it the right first command +against an unfamiliar space. + +Pushing individual pages is `mdd confluence create-page` and +`mdd confluence update-page`. Structural changes to a page — title, parent, +archived state — are `mdd confluence rename-page`, `mdd confluence move-page`, +`mdd confluence archive-page` and `mdd confluence unarchive-page`; each one +mutates Confluence and then replays the change in the mirror so the two stay in +step. + +`mdd confluence whoami` prints the account your credentials resolve to. Run it +first. It answers "whose name will appear on every edit", which is the question +you want answered before rather than after. + +Start at [Your first Confluence sync](07-confluence-first-sync.md). + +## Mirror a SharePoint site + +`mdd sharepoint` never calls the SharePoint API. It works through the OneDrive +folder that the sync client already keeps on your disk, so writes reach +SharePoint by way of OneDrive rather than over the network. + +`mdd sharepoint list-sites` shows what OneDrive has synced locally and writes +nothing. `mdd sharepoint sync-site` syncs one of those sites with a Markdown +mirror. `mdd sharepoint sync-folder` does the same for any local folder, which +is the escape hatch when the content you want is not laid out as a site. + +Start at [Your first SharePoint sync](08-sharepoint-first-sync.md). + +## Search + +`mdd search` wraps ripgrep over every mirror root in your configuration, so one +query covers all your Confluence spaces, SharePoint sites and docs repositories +at once. It needs `rg` installed. There is no network call, no API token and no +model behind it. + +Use `--json` when a script or an agent is reading the output, and `--include` +to search a directory that is not in your configuration. + +## AI assistance + +`mdd ai` needs an API token for a LiteLLM gateway. Nothing else in `mdd` calls +a model. + +`mdd ai rewrite` rewrites a Markdown file for clarity and tone. `mdd ai index` +walks a directory and proposes an `INDEX.md` of per-page summaries, printing it +rather than writing it until you pass `--apply`. `mdd ai review` looks across a +whole mirror for duplicate pages, contradictory claims and stale content, and +writes a report for a human to act on. + +Two things to keep in mind. These commands send document content to whatever +gateway you configured, so the content leaves your machine. And a model can be +wrong: `mdd ai rewrite` writes to `.rewrite.md` by default so you can +diff before you accept anything. + +## Agent skills + +`mdd skills` manages the Claude Code skills bundled with `mdd`. Installing them +symlinks the bundle into your skills directory so an agent knows when and how +to drive these commands. `mdd skills list` shows what is available and what is +installed; `mdd skills uninstall` removes only the symlinks `mdd` created. + +This group touches your skills directory and nothing else. + +## What writes to a remote system + +Everything above is local except the commands in this table. Each of these can +change content other people depend on. + +| Command | Writes to | +|---|---| +| `mdd confluence sync-space` | Confluence, unless you pass `--read-only` or `--dry-run` | +| `mdd confluence create-page` | Confluence | +| `mdd confluence update-page` | Confluence | +| `mdd confluence rename-page` | Confluence | +| `mdd confluence move-page` | Confluence | +| `mdd confluence archive-page` | Confluence | +| `mdd confluence unarchive-page` | Confluence | +| `mdd sharepoint sync-site` | the local OneDrive folder, which OneDrive uploads to SharePoint | +| `mdd sharepoint sync-folder` | the same, for an arbitrary folder | + +Two more cases that are not document writes but still leave your machine. The +`--push` flag on the sync commands pushes the mirror's git repository to its +remote. And every `mdd ai` subcommand sends content to your configured gateway. + +> [!CAUTION] +> `mdd` is beta software and these commands write to production by default. +> [Safety](04-safety.md) covers what each one can destroy, and which flags hold +> it back. diff --git a/docs/guide/07-confluence-first-sync.md b/docs/guide/07-confluence-first-sync.md new file mode 100644 index 0000000..3231141 --- /dev/null +++ b/docs/guide/07-confluence-first-sync.md @@ -0,0 +1,244 @@ +# Your first Confluence sync + +This page assumes you bring your own Confluence Cloud tenant. There is no +shared demo space, and there cannot be one: the interesting half of `mdd` +writes, and nobody can hand strangers write access to a Confluence space. + +The order below is deliberate. Every step before the last one is read-only. +Do not skip ahead. + +> [!CAUTION] +> `mdd confluence sync-space` pushes local changes to Confluence without +> asking for confirmation. Read [Safety](04-safety.md) before you run it +> without `--read-only`. + +## 1. Get a space you are allowed to break + +Create a new Confluence space, or take one nobody depends on. You want a +space where you can push a bad body, rename a page by accident, and archive +something, without explaining yourself afterwards. + +Put a handful of pages in it, with at least one nested page and one image, +so the mirror has some structure to look at. If your organization does not +let you create spaces, a personal space works. + +Do not start on a space that other automation publishes into. `mdd` will +refuse to push to those pages, which is correct behavior but a confusing +first experience. + +## 2. Configure a token + +Create an Atlassian API token for your account, store it in 1Password, and +reference it from a config file. [Configuration and +secrets](05-configuration.md) has the full shape; the short version is: + +```yaml +# configs/confluence.yaml +confluence: + url: https://your-instance.atlassian.net + username: you@example.com + api_token: op://Employee/confluence-pat/token +``` + +Confirm it before you do anything else: + +```bash +mdd confluence whoami +``` + +That prints the account `mdd` authenticated as, and compares it against +any configured external publishers. If it prints a different account, +stop and fix that: every page version you create will be attributed to it. + +## 3. Export one page, read-only + +Pick a single page and export it. Pass the page URL or its numeric id. + +```bash +mdd confluence export-page https://your-instance.atlassian.net/wiki/spaces/SCRATCH/pages/12345/Some+Page --output ./scratch +``` + +This reads and writes nothing to Confluence. It produces one `.md` file +named after the page title, plus a `-attachments/` directory if +the page has images. Add `--no-attachments` for a faster first look. + +Now open the file. This is the most useful five minutes in this guide. + +You will see a YAML frontmatter block, an export callout, and the body: + +```markdown +--- +confluence: + url: https://markdown.atlassian.net/spaces/MDD/pages/164122/Platform+glossary + page_id: '164122' + space_key: MDD + space_id: '131077' + parent_id: '131185' + title: Platform glossary + status: current + version: 2 + created_at: '2026-05-11T20:55:02.191Z' + created_by: + account_id: 557058:738d4176-8fd3-4b84-92d8-245731e9dfd9 + display_name: Leo Simons + updated_at: '2026-05-11T20:55:03.086Z' + labels: [] + exported_at: '2026-05-11T20:55:03.455150+00:00' + source_format: storage + attachments: [] +--- + +# Platform glossary + +A short glossary of terms used inside Acme's platform team. +``` + +That block is real: it is the head of +`tests/corpus/confluence/corpus/synth-glossary.md`, a page committed to +this repository as round-trip test data. Every mirrored file looks like it. + +Three fields matter. `page_id` is identity and must not be edited. +`version` is the conflict check. `title` is metadata, but the page title +`mdd` actually pushes comes from the first `# H1` in the body, so treat the +H1 as the title. + +Have a look at the corpus while you are here. `tests/corpus/confluence/` +in this repository holds fixtures for every shape `mdd` handles, one shape +per file, all of them exported from a live space. If you want to know what +a Confluence layout, a merged table cell or an unrecognized macro looks +like after export, it is in there. + +## 4. Pull the whole space, still read-only + +Make an empty directory, make it a git repository, and sync into it. Plan +first: + +```bash +mkdir -p ~/mirrors/SCRATCH && cd ~/mirrors/SCRATCH && git init +mdd confluence sync-space SCRATCH --output ~/mirrors/SCRATCH --read-only --dry-run +``` + +`--dry-run` computes the whole plan and prints the summary without touching +files, git, or Confluence. Read it. Every page in the space should classify +as new. + +Then run it for real, keeping `--read-only`: + +```bash +mdd confluence sync-space SCRATCH --output ~/mirrors/SCRATCH --read-only +``` + +`--read-only` suppresses every write to Confluence: no page creation, no +body push, no Office publishing. The local mirror updates and `mdd` makes +one git commit for the run, so you can read `git show` to see exactly what +it did. + +If the space is large, `--head 5` caps the run at five pages, and +`--no-attachments` skips the downloads. + +Look at the layout it produced. Pages are `.md` files; a page with children +also gets a directory of the same name holding them; Confluence folders +become directories with no `.md`. [Concepts](03-concepts.md) has a diagram. + +Run the same command again. It should report nothing to sync and make no +commit. Confirm that a second run is a no-op before you trust it with +writes. + +## 5. Look at a push before you make one + +Now edit one page in the mirror. Change a sentence, not the H1, in a page +you do not care about. Then: + +```bash +mdd confluence update-page "Some Page.md" --dry-run +``` + +That fetches the live page, renders your Markdown to Confluence storage +format, and prints a unified diff of the two. It pushes nothing. + +Read the diff carefully the first time. You are looking for two things: +that your change appears, and that nothing *else* does. Spurious diff hunks +in parts of the page you did not touch mean the round-trip is not clean for +that page, and pushing would rewrite content you did not intend to. + +When the diff is what you expect, drop `--dry-run`: + +```bash +mdd confluence update-page "Some Page.md" +``` + +It prints the diff again and asks before pushing. Answer `y`. Add +`--message "..."` to set the comment that shows up in the page's version +history; it costs nothing and makes the history readable later. + +## 6. Then the rest of the write path + +Once a single-page push works, the other write operations follow the same +shape: `--dry-run` first, then a confirmation prompt. + +```bash +mdd confluence create-page ./new-page.md --space SCRATCH +mdd confluence rename-page "Some Page.md" "A better title" --dry-run +mdd confluence move-page "Some Page.md" --parent 12345 --dry-run +mdd confluence archive-page "Old Page.md" --dry-run +``` + +`create-page` is the exception with no prompt. Give it a Markdown file +whose frontmatter has no `page_id`, and it creates the page and writes the +full metadata back into your file. + +Full bidirectional sync is last: + +```bash +mdd confluence sync-space SCRATCH --output ~/mirrors/SCRATCH --dry-run +mdd confluence sync-space SCRATCH --output ~/mirrors/SCRATCH +``` + +Without `--read-only`, that run creates pages from local files carrying a +`space_key` and no `page_id`, pushes local edits back, and does it all +without prompting. Read the dry-run plan every time until you trust it. + +## Failure modes you will actually hit + +**The token will not resolve.** `mdd` tells you whether `op` is missing or +signed out. If you are signed in to more than one 1Password account, pin +the account in config or set `MDD_OP_ACCOUNT`. + +**The page URL host does not match config.** This is a hard error by +design, so a copied URL cannot send a command at the wrong tenant. Check +`confluence.url`. + +**"Mirror has uncommitted changes."** Sync refuses to run on a dirty tree +and there is no override. Commit or stash. If you get this and believe the +tree is clean, you are in a different directory than you think. + +**Sync cannot work out where to write.** Without `--output`, `mdd` only +defaults to the current directory when its `origin` URL names the space. +Otherwise it stops and asks for `--output`. + +**A page is reported as a conflict and skipped.** Both sides changed. `mdd` +does not merge: it skips the push and the pull for that page. Export the +page fresh somewhere else, reconcile by hand, then push. + +**A page is skipped as managed elsewhere.** Another system publishes it. +There is no override flag; the fix is to edit that page at its source. The +run still exits 0, because this is expected steady state, not a failure. + +**The push is refused as empty or too small.** Two guards abort a push +whose body is empty or under 10% of the live page's length. They are +there to override with `--allow-empty` and `--allow-shrink`. If you reach for +either one on a real page, stop and work out what happened to your content +first. + +**Two images with the same filename.** Confluence keys attachments by +filename within a page, so two local files sharing a basename is a hard +error. Rename one. + +**The page title changed and you did not mean it.** The first `# H1` in the +file is the title `mdd` pushes. Restore the heading and push again; +Confluence keeps the old title in page history. + +[S09](../spec/S09-confluence-command.md) and +[S14](../spec/S14-confluence-sync.md) are the design record for these +commands, and [S27](../spec/S27-confluence-page-rename-move-archive.md) +covers rename, move and archive. diff --git a/docs/guide/08-sharepoint-first-sync.md b/docs/guide/08-sharepoint-first-sync.md new file mode 100644 index 0000000..104aa08 --- /dev/null +++ b/docs/guide/08-sharepoint-first-sync.md @@ -0,0 +1,214 @@ +# Your first SharePoint sync + +SharePoint is not Confluence with different nouns. Read this section before +you run anything; almost every prerequisite and failure mode differs. + +## How this is different + +`mdd` never calls a SharePoint or Microsoft Graph API. It reads the folder +your OneDrive client has already synced to your disk, and writes back into +that same folder. OneDrive does the uploading. + +Four consequences follow: + +- **There are no credentials to configure.** Your OneDrive client holds the + authentication. `mdd` sees a directory. +- **macOS only.** The sync root is discovered under + `~/Library/CloudStorage/`. Windows OneDrive uses a different path shape + and there is no first-party Linux client. +- **A write is not local.** Overwriting a `.docx` in the OneDrive folder + publishes a new version to SharePoint for everyone, seconds later. The + absence of an API does not make this safer. +- **The unit of content is a binary Office file**, not a page. `mdd` + converts `.docx`, `.pptx` and `.pdf` to Markdown with Docling, and + renders Markdown back to Office with Quarto. Both directions lose + information in ways a page body does not. + +That last point drives the whole design. The Markdown-to-Office render +loses shapes, custom styles and inline metadata, so `mdd` refuses to do it +by default. Every file pair carries an `update_office` flag in frontmatter, +which starts as `false` for anything that came from an Office file. Until +you flip it, Markdown edits are ignored and reported as skipped. + +## 1. Get a site you are allowed to break + +Create a SharePoint site, or a document library in one, that nobody +depends on. Put a couple of Word documents in it, ideally with an image and +a table, so the conversion has something to chew on. + +Check the blacklist before you start. `mdd sharepoint` calls the +confidentiality check at the start of every run, before it converts +anything, so a site whose name matches an entry in +`configs/data-protection.yaml` aborts the whole command rather than only +the push. The shipped file blocks names like `Board`, `Governance`, +`Appraisal*` and `Customer-*`. Pick a scratch site name that does not +collide with one. + +## 2. Sync the site into OneDrive + +In the browser, open the document library and choose **Sync**. Then, in +Finder, right-click the synced folder and enable **Always Keep on This +Device**. + +That second step is not optional. Without it, OneDrive leaves cloud-only +placeholder stubs on disk, and `mdd` walks a tree of files that have no +content in them. + +Wait for the sync to finish before continuing. + +## 3. Confirm `mdd` can see it + +```bash +mdd sharepoint list-sites +``` + +This is read-only and touches nothing. For each folder it can sync, it prints +the site name, the folder name it came from, whether the folder is a whole +document library or a sub-folder, the repository name it would use, and +whether the blacklist allows it. + +Two things to check in that output. + +**The site name.** A whole library appears as ` - Documents` in +OneDrive, and `mdd` strips the suffix. A sub-folder sync appears under the +sub-folder's own name, which is not the site name at all. The name shown +here is what every other command and the blacklist match against. + +**The repository name.** Sites with no entry in +`configs/sharepoint-mapping.yaml` fall back to a default rule and are +flagged. If the derived name matters to you, pin it in the mapping file. + +If the command reports that it cannot find a sync root, and you have more +than one tenant synced, the discovery is ambiguous. Use +`mdd sharepoint sync-folder ` against the folder directly. + +## 4. Sync read-only into a mirror + +Create an empty git repository for the mirror. Plan the run first: + +```bash +mkdir -p ~/mirrors/scratch-site && cd ~/mirrors/scratch-site && git init +mdd sharepoint sync-site "Scratch Site" --output ~/mirrors/scratch-site --read-only --dry-run +``` + +`--dry-run` prints the plan and writes nothing. `--read-only` suppresses +every write back into the OneDrive folder: no Markdown-to-Office render, +and no divergence candidate. The Office-to-Markdown half and the local git +commit still run. + +Then run it for real, still read-only: + +```bash +mdd sharepoint sync-site "Scratch Site" --output ~/mirrors/scratch-site --read-only +``` + +`--head 5` caps the run while you are getting your bearings. The first run +of any conversion downloads Docling's models, roughly 500 MB into +`~/.cache/docling/`, so give it time. + +The summary line counts every category: first syncs in each direction, +conversions each way, divergences, skips for `update_office: false`, skips +for `--read-only`, ignored paths, Word-locked files, corrupt files, no-ops +and errors. Read it. It is the fastest way to see that something you +expected to happen did not. + +## 5. Look at what it produced + +The mirror copies the source directory structure. Each `Foo.docx` gets a +`Foo.docx.md` sibling with extracted images in `Foo.docx-attachments/`. +A PDF converts to `Foo.pdf.md` one way only. Plain `.md` files are copied +through. Spreadsheets and standalone image files are skipped by design, +the latter because they are usually already embedded in a document. + +Every generated file carries a `sharepoint:` frontmatter block and a +"SharePoint export" callout naming the source path. The interesting part is +the `sync` sub-block: + +```yaml +sharepoint: + site: Scratch Site + source_path: Handbook/Onboarding.docx + converter: docling-docx + sync: + office_sha256_at_sync: 9f86d081... + md_sha256_at_sync: e3b0c442... + last_sync: 2026-05-08T10:30:00Z + update_office: false +``` + +Those two hashes are how divergence is detected: `mdd` re-hashes both sides +on every run and compares them against what it recorded. `update_office: +false` is the gate described above. + +Read one of the converted files next to the original in Word. Conversion +quality is the thing that decides whether this workflow is useful for your +content, and no amount of documentation substitutes for looking. + +## 6. Only then, the write path + +To let Markdown edits flow back into an Office file, set +`update_office: true` in that file's frontmatter. It is per pair and +deliberate. + +```bash +mdd sharepoint sync-site "Scratch Site" --output ~/mirrors/scratch-site --dry-run +mdd sharepoint sync-site "Scratch Site" --output ~/mirrors/scratch-site --backup +``` + +Without `--read-only`, that run renders your Markdown to `.docx` through +Quarto and overwrites the file in the OneDrive folder. There is no +confirmation prompt. `--backup` copies the previous Office file to +`.mdd-backups//-` first; it is off by default, and +for your first write it is worth the disk. + +A Markdown file in the tree with no Office sibling is treated as a publish +candidate: `mdd` renders it to `.docx` and stamps `update_office: true`, +because in that direction Markdown was authoritative from the start. + +`mdd sharepoint sync-folder --output ` runs the same engine +against any local folder. It has no `--read-only` flag, and it requires +`--output`. + +## Failure modes you will actually hit + +**Cloud-only files.** If "Always Keep on This Device" is off, files on disk +are stubs. Turn it on and let OneDrive finish. + +**Multiple tenants synced.** Sync-root discovery gives up rather than +guessing. Use `sync-folder` with an explicit path. + +**"Mirror has uncommitted changes."** Sync refuses on a dirty output tree, +with no override. Commit or stash. + +**The blacklist blocked the run.** The site name matched an entry. The +match is on the name OneDrive shows, which for a sub-folder sync is the +sub-folder name. `mdd sharepoint list-sites` shows you what is being +matched. + +**A pair reports `DIVERGED`.** Both the `.docx` and the `.md` changed since +the last sync, and `update_office` was `true`. `mdd` writes +`Foo.from-md.docx` next to the original and touches neither source. Open +both in Word, port the changes by hand, delete the candidate, and re-run. +Until the candidate is gone, every run repeats the same message. + +**Markdown edits are silently ignored.** They are not silent, but they are +easy to miss: the summary counts them under `skipped(update_office=False)`. +Flip the flag in that file's frontmatter to opt the pair in. + +**"skipped: file open in Word."** A `~$Foo.docx` lock file exists. Close +Word and re-run. + +**Quarto is missing.** Markdown-to-Office rendering needs it. Install +Quarto, or stay read-only. + +**The first run takes forever.** Docling downloads models once. Later runs +reuse the cache. + +**A file you did not want got converted.** Add a `.mddignore` at the mirror +root with gitignore-style patterns. It filters the source side before +conversion, so it also saves the time. It does not delete anything already +synced unless you ask for that explicitly. + +[S10](../spec/S10-sharepoint-command.md) and +[S18](../spec/S18-sharepoint-sync.md) are the design record for this +command, including the full reconciliation table. diff --git a/docs/research/000-research.md b/docs/research/000-research.md index 68d34cd..6be1d73 100644 --- a/docs/research/000-research.md +++ b/docs/research/000-research.md @@ -31,6 +31,7 @@ as "note 007" in prose; `R07` and `007` are the same note. | R12 | [IR foundation comparison and recommendation](R12-confluence-ir-comparison.md) | Cross-pipeline comparison across all spikes, and the recommendation that became [S28](../spec/S28-document-ir-foundation.md). | | R13 | [IR spike: pure-Python pipeline](R13-confluence-ir-spike-pure-python.md) | Measurements for a first-party typed IR with no external converter — the option the recommendation ultimately landed on. | | R14 | [Documentation strategy and a docs site for mdd](R14-documentation-strategy-and-site.md) | Audience framing (Diátaxis plus an operator/integrator/contributor/agent split), a generated-vs-executed-vs-checked-vs-prose freshness model, and a July 2026 tooling survey: Starlight vs the Material for MkDocs/Zensical transition, publishing `docs/` into the site, CLI reference from `mdd help --json`, griffe for a curated extension API, import-linter for architecture contracts, Sybil for executable examples, llms.txt, and Vale. | +| R15 | [AI-tell detection, and how to evaluate a prose linter](R15-ai-tell-detection-and-prose-evaluation.md) | Measurements for the `vale-ai-tells` package over this corpus, why an alert-count comparison cannot judge it when the whole baseline is AI-written, and a proposed preference-elicitation method: minimal rewrite to satisfy a rule, then human side-by-side judgement attributed back to the rule. | ## Provenance diff --git a/docs/research/R06-confluence-bidirectional-sync-ir.md b/docs/research/R06-confluence-bidirectional-sync-ir.md index 484df9d..e517990 100644 --- a/docs/research/R06-confluence-bidirectional-sync-ir.md +++ b/docs/research/R06-confluence-bidirectional-sync-ir.md @@ -12,8 +12,8 @@ user can resolve them, and the resolution UX today is "go fix it manually." We need real bidirectional merge. Round-tripping a real, macro-heavy Confluence page through -[`storage_to_md.py`](../../src/mdd/confluence/storage_to_md.py) and -[`md_to_storage.py`](../../src/mdd/confluence/md_to_storage.py) made +[`storage_to_md.py`](../../src/mdd/confluence/ir/reader.py) and +[`md_to_storage.py`](../../src/mdd/confluence/ir/writer/) made the deeper problem visible: those two converters share no intermediate representation. They each independently encode/decode between Confluence storage XHTML and markdown, with `{=confluence}` @@ -470,20 +470,20 @@ layout, and migration path. Reading list when the spikes start, in rough order of importance: -- [`src/mdd/confluence/storage_to_md.py`](../../src/mdd/confluence/storage_to_md.py) +- [`src/mdd/confluence/storage_to_md.py`](../../src/mdd/confluence/ir/reader.py) — current XHTML → markdown converter; the Python parser logic is reusable in spikes B and C. -- [`src/mdd/confluence/md_to_storage.py`](../../src/mdd/confluence/md_to_storage.py) +- [`src/mdd/confluence/md_to_storage.py`](../../src/mdd/confluence/ir/writer/) — current markdown → XHTML converter; gets replaced wholesale in spikes B and C. -- [`src/mdd/confluence/sync.py`](../../src/mdd/confluence/sync.py) +- [`src/mdd/confluence/sync.py`](../../src/mdd/confluence/sync/) — the orchestrator with the existing `LOCAL_PUSH` event class that any merge engine will plug into. - [`src/mdd/confluence/state.py`](../../src/mdd/confluence/state.py) — `LocalPage` model; will need fields for cache pointers (storage hash, export_view hash, provenance path) when we ship. -- [`src/mdd/confluence/client.py`](../../src/mdd/confluence/client.py) +- [`src/mdd/confluence/client.py`](../../src/mdd/confluence/client/) — currently fetches `body-format=storage` only. Will need a method for `body-format=export_view` when we want rendered HTML for conflict UX. diff --git a/docs/research/R09-confluence-ir-spike-status-quo.md b/docs/research/R09-confluence-ir-spike-status-quo.md index 6292801..d7690f2 100644 --- a/docs/research/R09-confluence-ir-spike-status-quo.md +++ b/docs/research/R09-confluence-ir-spike-status-quo.md @@ -22,7 +22,7 @@ measurement of the baseline. ## Harness and corpus state -- Harness: [scripts/ir_experiment/](../../scripts/ir_experiment/) at +- Harness: `scripts/ir_experiment/` at commit landing this note. - Corpus: `test-confluence/MDD/_snapshots/`, 35 captured pages (`mise run refresh-corpus` run on 2026-05-12 against the same @@ -374,7 +374,7 @@ narrative trivial to extract. specification. - [Research note 008](R08-confluence-ir-experiment-harness.md) — the harness that produced this report. -- [`scripts/ir_experiment/`](../../scripts/ir_experiment/) — the +- `scripts/ir_experiment/` — the harness code itself. ## Next research note diff --git a/docs/research/R10-confluence-ir-spike-pandoc-lua.md b/docs/research/R10-confluence-ir-spike-pandoc-lua.md index b718aef..44d643e 100644 --- a/docs/research/R10-confluence-ir-spike-pandoc-lua.md +++ b/docs/research/R10-confluence-ir-spike-pandoc-lua.md @@ -26,7 +26,7 @@ gets us out of the gate. ## Harness and corpus state -- Harness: [scripts/ir_experiment/](../../scripts/ir_experiment/) at +- Harness: `scripts/ir_experiment/` at commit landing this note. Same as notes 009 + 011 plus the `pipelines/pandoc_lua.py` adaptor and the `pipelines/confluence_storage.lua` writer. @@ -392,9 +392,9 @@ definition. status-quo baseline. - [Research note 011](R11-confluence-ir-spike-docling.md) — docling spike, the other contender. -- [`scripts/ir_experiment/pipelines/pandoc_lua.py`](../../scripts/ir_experiment/pipelines/pandoc_lua.py) — +- `scripts/ir_experiment/pipelines/pandoc_lua.py` — Python adaptor. -- [`scripts/ir_experiment/pipelines/confluence_storage.lua`](../../scripts/ir_experiment/pipelines/confluence_storage.lua) — +- `scripts/ir_experiment/pipelines/confluence_storage.lua` — custom Lua writer. - HTML report: the harness also emitted a side-by-side HTML diff report with the same data plus the timing slowdown table. It is diff --git a/docs/research/R11-confluence-ir-spike-docling.md b/docs/research/R11-confluence-ir-spike-docling.md index 1b6d372..87eb8ab 100644 --- a/docs/research/R11-confluence-ir-spike-docling.md +++ b/docs/research/R11-confluence-ir-spike-docling.md @@ -24,7 +24,7 @@ to bolt Confluence semantics on top?" ## Harness and corpus state -- Harness: [scripts/ir_experiment/](../../scripts/ir_experiment/) at +- Harness: `scripts/ir_experiment/` at commit landing this note. Same as note 009 plus the `pipelines/docling.py` adapter and wall-clock timing instrumentation. - Corpus: `test-confluence/MDD/_snapshots/`, the same 35 captured @@ -360,7 +360,7 @@ real maintenance cost. the harness that produced this report. - [Research note 009](R09-confluence-ir-spike-status-quo.md) — status-quo baseline, directly comparable numbers. -- [`scripts/ir_experiment/pipelines/docling.py`](../../scripts/ir_experiment/pipelines/docling.py) — +- `scripts/ir_experiment/pipelines/docling.py` — the pipeline adaptor. - HTML report: the harness also emitted a side-by-side HTML diff report with the same data plus the timing slowdown table. It is diff --git a/docs/research/R12-confluence-ir-comparison.md b/docs/research/R12-confluence-ir-comparison.md index 0420f16..bac363b 100644 --- a/docs/research/R12-confluence-ir-comparison.md +++ b/docs/research/R12-confluence-ir-comparison.md @@ -452,7 +452,7 @@ provenance follow-ups are already done. docling spike. - [Research note 013](R13-confluence-ir-spike-pure-python.md) — pure-Python IR spike (the revised recommendation). -- [`scripts/ir_experiment/`](../../scripts/ir_experiment/) — the +- `scripts/ir_experiment/` — the harness and pipelines used in this comparison. - HTML reports: the harness also emitted side-by-side HTML diff reports per spike. They are not carried in this repository — the diff --git a/docs/research/R13-confluence-ir-spike-pure-python.md b/docs/research/R13-confluence-ir-spike-pure-python.md index 7e01aca..170950f 100644 --- a/docs/research/R13-confluence-ir-spike-pure-python.md +++ b/docs/research/R13-confluence-ir-spike-pure-python.md @@ -402,9 +402,9 @@ Truncated to non-perfect rows; the other 27 fixtures all score docling spike. - [Research note 012](R12-confluence-ir-comparison.md) — the comparison and (now revised) recommendation. -- [`scripts/ir_experiment/pipelines/pure_python/`](../../scripts/ir_experiment/pipelines/pure_python/) +- `scripts/ir_experiment/pipelines/pure_python/` — the pipeline implementation. -- [`tests/ir_experiment/test_pure_python_pipeline.py`](../../tests/ir_experiment/test_pure_python_pipeline.py) +- `tests/ir_experiment/test_pure_python_pipeline.py` — JSON round-trip and metric-threshold tests. - HTML report: the harness also emitted an HTML diff report with the same data plus per-fixture diffs and the timing slowdown table. It diff --git a/docs/research/R15-ai-tell-detection-and-prose-evaluation.md b/docs/research/R15-ai-tell-detection-and-prose-evaluation.md new file mode 100644 index 0000000..ac310d1 --- /dev/null +++ b/docs/research/R15-ai-tell-detection-and-prose-evaluation.md @@ -0,0 +1,242 @@ +# 015 — AI-tell detection, and how to evaluate a prose linter + +**Status:** Open. Proposes an evaluation method; no adoption decision yet. + +This note records a first evaluation of the `vale-ai-tells` Vale package +against `mdd`'s prose, explains why that evaluation could not answer the +question it was asked, and proposes a preference-elicitation method that can. + +The immediate trigger is [S06](../spec/S06-documentation-site.md), which added +a Vale gate over the documentation site's prose and deferred a rule-set +review until there was content to judge against. The larger question is +narrower and harder than "which style package": almost all of this +repository's prose is written by AI agents under human review, so a +deterministic check against AI-slop tells is a check on the dominant +production process, not a spot check on an occasional contributor. + +## The package + +[`tbhb/vale-ai-tells`](https://github.com/tbhb/vale-ai-tells), MIT licensed, +actively maintained — around two tagged releases a week since March 2026, at +`v1.26.0` as of this evaluation. It ships three artifacts: `ai-tells` +(77 rules, all at `level: error`), `ai-tells-experimental` (17 rules, mostly +Tengo scripts doing structural analysis), and `ai-tells-commits` (13, scoped +to commit messages). 74 of the 77 core rules are `existence` rules — token +and regex lists. + +Pinned release URLs work in `Packages =` exactly as the `write-good` v0.4.1 +pin already does, and the synced styles directory would be gitignored the same +way, so nothing is redistributed and the licence question does not arise. + +## What the first evaluation measured + +Two agents ran the package over three corpora and over the four synthesised +articles drafted for the documentation site. Whole package, all 77 rules: + +| Corpus | Words | Files | Alerts | per 1k | +|---|---:|---:|---:|---:| +| `docs/guide/` | 10,515 | 9 | 69 | 6.6 | +| `docs/spec/` + `docs/research/` | 98,261 | 54 | 1,682 | 17.1 | +| `README` + `CONTRIBUTING` + `AGENTS` | 1,619 | 3 | 18 | 11.1 | + +42 of the 77 rules never fired on any corpus. A curated 36-rule subset — the +never-fired rules plus the low-noise ones — produced 3 alerts on the guide, 22 +on the design record, and 0 on the root files, and all 3 guide alerts were +false positives. Precision over the four articles was about 4%: roughly 118 +hits, roughly 5 worth acting on. + +Both agents independently recommended rejecting the same rules. Some of those +rejections are clearly right on the evidence: + +- **`FormalRegister`** — 107 hits, 92 of which are the word `implement` or + `implementation`. There is no plainer synonym in this domain. +- **`VerbTricolon`** — its regex treats any word ending in *s* as a verb, so + it fires on noun lists and on colon-plus-comma enumerations of numbers. + Near-total false positive. +- **`ShipOveruse`** — "ships with `mdd`" means *is in the production tree*, + which is the exact distinction the sentences using it exist to draw. + +Two other findings are worth keeping regardless of what happens next. + +**The tone words are better served by a custom style.** The package covers +only 2 of the 5 words this project already catches by hand (`seamlessly` and +`robust`); `simply`, `easily` and `powerful` are absent as bare tokens. And +the rule carrying those two, `OverusedVocabulary`, also bans `comprehensive`, +`crucial`, `significant`, `genuine` and `actionable`, which this corpus uses +correctly. A hand-written 35-token style caught all 9 tone markers in a +synthetic slop page and produced 9 hits across 110k words of real prose — four +of them being [R14](R14-documentation-strategy-and-site.md)'s own sentence +listing the banned words. Write that style regardless. + +**The structural rules found something the lexical rules could not.** +`ai-tells-experimental.ContractionAvoidance` fires on 8 of 9 guide pages. +Contractions per 1000 words across the repository: + +| Group | Words | Contractions | per 1k | +|---|---:|---:|---:| +| Articles (4) | 6,473 | 1 | 0.15 | +| `docs/guide/` (9) | 9,730 | 0 | 0.00 | +| `README` + `CONTRIBUTING` + `AGENTS` | 1,524 | 0 | 0.00 | +| `docs/spec/` | 50,577 | 65 | 1.29 | +| `docs/research/` | 39,338 | 144 | 3.66 | + +One contraction in 6,473 words is not a style choice; it is the absence of +one. Whether that matters is a house-voice question, but it is a deterministic +finding that human review did not produce, which is the reason to want a tool +like this at all. + +## Why the evaluation could not answer the question + +The first evaluation's central argument was comparative. It observed that the +package flags `AGENTS.md` (20.2 alerts/1k) and +[S07](../spec/S07-data-protection.md) (19.8/1k) harder than three of the four +agent-drafted articles, and that on ex-punctuation counts +[S06](../spec/S06-documentation-site.md) (8.2) beats two of them. It concluded +that a tool ranking the maintainer's prose above the agents' prose is not +measuring authorship, and used that to justify keeping several rules off. + +**That argument rests on a false premise.** Very little of this repository's +prose was written by a human unaided. The specs, the research notes, `AGENTS.md` +and the README are AI-written or AI-assisted, the same as the guide pages and +the articles. There is no human-written control group in the corpus, so +"it flags your prose harder than the agents'" is not evidence of miscalibration. +It is equally consistent with the package working correctly and the entire +baseline being the thing the package detects. + +Every conclusion in the first evaluation that leans on the corpus as a +reference standard inherits this error. The rules rejected on measurement +grounds — `FormalRegister`, `VerbTricolon`, `ShipOveruse` — survive it, because +their failures are demonstrable sentence by sentence. The rules rejected on +*house voice* grounds do not, because house voice is exactly what is in +question. + +Two of those rejections look wrong on the maintainer's own judgment: + +- **`EmDashUsage`.** The single highest-firing rule: 826 hits in the design + record (8.41/1k), 15 in the guide, 12 across the root files. Rejected on the + grounds that em-dashes here are deliberate. They are — and there are also + far too many of them. The rule is crude (it flags every `—` and `–` at error + level with no exemptions, which is unusable as written), but the signal it + is reporting is real. +- **`ContrastiveFormulas`.** Rejected because its bare-appositive tokens fire + on "a heuristic, not a gate" and similar, called "a defining move in this + repo's register". That construction is also over-used, and being a defining + move of an AI-written corpus is not a defence. + +So the two rules the evaluation argued hardest to keep off are two the +maintainer wants flagged. This is not a small correction at the edges; it +inverts the method. Counting alerts against an AI-written baseline measures +how much a corpus resembles itself. + +## What would actually settle it + +The missing ingredient is human preference, elicited on concrete prose rather +than on rule descriptions. A rule is worth adopting if applying it produces +text a human reader prefers. Nothing else is decisive, and nothing measured so +far is a proxy for it. + +Proposed method: + +1. **Fix a rule set under test.** Start with the full 77-rule package plus the + experimental structural rules, minus the rules whose failures are already + demonstrated sentence by sentence. Keep `EmDashUsage`, + `ContrastiveFormulas`, `CataphoricForecasting`, `SemicolonUsage` and the + other register rules **in**. They are the contested cases and the whole + point is to stop adjudicating them by argument. + +2. **Sample passages from existing prose.** Draw from the guide pages, the + four articles, and the design record, weighted toward passages that trigger + contested rules but including untriggered passages as controls. The control + passages matter: without them the exercise only measures whether rewriting + helps where a rule fired, not whether the rule fired in the right places. + +3. **Have an agent produce a minimal rewrite** that clears the findings. + Minimality is the load-bearing constraint — the instruction is to make the + smallest edit that satisfies the rule, not to improve the passage. A + rewrite that improves the prose for unrelated reasons contaminates the + comparison, because then the preference measures the rewriter rather than + the rule. + +4. **Present pairs side by side for human judgement.** Original and rewrite, + order randomised, rule attribution hidden at the point of choice. Record + the preference, and record *why* in a few words, because the reason is what + distinguishes "this rule is wrong" from "this rule is right and the rewrite + was bad". + +5. **Attribute preferences back to rules.** A rule whose rewrites are + preferred is worth adopting. A rule whose rewrites are consistently + rejected is not, and now with evidence rather than assertion. A rule whose + rewrites are preferred only sometimes needs its token list narrowed, which + is the outcome the first evaluation reached for `ContrastiveFormulas` by + inspection and could not confirm. + +### Design questions this raises + +**Unit of comparison.** Sentence, paragraph, or section. Sentences isolate the +rule best but strip the context that makes a construction good or bad — +`CataphoricForecasting`'s hits are individually fine and collectively +monotonous, which a sentence-level comparison cannot see. Paragraph is +probably the right default, with a section-level pass for the rules whose +complaint is about rhythm. + +**Blinding.** Which is original and which is rewritten will often be guessable. +That is tolerable if the preference is honest, and it is a reason to record +reasons rather than only choices. + +**Sample size.** Enough per rule to distinguish a preference from noise, and +the contested rules are the ones that need the most. This is the main cost of +the method and the reason to pick the rule set deliberately in step 1. + +**Who judges.** One judge, which is both the constraint and the point: the +question is what this project's prose should read like, and there is one +person whose answer settles that. + +**Reusability.** If the harness is worth building, it is worth building so a +second package, or a hand-written style, can be run through it later. The +output is a preference dataset, not a verdict on one package. + +## What this is not + +Not an `mdd` feature. Running a prose linter over mirrored content is a +plausible and much larger piece of work with an audience well beyond this +repository, and [S06](../spec/S06-documentation-site.md) already places it out +of scope. This note is about this project's own prose gate. + +Not a decision to adopt or reject the package. The first evaluation's +demonstrated false positives stand, and its two side findings — the custom +tone-word style, and the contraction result — are worth acting on +independently of anything else here. The contested register rules stay +unadjudicated until there is preference data. + +## Open questions + +1. Is the minimal-rewrite constraint achievable in practice, or will an agent + asked to satisfy a rule reliably improve the passage in other ways at the + same time? If it cannot be held, the comparison measures the rewriter and + the method needs a different control. +2. Should the design record be in scope at all? It carries the largest alert + counts and the loosest register, and [S06](../spec/S06-documentation-site.md) + deliberately exempts it from the Vale gate. Including it in the evaluation + while keeping it out of the gate is defensible — the preference data is + about the rules, not about which paths get linted — but it inflates the + sampling cost. +3. Does a preference for a rewritten passage generalise to a preference for + the rule, given that the rewrite is one of many that would satisfy it? +4. What is the right treatment of a rule that is right in aggregate and wrong + per instance? `CataphoricForecasting` is the clear case: eight "N things" + openers across four articles is a rhythm worth breaking, but no individual + hit is a defect. A gate that fires per occurrence is the wrong shape for + that complaint, and an occurrence-density rule may be the right one. +5. Is there a usable human-written control group anywhere — older commits, + another project by the same author — that would let the comparative + framing be repaired rather than discarded? + +## Artifacts + +The first evaluation ran in a scratch directory outside the repository and +modified nothing tracked. Its reproducible parts: a throwaway `.vale.ini` per +configuration (whole package, curated subset, experimental package), a +35-token `mdd.ToneWords` style, a synthetic AI-slop guide page used as a +positive control, and raw JSON alert dumps per corpus. If the evaluation +described above goes ahead, those should be rebuilt inside a committed +harness rather than recovered from scratch space. diff --git a/docs/spec/S06-documentation-site.md b/docs/spec/S06-documentation-site.md index 813005b..5f1973e 100644 --- a/docs/spec/S06-documentation-site.md +++ b/docs/spec/S06-documentation-site.md @@ -89,10 +89,13 @@ pure Markdown corpus. mdd/ ├── docs/ checked-in Markdown, no build artefacts │ ├── guide/ operator documentation (new) +│ ├── articles/ promoted synthesised articles (new) +│ ├── design-record/ introduction to the spec/research section (new) │ ├── reference/ generated, committed (Phase 3+) │ ├── research/ │ └── spec/ ├── scripts/ +│ ├── check-mdd-commands.py │ └── sync-docs.py ├── site/ Astro + Starlight application │ ├── astro.config.mjs @@ -101,7 +104,9 @@ mdd/ │ └── src/ │ ├── content/docs/ │ │ ├── index.mdx hand-authored landing page +│ │ ├── _drafts/ unpromoted article drafts (tracked, not built) │ │ ├── guide/ synced (gitignored) +│ │ ├── articles/ synced (gitignored) │ │ ├── reference/ synced (gitignored) │ │ ├── spec/ synced (gitignored) │ │ └── research/ synced (gitignored) @@ -109,6 +114,11 @@ mdd/ └── src/mdd/ ``` +Guide pages carry a numeric filename prefix — `docs/guide/01-install.md` — which +gives the corpus an order when read on GitHub and supplies the Starlight +`sidebar.order`. The prefix is stripped from the published slug, so the page is +served at `/mdd/guide/install/`. + This diverges from the Starlight convention, which puts a project's own site in `docs/`, and from the organisation repository, which does the same. The reason is specific to this project: `mdd` is a Markdown-mirror tool and @@ -202,6 +212,31 @@ Guide pages do without them. One hand-authored `index.mdx` lives in `site/` for the landing page, since a hero and card grid are presentation rather than documentation. +### Sections that are not the guide + +Two sidebar groups hold prose that is neither operator documentation nor a +synthesised article. + +**Design record** opens with `docs/design-record/index.md`, a short +introduction sitting above the collapsed Specs and Research subgroups. It +belongs there rather than in the guide: it describes the corpus a reader is +about to enter, and an operator following the guide has no reason to read it. +An `index.md` in any section publishes at the section root, so the page is +served at `/mdd/design-record/`. + +**Get involved** is last, and holds the project's governance and legal +documents — `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md` and +`LICENSE`. These are the only published pages whose source lives outside +`docs/`, because they are also the only ones a tool other than this site reads +from a fixed location: GitHub renders `CONTRIBUTING.md` in its pull-request +flow and surfaces `SECURITY.md` in its advisory UI, both by path. Moving them +would break that, so `sync-docs.py` names them explicitly and reads them from +the root at build time. + +`LICENSE` is not Markdown. It is published with an explicit title and its text +in a fenced block, so the Apache-2.0 text renders verbatim rather than being +reflowed by Markdown's paragraph handling. + ### Demoting the design record Specs and research notes are published, but on three demotion axes, all @@ -242,10 +277,24 @@ enforce them: default produce a tidy narrative in which the right answer was reached directly; that erases the most interesting content and yields prose with no opinions in it. -- **Never overwrite.** Drafts go to `site/src/content/docs/_drafts/`. The - underscore prefix is already what `docsLoader()` ignores, so drafts do not - build. Promotion is a `git mv`. Re-running the skill against a promoted - article diffs and proposes; it does not overwrite. +- **Never overwrite.** Drafts go to + `site/src/content/docs/_drafts/_.md`, with an underscore on both the + directory and the filename. `docsLoader()`'s glob is `**/[^_]*.{md,mdx}`, and + the `[^_]` applies to the filename rather than to any directory above it, so + a draft at `_drafts/foo.md` is still loaded and still breaks the build by + failing schema validation for having no `title`. Only the underscore on the + filename excludes it. Promotion is a `git mv` into `docs/articles/`, which + drops both underscores, and from where the article + is synced like everything else under `docs/` — requirement 2 applies to + articles as much as to anything else, so a published article is Markdown + under `docs/`, not a file that only exists inside the Astro application. + Drafts are tracked in git, so a draft is reviewable in a pull request before + anyone promotes it. Re-running the skill against a promoted article writes a + fresh draft and diffs it; it does not overwrite. + + A draft's relative links are written as though it already lived in + `docs/articles/`, so they are broken while it sits in `_drafts/`. Nothing + checks a draft, and they resolve on promotion. ### Deployment @@ -267,10 +316,44 @@ Checks are split by what makes them fail, not by what kind of check they are. |---|---|---| | Generated reference drift | code changed | `mise run ci` | | `mdd …` strings resolve against the command tree | code changed | `mise run ci` | +| Same check over the design record | code changed | advisory, never fails | | Vale prose lint | prose changed | `mise run docs-check` | +| Repo-relative links resolve | prose changed | `mise run docs-sync` | +| Emitted URLs resolve against the built site | prose or site config changed | `mise run docs-check` | | `astro check` and site build | prose or site config changed | `mise run docs-check` | | External link rot | neither; links rot on their own | scheduled workflow | +The `mdd …` string check covers prose that documents current behaviour — +`README.md`, `CONTRIBUTING.md`, `AGENTS.md`, `SECURITY.md`, and `docs/guide/`, +`docs/articles/` and `docs/reference/`. It does **not** block on `docs/spec/` +or `docs/research/`, and requirement 7 should be read as scoped accordingly. + +Running it over the whole corpus surfaced 95 mismatches, and every one was +correct. About half are commands that were later renamed — [S35](S35-argparse-cli-parsing.md) +flattened the CLI, so a spec predating it says `mdd confluence sync` where the +command is now `mdd confluence sync-space`. The rest are commands that were +proposed and rejected, or deferred and never built, named in exactly the +"Rejected"/"Out of scope" sections that make a design record worth keeping. + +Neither is a defect. A spec records intent at the time of writing; rewriting it +to match today's command tree would falsify the record, and requiring a +rejected alternative to resolve against a real command is a category error. The +check therefore runs over the design record in an advisory mode that reports +and always exits zero, which keeps the drift visible without making it +blocking. + +The two link checks answer different questions and both are needed. The sync +step resolves each repo-relative link against the working tree, which catches a +link to a file that does not exist. It cannot catch a link to a file that does +exist but is served at a different URL — Starlight lowercases the slug it +derives from a filename, so `S07-data-protection.md` is published at +`.../s07-data-protection/`, and a link carrying the filename's case satisfies +the first check and 404s for every reader. `scripts/check-site-links.py` +therefore crawls `site/dist/` after the build and resolves every internal +`href` and `src` against the pages and assets Astro actually emitted. It +strips fragments rather than verifying anchors, which is a larger job and is +not attempted. + Code-coupled checks must be in `mise run ci` because a docs-only workflow does not run on a Python pull request — and that is precisely the change that invalidates a doc. Prose checks are path-filtered on `docs/**` and `site/**`; @@ -394,7 +477,14 @@ abstract. The initial configuration is: - A vocabulary accept-list seeded with the project's domain terms (`Confluence`, `SharePoint`, `frontmatter`, `roundtrip`, `mddignore`, `Quarto`, `docling`). -- `filter_mode: added`, at warning level. +- `MinAlertLevel = warning`, but only **errors** gate. Vale's exit code covers + everything at or above `MinAlertLevel` and offers no way to print a class of + finding without failing on it, so `mise run docs-vale` makes two passes: one + with `--no-exit` that shows every warning, and one with + `--minAlertLevel=error` that decides the outcome. Misspellings and + wrong-cased terms are objective and block. Passive voice is a judgement a + reviewer makes; 135 warnings across the first ten pages, most of them correct + usage, is advice rather than a gate. Both rules are chosen for the same reason: much of `mdd`'s audience reads English as a second language. Passive constructions and idiom are what that @@ -424,9 +514,18 @@ survive the change of register. The tone rules that prose review currently catches by hand — `simply`, `seamlessly`, `powerful`, `robust`, `easily` — belong in that pass too, as a custom style rather than a packaged one. +**Per-page raw Markdown twins.** `sync-docs.py` writes a second copy of each +non-demoted page to `site/public/.md`, so +`https://schubergphilis.github.io/mdd/guide/install.md` serves an agent the +page's Markdown. The twin is the link-rewritten body without the Starlight +frontmatter. Spec and research pages get no twin, on the same demotion +reasoning that keeps them out of `llms.txt`. + **`.gitignore` additions.** `site/node_modules/`, `site/dist/`, -`site/.astro/`, and the synced content directories -(`site/src/content/docs/{guide,spec,research}/`). +`site/.astro/`, the synced content directories +(`site/src/content/docs/{guide,articles,reference,spec,research}/`), the raw +Markdown twins (`site/public/{guide,articles,reference}/`), and the Vale styles +fetched by `vale sync` (`.vale/styles/write-good/`). ## Rollout plan @@ -525,5 +624,11 @@ No open questions remain. sync in both directions, is a product capability of its own and would need its own research note and spec. - Migrating `README.md`, `CONTRIBUTING.md` or `AGENTS.md` into the site. They - stay at the repository root; the site may restate their content but does not - replace them. + stay at the repository root. `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, + `SECURITY.md` and `LICENSE` are *published* from there under a **Get + involved** group at the bottom of the sidebar — synced by the same script as + everything else, read from the root file at build time so they cannot drift. + That is republishing, not migrating: the root remains the only copy, and + `README.md` and `AGENTS.md` are not published at all, since the site's + landing page and the guide already cover the first and no human reads the + second. diff --git a/scripts/check-mdd-commands.py b/scripts/check-mdd-commands.py new file mode 100644 index 0000000..139c6bb --- /dev/null +++ b/scripts/check-mdd-commands.py @@ -0,0 +1,438 @@ +"""check-mdd-commands: verify every `mdd …` string in prose against the real CLI. + +The command tree is built by introspecting the argparse parser assembled by +``mdd.cli.build_dispatcher`` — never by shelling out to ``mdd --help`` and +scraping text — so the check tracks the actual code, not a copy of it. + +Scanned sources, per file: + +1. Inline code spans, e.g. `` `mdd confluence sync-space ` ``. +2. Fenced code blocks whose language is ``bash``, ``sh``, ``shell``, + ``console``, or absent. ``$ `` prompts and trailing ``\\`` line + continuations are handled. + +Bare prose outside a code span is never scanned: distinguishing "the `mdd` +tool does X" from a literal invocation in free text is not reliable enough to +justify blocking CI on it, so that class of sentence is left alone entirely. + +A candidate string only counts as an invocation when the token following +`mdd` looks like a subcommand (``[a-z][a-z0-9-]*``) or an option (starts with +``-``); a bare `` `mdd` `` reference to the project name is not a command and +is skipped. + +Within a candidate invocation, the longest leading run of tokens that are +neither options nor placeholders is walked against the command tree. A +placeholder — ``<...>``, ``[...]``, ``{...}``, ``...``/``…``, an ALL-CAPS +word, or a ``$VAR`` — ends the walk without being validated itself. A +``a|b|c`` alternation (used in prose to mean "one of these subcommands") is +accepted when every alternative names a real child of the current node. Long +options (``--foo``) found after the walk are checked against the resolved +command's own parser; short options are not validated. + +Output: one ``file:line: `mdd ...` — reason`` line per violation, with a +``did you mean`` suggestion from the closest real name when one exists. + +By default only prose that documents *current* behaviour is scanned — the +top-level docs plus ``docs/guide``, ``docs/articles``, ``docs/reference`` — +and the check exits 1 on any violation. Specs and research notes are excluded +from that default: they record design intent as it stood at the time of +writing, including proposals that were rejected or never built and command +names from before a later rename, so holding that prose to "resolves against +today's CLI" would falsify the historical record rather than correct it. +Pass ``--all`` to additionally scan ``docs/spec`` and ``docs/research`` and +print every hit found there; that run is advisory and always exits 0. +""" + +from __future__ import annotations + +import argparse +import difflib +import itertools +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +from mdd.cli import build_dispatcher + +if TYPE_CHECKING: + from collections.abc import Generator, Iterator, Sequence + +REPO_ROOT = Path(__file__).resolve().parent.parent + +PROSE_FILES = ("README.md", "docs/README.md", "CONTRIBUTING.md", "AGENTS.md", "SECURITY.md") +PROSE_DIRS = ("docs/guide", "docs/articles", "docs/reference") +ADVISORY_DIRS = ("docs/spec", "docs/research") + +FENCE_LANGS = frozenset({"bash", "sh", "shell", "console", ""}) +FENCE_RE = re.compile(r"^ {0,3}```\s*([A-Za-z0-9_+-]*)\s*$") +INLINE_CODE_RE = re.compile(r"`([^`\n]+)`") +STATEMENT_SPLIT_RE = re.compile(r"&&|\|\||\||;") +PROMPT_PREFIX_RE = re.compile(r"^\$\s*") +RUN_PREFIX_RE = re.compile(r"^(?:uv\s+run\s+|uv\s+tool\s+run\s+)") +SUBCOMMAND_TOKEN_RE = re.compile(r"^[a-z][a-z0-9-]*$") +PLACEHOLDER_LITERALS = frozenset({"...", "…"}) +BRACKET_PAIRS = (("<", ">"), ("[", "]"), ("{", "}")) + + +# --------------------------------------------------------------------------- +# The real command tree, introspected from argparse. +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class CommandNode: + """The valid subcommands and long options at one point in the mdd command tree.""" + + children: frozenset[str] + options: frozenset[str] + + +def build_command_tree() -> dict[tuple[str, ...], CommandNode]: + """Introspect ``mdd``'s argparse tree into a path -> (subcommands, options) map.""" + tree: dict[tuple[str, ...], CommandNode] = {} + _walk_parser(build_dispatcher(), (), tree) + return tree + + +def _walk_parser( + parser: argparse.ArgumentParser, + path: tuple[str, ...], + tree: dict[tuple[str, ...], CommandNode], +) -> None: + sub_action = _subparsers_action(parser) + children = frozenset(sub_action.choices) if sub_action is not None else frozenset() + tree[path] = CommandNode(children=children, options=_long_options(parser)) + if sub_action is None: + return + for name, subparser in sub_action.choices.items(): + _walk_parser(subparser, (*path, name), tree) + + +def _subparsers_action( + parser: argparse.ArgumentParser, +) -> argparse._SubParsersAction[argparse.ArgumentParser] | None: # pyright: ignore[reportPrivateUsage] + for action in parser._actions: + if isinstance(action, argparse._SubParsersAction): # pyright: ignore[reportPrivateUsage] + return action + return None + + +def _long_options(parser: argparse.ArgumentParser) -> frozenset[str]: + return frozenset( + opt for action in parser._actions for opt in action.option_strings if opt.startswith("--") + ) + + +# --------------------------------------------------------------------------- +# Scanning prose for candidate `mdd ...` invocations. +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class Invocation: + """A candidate `mdd ...` string found in prose, ready for validation.""" + + file: Path + line: int + text: str + tokens: tuple[str, ...] + + +def discover_files(root: Path, *, include_advisory: bool = False) -> list[Path]: + """List every prose file the check scans, skipping directories that don't exist yet. + + With *include_advisory*, also lists ``docs/spec`` and ``docs/research`` — the + design record, scanned only for the advisory ``--all`` run. + """ + files: list[Path] = [root / name for name in PROSE_FILES if (root / name).is_file()] + dirs = (*PROSE_DIRS, *ADVISORY_DIRS) if include_advisory else PROSE_DIRS + for rel in dirs: + base = root / rel + if base.is_dir(): + files.extend(sorted(base.rglob("*.md"))) + return files + + +def scan_file(path: Path) -> list[Invocation]: + """Find every candidate `mdd ...` invocation in one Markdown file.""" + return list(_scan_text(path, path.read_text(encoding="utf-8"))) + + +def _scan_text(path: Path, text: str) -> Iterator[Invocation]: + lines = text.splitlines() + idx = 0 + while idx < len(lines): + fence = FENCE_RE.match(lines[idx]) + if fence is not None: + idx = yield from _scan_fence(path, lines, idx, fence.group(1).lower()) + continue + yield from _scan_prose_line(path, lines[idx], idx + 1) + idx += 1 + + +def _scan_fence( + path: Path, lines: list[str], start_idx: int, lang: str +) -> Generator[Invocation, None, int]: + idx = start_idx + 1 + content_start_line = idx + 1 + block_lines: list[str] = [] + while idx < len(lines) and not FENCE_RE.match(lines[idx]): + block_lines.append(lines[idx]) + idx += 1 + if idx < len(lines): + idx += 1 # skip the closing fence line + if lang in FENCE_LANGS: + yield from _scan_shell_block(path, block_lines, content_start_line) + return idx + + +def _scan_shell_block(path: Path, block_lines: list[str], start_line: int) -> Iterator[Invocation]: + for line_no, logical in _join_continuations(block_lines, start_line): + for statement in STATEMENT_SPLIT_RE.split(logical): + tokens = _extract_invocation_tokens(statement) + if tokens is not None: + yield Invocation(path, line_no, "mdd " + " ".join(tokens), tokens) + + +def _scan_prose_line(path: Path, line: str, line_no: int) -> Iterator[Invocation]: + for match in INLINE_CODE_RE.finditer(line): + tokens = _extract_invocation_tokens(match.group(1)) + if tokens is not None: + yield Invocation(path, line_no, "mdd " + " ".join(tokens), tokens) + + +def _join_continuations(block_lines: list[str], start_line: int) -> list[tuple[int, str]]: + """Join `\\`-continued shell lines, keeping the first physical line number.""" + result: list[tuple[int, str]] = [] + buf: list[str] = [] + buf_start = start_line + for offset, raw in enumerate(block_lines): + line_no = start_line + offset + stripped = raw.rstrip() + continued = stripped.endswith("\\") + if not buf: + buf_start = line_no + buf.append(stripped[:-1] if continued else stripped) + if not continued: + result.append((buf_start, " ".join(buf))) + buf = [] + if buf: + result.append((buf_start, " ".join(buf))) + return result + + +def _extract_invocation_tokens(raw: str) -> tuple[str, ...] | None: + """Return the tokens after `mdd` if *raw* looks like an mdd invocation, else None.""" + stripped = RUN_PREFIX_RE.sub("", PROMPT_PREFIX_RE.sub("", raw.strip())) + tokens = stripped.split() + if len(tokens) < 2 or tokens[0] != "mdd": + return None + nxt = tokens[1] + looks_like_command = ( + nxt.startswith("-") or SUBCOMMAND_TOKEN_RE.match(nxt) or _is_placeholder(nxt) or "|" in nxt + ) + return tuple(tokens[1:]) if looks_like_command else None + + +def _strip_wrapping_quotes(tok: str) -> str: + if len(tok) >= 2 and tok[0] == tok[-1] and tok[0] in "\"'": + return tok[1:-1] + return tok + + +def _is_placeholder(raw_tok: str) -> bool: + tok = _strip_wrapping_quotes(raw_tok) + if tok in PLACEHOLDER_LITERALS: + return True + if tok.startswith("$") and len(tok) > 1: + return True + if any(tok.startswith(open_) and tok.endswith(close) for open_, close in BRACKET_PAIRS): + return True + return bool(tok) and tok.isupper() and any(ch.isalpha() for ch in tok) + + +# --------------------------------------------------------------------------- +# Validating a candidate invocation against the command tree. +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class WalkResult: + """Where an invocation's token walk stopped, and why -- if it failed.""" + + path: tuple[str, ...] + remainder: tuple[str, ...] + error: str | None + bad_token: str | None + candidates: tuple[str, ...] + + +def validate_invocation( + tokens: Sequence[str], tree: dict[tuple[str, ...], CommandNode] +) -> WalkResult | None: + """Validate one invocation's tokens against *tree*. None means it resolves cleanly.""" + walk = _walk_path(tokens, tree) + if walk.error is not None: + return walk + return _check_options(walk.path, walk.remainder, tree) + + +def _alternatives_valid(tok: str, children: frozenset[str]) -> bool: + if "|" not in tok: + return False + parts = tok.split("|") + return bool(parts) and all(part in children for part in parts) + + +def _walk_path(tokens: Sequence[str], tree: dict[tuple[str, ...], CommandNode]) -> WalkResult: + path: list[str] = [] + node = tree[()] + idx = 0 + while idx < len(tokens): + if not node.children: + # A leaf command has no further subcommands to walk into, so whatever + # remains is positional argument text (a file path, a quoted title, a + # bracketed placeholder group, a trailing comment, ...), not a path + # segment we can validate. + break + tok = tokens[idx] + if tok.startswith("-") or _is_placeholder(tok): + break + if _alternatives_valid(tok, node.children): + idx += 1 + break + if tok not in node.children: + return WalkResult( + path=tuple(path), + remainder=tuple(tokens[idx:]), + error="unknown subcommand", + bad_token=tok, + candidates=tuple(sorted(node.children)), + ) + path.append(tok) + node = tree[tuple(path)] + idx += 1 + return WalkResult( + path=tuple(path), remainder=tuple(tokens[idx:]), error=None, bad_token=None, candidates=() + ) + + +def _check_options( + path: tuple[str, ...], + remainder: Sequence[str], + tree: dict[tuple[str, ...], CommandNode], +) -> WalkResult | None: + node = tree[path] + for tok in remainder: + if not tok.startswith("--") or _is_placeholder(tok): + continue + opt = tok.split("=", 1)[0] + if opt not in node.options: + return WalkResult( + path=path, + remainder=(), + error="unknown option", + bad_token=opt, + candidates=tuple(sorted(node.options)), + ) + return None + + +# --------------------------------------------------------------------------- +# Reporting. +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class Violation: + """One `mdd ...` string that failed to resolve against the command tree.""" + + file: Path + line: int + text: str + reason: str + suggestion: str | None + + +def _closest_suggestion(result: WalkResult) -> str | None: + if result.bad_token is None or not result.candidates: + return None + matches = difflib.get_close_matches(result.bad_token, result.candidates, n=1) + if not matches: + return None + return " ".join((*result.path, matches[0])) + + +def _violation_for(inv: Invocation, tree: dict[tuple[str, ...], CommandNode]) -> Violation | None: + result = validate_invocation(inv.tokens, tree) + if result is None: + return None + reason = f"{result.error} `{result.bad_token}`" + return Violation(inv.file, inv.line, inv.text, reason, _closest_suggestion(result)) + + +def _violation_detail(violation: Violation) -> str: + hint = f" (did you mean `mdd {violation.suggestion}`?)" if violation.suggestion else "" + return f"{violation.line}: `{violation.text}` — {violation.reason}{hint}" + + +def _format_violation(violation: Violation, root: Path) -> str: + rel = violation.file.relative_to(root) + return f"{rel}:{_violation_detail(violation)}" + + +def _print_advisory_report(violations: list[Violation], root: Path) -> None: + print( + "ADVISORY — docs/spec/** and docs/research/** record design intent as it stood\n" + "at the time of writing. These hits are informational only and never fail the build." + ) + for file, group in itertools.groupby(violations, key=lambda v: v.file): + print(f"\n{file.relative_to(root)}:") + for violation in group: + print(f" {_violation_detail(violation)}") + + +def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + _ = parser.add_argument( + "--all", + action="store_true", + help="Also scan docs/spec and docs/research; advisory only, always exits 0", + ) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = _parse_args(argv) + tree = build_command_tree() + files = discover_files(REPO_ROOT, include_advisory=args.all) + invocations = [inv for file in files for inv in scan_file(file)] + violations = [v for inv in invocations if (v := _violation_for(inv, tree)) is not None] + + if args.all: + _print_advisory_report(violations, REPO_ROOT) + print( + f"\ncheck-mdd-commands --all: {len(violations)} advisory hit(s) " + f"across {len(files)} file(s) (not enforced)" + ) + return 0 + + for violation in violations: + print(_format_violation(violation, REPO_ROOT)) + if violations: + print( + f"check-mdd-commands: {len(violations)} invalid `mdd ...` string(s) found", + file=sys.stderr, + ) + return 1 + print( + f"check-mdd-commands: {len(invocations)} `mdd ...` string(s) checked " + f"across {len(files)} file(s), all resolve" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check-site-links.py b/scripts/check-site-links.py new file mode 100644 index 0000000..1f1c7cf --- /dev/null +++ b/scripts/check-site-links.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Check that every internal link in the built site resolves to something served. + +The sync step validates that a link's *target file exists in the repository*. +That is a different question from whether the URL it emits matches what the +site actually serves, and only the second one matters to a reader. A case +mismatch between a filename and the slug derived from it satisfies the first +check and 404s on the second. + +This runs over `site/dist/` after a build and resolves every internal `href` +and `src` against the pages and assets that were emitted. Fragments are +stripped rather than verified: checking that an anchor exists on the target +page is a larger job and is not attempted here. + +Run with `mise run docs-links`, which builds the site first. +""" + +from __future__ import annotations + +import re +import sys +from collections import defaultdict +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +DIST = REPO_ROOT / "site" / "dist" +BASE = "/mdd/" + +# `href="..."` and `src="..."`, single or double quoted. +LINK_RE = re.compile(r"""\b(?:href|src)=["']([^"']+)["']""") +EXTERNAL_SCHEMES = ("http://", "https://", "//", "mailto:", "tel:", "data:") + +# Documentation quotes HTML at itself — `` in a spec table, an +# `` in a research note. Inside and
 the angle
+# brackets are escaped but the quotes are not, so the attribute text survives
+# verbatim and looks exactly like a real link to a regex. Drop those regions
+# before scanning.
+CODE_RE = re.compile(r"<(code|pre)\b[^>]*>.*?", re.DOTALL | re.IGNORECASE)
+
+
+def is_internal(url: str) -> bool:
+    """True for a link this site is responsible for serving."""
+    if not url or url.startswith(("#", "?")):
+        return False
+    return not url.startswith(EXTERNAL_SCHEMES)
+
+
+def target_path(url: str) -> str | None:
+    """Strip the deploy base, the fragment and any query from an internal URL.
+
+    Returns None for a link that is not under the deploy base, which a
+    correctly built page should never contain.
+    """
+    path = url.partition("#")[0].partition("?")[0]
+    if not path.startswith(BASE):
+        return None
+    return path[len(BASE) :].strip("/")
+
+
+def is_served(target: str, pages: set[str], dist: Path) -> bool:
+    """True if `target` resolves to a rendered page or an emitted asset."""
+    if not target:
+        return True  # the site root
+    return target in pages or (dist / target).is_file()
+
+
+def collect_pages(dist: Path) -> set[str]:
+    """Every directory that has an index.html, as a base-relative path."""
+    return {p.parent.relative_to(dist).as_posix().strip(".") for p in dist.rglob("index.html")}
+
+
+def find_broken(dist: Path) -> dict[str, set[str]]:
+    """Map each unresolvable URL to the pages that link to it."""
+    pages = collect_pages(dist)
+    broken: dict[str, set[str]] = defaultdict(set)
+    for html in dist.rglob("*.html"):
+        source = html.relative_to(dist).as_posix()
+        markup = CODE_RE.sub("", html.read_text(encoding="utf-8"))
+        for url in LINK_RE.findall(markup):
+            if not is_internal(url):
+                continue
+            # A None target is a root-relative link that missed the deploy
+            # base, which 404s on a project site just as surely as a typo.
+            target = target_path(url)
+            if target is None or not is_served(target, pages, dist):
+                broken[url].add(source)
+    return broken
+
+
+def report(broken: dict[str, set[str]]) -> None:
+    for url in sorted(broken):
+        sources = sorted(broken[url])
+        shown = ", ".join(sources[:3])
+        more = f" (+{len(sources) - 3} more)" if len(sources) > 3 else ""
+        print(f"broken link: {url}\n  linked from: {shown}{more}", file=sys.stderr)
+
+
+def main() -> int:
+    if not DIST.is_dir():
+        print(f"no build found at {DIST}; run `mise run docs-build` first", file=sys.stderr)
+        return 1
+
+    pages = collect_pages(DIST)
+    broken = find_broken(DIST)
+    if broken:
+        report(broken)
+        print(
+            f"check-site-links: {len(broken)} broken internal link target(s)",
+            file=sys.stderr,
+        )
+        return 1
+
+    print(f"check-site-links: all internal links resolve across {len(pages)} page(s)")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/scripts/derive-dark-logo.py b/scripts/derive-dark-logo.py
new file mode 100644
index 0000000..753f123
--- /dev/null
+++ b/scripts/derive-dark-logo.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+"""Derive the dark-mode site logo from the light-mode one.
+
+The logo is a transparent PNG in two colours: a dark slate for the wordmark
+and the document outlines, and an amber that already matches the site's accent
+ramp. Only the slate needs to change for a dark background; recolouring it to
+the warm near-white the site uses for headings keeps the amber intact and the
+two variants identical in shape.
+
+Pixels are mapped by nearest source colour, so antialiased edges -- which are
+blends of a source colour and transparency -- follow the colour they came
+from. Alpha is preserved untouched.
+
+Run with `mise run derive-dark-logo`.
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+from PIL import Image
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+SOURCE = REPO_ROOT / "assets" / "mdd-logo.png"
+TARGET = REPO_ROOT / "site" / "src" / "assets" / "mdd-logo-dark.png"
+
+type Rgb = tuple[int, int, int]
+
+# Slate becomes the warm near-white the dark theme uses for headings; amber is
+# already the accent colour and maps to itself.
+SLATE: Rgb = (51, 58, 64)
+AMBER: Rgb = (193, 122, 35)
+RECOLOUR: dict[Rgb, Rgb] = {SLATE: (255, 235, 210), AMBER: AMBER}
+
+
+def nearest(pixel: Rgb, anchors: list[Rgb]) -> Rgb:
+    """Return the anchor colour closest to `pixel` in squared RGB distance."""
+
+    def distance(anchor: Rgb) -> int:
+        return sum((a - b) ** 2 for a, b in zip(pixel, anchor, strict=True))
+
+    return min(anchors, key=distance)
+
+
+def recolour(image: Image.Image) -> Image.Image:
+    """Map every visible pixel onto its recoloured anchor, keeping alpha."""
+    anchors = list(RECOLOUR)
+    out = Image.new("RGBA", image.size)
+    source = image.load()
+    dest = out.load()
+    if source is None or dest is None:  # pragma: no cover - Pillow always loads
+        raise RuntimeError("could not access pixel data")
+
+    width, height = image.size
+    for y in range(height):
+        for x in range(width):
+            r, g, b, a = source[x, y]
+            if a == 0:
+                continue
+            dest[x, y] = (*RECOLOUR[nearest((r, g, b), anchors)], a)
+    return out
+
+
+def main() -> int:
+    if not SOURCE.exists():
+        print(f"source logo not found: {SOURCE}", file=sys.stderr)
+        return 1
+
+    with Image.open(SOURCE) as image:
+        result = recolour(image.convert("RGBA"))
+
+    TARGET.parent.mkdir(parents=True, exist_ok=True)
+    result.save(TARGET)
+    print(f"wrote {TARGET.relative_to(REPO_ROOT)} ({result.width}x{result.height})")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/scripts/sync-docs.py b/scripts/sync-docs.py
new file mode 100644
index 0000000..d9c4ee4
--- /dev/null
+++ b/scripts/sync-docs.py
@@ -0,0 +1,695 @@
+"""sync-docs: copy docs/**.md into site/src/content/docs/, adding the
+Starlight frontmatter the source files do not carry, and rewriting
+repo-relative links so they resolve on the built site (or on GitHub, for
+files the site does not publish).
+
+Source -> destination mapping:
+
+    docs/guide/NN-slug.md        -> site/src/content/docs/guide/slug.md
+    docs/articles/*.md           -> site/src/content/docs/articles/*.md
+    docs/reference/**/*.md       -> site/src/content/docs/reference/**/*.md
+    docs/spec/*.md                -> site/src/content/docs/spec/*.md
+    docs/research/*.md            -> site/src/content/docs/research/*.md
+    docs/design-record/*.md       -> site/src/content/docs/design-record/*.md
+      -> site/src/content/docs/get-involved/.md
+
+A source file named `index.md` publishes at its section's own root slug
+(`docs/design-record/index.md` -> `/mdd/design-record/`) instead of a
+`.../index/` segment. The get-involved section is not a directory glob: its
+sources are the fixed list in `GET_INVOLVED_FILES`, read from the repository
+root and left there — synced, not moved. `LICENSE` in that list is plain
+text, not Markdown; it is wrapped in a fenced block rather than parsed.
+
+Each destination directory is deleted before it is repopulated, so a file
+removed from `docs/` does not linger in the synced output. `docs/reference/`
+and `docs/articles/` may not exist yet; an absent or empty source directory
+is not an error.
+
+Frontmatter is derived per file: `title` from the first level-1 heading
+(which is then removed from the body, since Starlight renders `title` as the
+page's own heading), and `description` from the `**Purpose:**` line (spec
+files) or the first paragraph of body text (everything else). Guide pages
+get a `sidebar.order` from their filename's numeric prefix. Spec and
+research pages are demoted: `pagefind: false`, a `sidebar.label` that is
+just the document's id (`S06`, `R14`, `000`), and a `banner` marking them as
+a design record rather than user documentation. Every page also gets an
+`editUrl` pointing at its real source file — the synced path Starlight sees
+is not that file (a numeric prefix is stripped, or the file lives at the
+repository root rather than under `docs/`), so only this script can supply
+a working "edit this page" link.
+
+Every relative link is resolved against the linking file's directory. A
+link to a file this script publishes becomes a site-absolute URL; a link to
+a file that exists but is not published (README.md, source code, tests, ...)
+becomes a GitHub blob URL; a link to a file that does not exist anywhere is
+reported to stderr and makes the run fail. Absolute URLs, `mailto:` links and
+bare `#fragment` links are left alone. Links inside fenced or indented code
+blocks, and inside inline code spans, are never rewritten.
+
+For every synced page that is not part of the demoted design record, a raw
+Markdown twin — the same link-rewritten body, without frontmatter, with its
+heading restored — is written to `site/public/.md`, so the deployed
+site can serve a page's Markdown directly to a client that fetches it.
+
+Run with no arguments: `uv run python scripts/sync-docs.py`.
+"""
+
+from __future__ import annotations
+
+import re
+import shutil
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+import yaml
+
+GITHUB_BLOB_BASE = "https://github.com/schubergphilis/mdd/blob/main"
+# Deliberately a separate constant from GITHUB_BLOB_BASE, not derived from
+# it: "blob" (used for links out to non-published files) and "edit" (used
+# for each page's own editUrl) are different GitHub paths, and conflating
+# them would be a subtle bug the next person to touch this would not expect.
+GITHUB_EDIT_BASE = "https://github.com/schubergphilis/mdd/edit/main"
+SITE_BASE = "/mdd"
+
+DEMOTED_KINDS = {"spec", "research"}
+# Pages that carry a `sidebar.order`. Guide pages derive it from a filename
+# prefix; get-involved pages get it from their fixed position in
+# GET_INVOLVED_FILES. Either way, `build_sidebar` just reads `page.order`.
+NUMBERED_KINDS = {"guide", "get-involved"}
+ID_LABEL_KINDS = {"spec", "research"}
+
+GET_INVOLVED_FILES: tuple[tuple[str, str, int], ...] = (
+    ("CONTRIBUTING.md", "contributing", 1),
+    ("CODE_OF_CONDUCT.md", "code-of-conduct", 2),
+    ("SECURITY.md", "security", 3),
+    ("LICENSE", "license", 4),
+)
+
+# The new-spec scaffold, not a spec itself. Its `[NNN-](.md)` line
+# is a placeholder for a future author to fill in, not a real link, so it is
+# excluded rather than published or treated as a broken link.
+EXCLUDED_SOURCES = frozenset({("spec", "spec-template.md")})
+
+MAX_DESCRIPTION_LENGTH = 155
+
+DEMOTION_BANNER = (
+    "This document describes intent at the time it was written. It is part "
+    "of the design record, not user documentation, and may not reflect the "
+    "current behaviour of the code."
+)
+
+NUMERIC_PREFIX_RE = re.compile(r"^(\d+)-(.+)$")
+DOC_ID_RE = re.compile(r"^([A-Za-z]*\d+)")
+FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?", re.DOTALL)
+H1_RE = re.compile(r"^#\s+(.+?)\s*#*\s*$")
+PURPOSE_RE = re.compile(r"^\*\*Purpose:\*\*\s*(.+)$", re.MULTILINE)
+INLINE_LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]+)\)")
+REF_DEF_RE = re.compile(r"^(\s{0,3}\[[^\]]+\]:\s*)(\S+)(.*)$")
+FENCE_RE = re.compile(r"^(\s{0,3})(`{3,}|~{3,})")
+LIST_MARKER_RE = re.compile(r"^([-*+]|\d+[.)])\s")
+CODE_SPAN_RE = re.compile(r"(`+)((?:(?!\1).)+?)\1", re.DOTALL)
+INLINE_LINK_TEXT_RE = re.compile(r"\[([^\]]*)\]\([^)]*\)")
+EMPHASIS_RE = re.compile(r"(\*{1,3}|_{1,3})(?!\s)(.+?)(? destination-tree mapping."""
+
+    kind: str
+    source: Path
+    dest: Path
+    recursive: bool
+
+
+@dataclass(frozen=True)
+class Page:
+    """A single Markdown file discovered under one `Section`."""
+
+    kind: str
+    source: Path
+    slug: str
+    dest: Path
+    twin: Path | None
+    order: int | None
+    doc_id: str | None
+
+
+@dataclass(frozen=True)
+class RenderResult:
+    """The rendered output for one `Page`, plus any violations found in it."""
+
+    content: str
+    twin: str | None
+    violations: list[str]
+
+
+def build_sections(docs_dir: Path, content_dir: Path) -> tuple[Section, ...]:
+    return (
+        Section("guide", docs_dir / "guide", content_dir / "guide", recursive=False),
+        Section("articles", docs_dir / "articles", content_dir / "articles", recursive=False),
+        Section("reference", docs_dir / "reference", content_dir / "reference", recursive=True),
+        Section("spec", docs_dir / "spec", content_dir / "spec", recursive=False),
+        Section("research", docs_dir / "research", content_dir / "research", recursive=False),
+        Section(
+            "design-record",
+            docs_dir / "design-record",
+            content_dir / "design-record",
+            recursive=False,
+        ),
+    )
+
+
+@dataclass(frozen=True)
+class NamedFileSection:
+    """A section whose sources are an explicit, ordered list of files at
+    fixed repository paths, rather than a directory glob — governance
+    documents that live at the repository root and stay there.
+    """
+
+    kind: str
+    dest: Path
+    files: tuple[tuple[Path, str, int], ...]  # (source, slug name, sidebar order)
+
+
+def build_named_file_sections(repo_root: Path, content_dir: Path) -> tuple[NamedFileSection, ...]:
+    return (
+        NamedFileSection(
+            "get-involved",
+            content_dir / "get-involved",
+            tuple(
+                (repo_root / filename, name, order) for filename, name, order in GET_INVOLVED_FILES
+            ),
+        ),
+    )
+
+
+def _clean_destination(kind: str, dest: Path, public_dir: Path) -> None:
+    if dest.exists():
+        shutil.rmtree(dest)
+    if kind not in DEMOTED_KINDS:
+        twin_dir = public_dir / kind
+        if twin_dir.exists():
+            shutil.rmtree(twin_dir)
+
+
+def clean_destinations(
+    sections: tuple[Section, ...],
+    named_sections: tuple[NamedFileSection, ...],
+    public_dir: Path,
+) -> None:
+    for section in sections:
+        _clean_destination(section.kind, section.dest, public_dir)
+    for named_section in named_sections:
+        _clean_destination(named_section.kind, named_section.dest, public_dir)
+
+
+def strip_numeric_prefix(name: str) -> tuple[str, int | None]:
+    match = NUMERIC_PREFIX_RE.match(name)
+    if match is None:
+        return name, None
+    return match.group(2), int(match.group(1))
+
+
+def derive_doc_id(stem: str) -> str | None:
+    match = DOC_ID_RE.match(stem)
+    return match.group(1) if match else None
+
+
+def build_page(section: Section, path: Path, public_dir: Path) -> Page:
+    rel_no_ext = path.relative_to(section.source).with_suffix("")
+    parts = list(rel_no_ext.parts)
+    order: int | None = None
+    if section.kind in NUMBERED_KINDS:
+        parts[-1], order = strip_numeric_prefix(parts[-1])
+    rel_stem = Path(*parts)
+    dest = section.dest / rel_stem.with_suffix(".md")
+
+    # `index.md` is the section's own root page, not a page named "index"
+    # nested under it, so the emitted URL drops the filename rather than
+    # adding a literal `.../index/` segment. This applies at any depth (a
+    # future `docs/reference/cli/index.md` gets the same treatment), and the
+    # destination filename above is untouched — Starlight derives a root
+    # page from a file named `index.md`, so only the URL we emit needs this.
+    is_index = parts[-1].lower() == "index"
+    slug_parts = parts[:-1] if is_index else parts
+    # Starlight lowercases the slug it derives from a filename, so a page
+    # written as `S07-data-protection.md` is served at `.../s07-data-protection/`.
+    # URLs built here must match, or every link to a spec or research note is a
+    # 404. The destination filename keeps its original case.
+    url_stem = "/".join(part.lower() for part in slug_parts)
+    slug = f"{section.kind}/{url_stem}" if url_stem else section.kind
+
+    twin = None
+    if section.kind not in DEMOTED_KINDS:
+        if is_index:
+            twin_rel = f"{url_stem}/index.md" if url_stem else "index.md"
+        else:
+            twin_rel = f"{url_stem}.md"
+        twin = public_dir / section.kind / twin_rel
+    doc_id = derive_doc_id(path.stem) if section.kind in ID_LABEL_KINDS else None
+    return Page(section.kind, path, slug, dest, twin, order, doc_id)
+
+
+def discover_named_file_section(section: NamedFileSection, public_dir: Path) -> list[Page]:
+    pages: list[Page] = []
+    for source, name, order in section.files:
+        if not source.is_file():
+            continue
+        slug = f"{section.kind}/{name}"
+        dest = section.dest / f"{name}.md"
+        twin = None
+        if section.kind not in DEMOTED_KINDS:
+            twin = public_dir / section.kind / f"{name}.md"
+        pages.append(Page(section.kind, source, slug, dest, twin, order, None))
+    return pages
+
+
+def discover_section(section: Section, public_dir: Path) -> list[Page]:
+    if not section.source.is_dir():
+        return []
+    pattern = "**/*.md" if section.recursive else "*.md"
+    return [
+        build_page(section, path, public_dir)
+        for path in sorted(section.source.glob(pattern))
+        if (section.kind, path.name) not in EXCLUDED_SOURCES
+    ]
+
+
+def discover_pages(
+    sections: tuple[Section, ...],
+    named_sections: tuple[NamedFileSection, ...],
+    public_dir: Path,
+) -> list[Page]:
+    pages: list[Page] = []
+    for section in sections:
+        pages.extend(discover_section(section, public_dir))
+    for named_section in named_sections:
+        pages.extend(discover_named_file_section(named_section, public_dir))
+    return pages
+
+
+def strip_inline_markdown(text: str) -> str:
+    text = INLINE_LINK_TEXT_RE.sub(r"\1", text)
+    text = CODE_SPAN_RE.sub(r"\2", text)
+    text = EMPHASIS_RE.sub(r"\2", text)
+    return text.strip()
+
+
+def truncate_at_sentence(text: str, limit: int = MAX_DESCRIPTION_LENGTH) -> str:
+    text = " ".join(text.split())
+    if len(text) <= limit:
+        return text
+    truncated = text[:limit]
+    boundary = max(truncated.rfind(". "), truncated.rfind("! "), truncated.rfind("? "))
+    if boundary > 0:
+        return truncated[: boundary + 1].strip()
+    last_space = truncated.rfind(" ")
+    head = truncated[:last_space] if last_space > 0 else truncated
+    return head.rstrip() + "…"
+
+
+def extract_purpose(body: str) -> str | None:
+    match = PURPOSE_RE.search(body)
+    if match is None:
+        return None
+    return strip_inline_markdown(match.group(1))
+
+
+def extract_first_paragraph(body: str) -> str | None:
+    paragraph: list[str] = []
+    for line in body.splitlines():
+        stripped = line.strip()
+        if not stripped or stripped.startswith("#"):
+            if paragraph:
+                break
+            continue
+        paragraph.append(stripped)
+    if not paragraph:
+        return None
+    return strip_inline_markdown(" ".join(paragraph))
+
+
+def derive_description(kind: str, body: str) -> str | None:
+    text = extract_purpose(body) if kind == "spec" else None
+    if text is None:
+        text = extract_first_paragraph(body)
+    if not text:
+        return None
+    return truncate_at_sentence(text)
+
+
+def extract_title(body: str) -> tuple[str | None, str, int]:
+    """Pull out the first level-1 heading and drop it (and a following blank
+    line) from the body. Also returns how many lines were consumed, so a
+    caller can keep reporting line numbers against the original file.
+    """
+    lines = body.splitlines(keepends=True)
+    for index, line in enumerate(lines):
+        if not line.strip():
+            continue
+        match = H1_RE.match(line.strip("\n"))
+        if match is None:
+            return None, body, 0
+        title = strip_inline_markdown(match.group(1))
+        consumed = index + 1
+        remainder = lines[index + 1 :]
+        if remainder and not remainder[0].strip():
+            remainder = remainder[1:]
+            consumed += 1
+        return title, "".join(remainder), consumed
+    return None, body, 0
+
+
+def split_frontmatter(raw: str) -> tuple[dict[str, object], str, int]:
+    match = FRONTMATTER_RE.match(raw)
+    if match is None:
+        return {}, raw, 0
+    data = yaml.safe_load(match.group(1)) or {}
+    consumed = match.group(0).count("\n")
+    return data, raw[match.end() :], consumed
+
+
+def build_sidebar(page: Page) -> dict[str, object] | None:
+    if page.kind in NUMBERED_KINDS and page.order is not None:
+        return {"order": page.order}
+    if page.kind in ID_LABEL_KINDS and page.doc_id is not None:
+        return {"label": page.doc_id}
+    return None
+
+
+def build_edit_url(source: Path, repo_root: Path) -> str:
+    """The GitHub "edit this page" URL for a page's real source file.
+
+    Starlight's own `editLink.baseUrl` appends the page's path *within the
+    content collection* (e.g. `guide/install.md`), not its real source (e.g.
+    `docs/guide/01-install.md`) — the two differ by a numeric prefix for
+    guide pages, and are unrelated paths entirely for get-involved pages,
+    which live at the repository root. Only this script knows the mapping,
+    so it emits a per-page `editUrl` override rather than leaving Starlight
+    to guess from the synced path.
+    """
+    rel = source.relative_to(repo_root)
+    return f"{GITHUB_EDIT_BASE}/{rel.as_posix()}"
+
+
+def build_frontmatter(
+    page: Page, title: str, body: str, existing: dict[str, object], edit_url: str
+) -> dict[str, object]:
+    derived: dict[str, object] = {"title": title}
+    description = derive_description(page.kind, body)
+    if description is not None:
+        derived["description"] = description
+    sidebar = build_sidebar(page)
+    if sidebar is not None:
+        derived["sidebar"] = sidebar
+    if page.kind in DEMOTED_KINDS:
+        derived["pagefind"] = False
+        derived["banner"] = {"content": DEMOTION_BANNER}
+    derived["editUrl"] = edit_url
+    return {**existing, **derived}
+
+
+def classify_url(url: str) -> str:
+    if url.startswith("#"):
+        return "fragment"
+    if url.startswith("mailto:"):
+        return "mailto"
+    if SCHEME_RE.match(url) or url.startswith("//"):
+        return "absolute"
+    return "relative"
+
+
+def split_url_title(raw: str) -> tuple[str, str]:
+    raw = raw.strip()
+    match = re.match(r'^(\S+)(\s+".*")?$', raw)
+    if match is None:
+        return raw, ""
+    return match.group(1), match.group(2) or ""
+
+
+def rewrite_url(
+    url: str, source: Path, lineno: int, site_paths: dict[Path, str], repo_root: Path
+) -> tuple[str, str | None]:
+    if classify_url(url) != "relative":
+        return url, None
+    path_part, _, fragment = url.partition("#")
+    fragment_suffix = f"#{fragment}" if fragment else ""
+    if not path_part:
+        return url, None
+    resolved = (source.parent / path_part).resolve()
+    slug = site_paths.get(resolved)
+    if slug is not None:
+        return f"{SITE_BASE}/{slug}/{fragment_suffix}", None
+    if resolved.exists():
+        try:
+            rel = resolved.relative_to(repo_root)
+        except ValueError:
+            return url, f"{source}:{lineno}: link escapes the repository: {url}"
+        return f"{GITHUB_BLOB_BASE}/{rel.as_posix()}{fragment_suffix}", None
+    return url, f"{source}:{lineno}: broken link: {url}"
+
+
+def mask_code_spans(content: str) -> tuple[str, list[str]]:
+    spans: list[str] = []
+
+    def stash(match: re.Match[str]) -> str:
+        spans.append(match.group(0))
+        return f"{len(spans) - 1}"
+
+    return CODE_SPAN_RE.sub(stash, content), spans
+
+
+def unmask_code_spans(content: str, spans: list[str]) -> str:
+    for index, original in enumerate(spans):
+        content = content.replace(f"{index}", original)
+    return content
+
+
+def _rewrite_reference_definitions(
+    text: str, source: Path, start_line: int, site_paths: dict[Path, str], repo_root: Path
+) -> tuple[str, list[str]]:
+    """Reference-style definitions (`[label]: url`) are anchored to one
+    physical line each, unlike inline links, so these are rewritten per line
+    before the block is treated as a single unit for the rest.
+    """
+    violations: list[str] = []
+    rewritten: list[str] = []
+    for offset, line in enumerate(text.splitlines(keepends=True)):
+        content = line.splitlines()[0] if line.splitlines() else ""
+        match = REF_DEF_RE.match(content)
+        if match is None:
+            rewritten.append(line)
+            continue
+        prefix, url, rest = match.groups()
+        new_url, violation = rewrite_url(url, source, start_line + offset, site_paths, repo_root)
+        if violation:
+            violations.append(violation)
+        rewritten.append(line.replace(content, f"{prefix}{new_url}{rest}", 1))
+    return "".join(rewritten), violations
+
+
+def rewrite_block(
+    text: str, source: Path, start_line: int, site_paths: dict[Path, str], repo_root: Path
+) -> tuple[str, list[str]]:
+    """Rewrite every link in one contiguous run of non-code lines, treated as
+    a single unit so a link that wraps across a line break is still caught —
+    processing line by line would silently pass a wrapped link straight
+    through, unrewritten and unreported.
+    """
+    text, violations = _rewrite_reference_definitions(
+        text, source, start_line, site_paths, repo_root
+    )
+    masked, spans = mask_code_spans(text)
+
+    def replace(match: re.Match[str]) -> str:
+        link_text, raw_url = match.group(1), match.group(2)
+        url, title = split_url_title(raw_url)
+        match_line = start_line + masked.count("\n", 0, match.start())
+        new_url, violation = rewrite_url(url, source, match_line, site_paths, repo_root)
+        if violation:
+            violations.append(violation)
+        return f"[{link_text}]({new_url}{title})"
+
+    rewritten = INLINE_LINK_RE.sub(replace, masked)
+    return unmask_code_spans(rewritten, spans), violations
+
+
+def _fence_close(line: str, fence_char: str, fence_len: int) -> bool:
+    pattern = rf"^\s{{0,3}}{re.escape(fence_char)}{{{fence_len},}}\s*$"
+    return re.match(pattern, line) is not None
+
+
+def _fence_open(content: str) -> re.Match[str] | None:
+    """Match a genuine opening code fence, as distinct from a run of
+    backticks used as an inline code-span delimiter in running prose (for
+    example, four backticks wrapping literal text that itself contains
+    triple backticks). CommonMark's rule: a backtick fence's info string may
+    not itself contain a backtick, since that would be ambiguous with a code
+    span; a tilde fence has no such restriction.
+    """
+    match = FENCE_RE.match(content)
+    if match is None:
+        return None
+    if match.group(2)[0] == "`" and "`" in content[match.end() :]:
+        return None
+    return match
+
+
+@dataclass
+class _FenceState:
+    char: str = ""
+    length: int = 0
+
+
+def _classify_line(content: str, fence: _FenceState, list_active: bool) -> tuple[bool, bool]:
+    """Return (is_passthrough, new_list_active) for one physical line.
+
+    A passthrough line — fenced code, a fence delimiter, an indented code
+    line, or blank — is emitted unchanged and never joins a link-rewriting
+    block. Everything else is prose and gets grouped into a block by the
+    caller.
+    """
+    if fence.char:
+        if _fence_close(content, fence.char, fence.length):
+            fence.char, fence.length = "", 0
+        return True, list_active
+    fence_match = _fence_open(content)
+    if fence_match is not None:
+        fence.char, fence.length = fence_match.group(2)[0], len(fence_match.group(2))
+        return True, list_active
+    if not content.strip():
+        return True, list_active
+    stripped = content.lstrip(" ")
+    indent = len(content) - len(stripped)
+    if LIST_MARKER_RE.match(stripped) and indent <= 3:
+        list_active = True
+    elif indent < 4:
+        list_active = False
+    return indent >= 4 and not list_active, list_active
+
+
+def rewrite_links_in_body(
+    body: str, source: Path, site_paths: dict[Path, str], repo_root: Path, start_line: int = 1
+) -> tuple[str, list[str]]:
+    violations: list[str] = []
+    out_parts: list[str] = []
+    fence = _FenceState()
+    list_active = False
+    prose_lines: list[str] = []
+    prose_start = start_line
+
+    def flush_prose() -> None:
+        if not prose_lines:
+            return
+        block_text = "".join(prose_lines)
+        new_text, block_violations = rewrite_block(
+            block_text, source, prose_start, site_paths, repo_root
+        )
+        out_parts.append(new_text)
+        violations.extend(block_violations)
+        prose_lines.clear()
+
+    for lineno, line in enumerate(body.splitlines(keepends=True), start=start_line):
+        content = line.splitlines()[0] if line.splitlines() else ""
+        passthrough, list_active = _classify_line(content, fence, list_active)
+        if passthrough:
+            flush_prose()
+            out_parts.append(line)
+            continue
+        if not prose_lines:
+            prose_start = lineno
+        prose_lines.append(line)
+
+    flush_prose()
+    return "".join(out_parts), violations
+
+
+def render_license_page(page: Page, repo_root: Path) -> RenderResult:
+    """`LICENSE` is the verbatim Apache-2.0 text, not Markdown: it has no
+    heading, and letting it flow through paragraph/list handling would
+    mangle its formatting. It is read here at sync time, rather than copied
+    into `docs/`, so the published copy cannot drift from the file it
+    mirrors. Wrapping it in a fenced block keeps it byte-for-byte; the title
+    is given explicitly since there is no heading to derive one from.
+
+    It gets a real `editUrl`, the same as every other page, rather than
+    `editUrl: false`: it points at an actual file, and "edit" landing on the
+    Apache text is harmless and honest.
+    """
+    title = "License"
+    license_text = page.source.read_text(encoding="utf-8")
+    body = (
+        "`mdd` is licensed under the Apache License 2.0. This page mirrors the "
+        f"[`LICENSE`]({GITHUB_BLOB_BASE}/LICENSE) file at the repository root.\n\n"
+        f"```text\n{license_text}```\n"
+    )
+    edit_url = build_edit_url(page.source, repo_root)
+    frontmatter = build_frontmatter(page, title, body, {}, edit_url)
+    dumped = yaml.safe_dump(
+        frontmatter, sort_keys=False, allow_unicode=True, default_flow_style=False
+    )
+    content = f"---\n{dumped}---\n\n{body}"
+    twin = f"# {title}\n\n{body}" if page.twin is not None else None
+    return RenderResult(content, twin, [])
+
+
+def render_page(page: Page, site_paths: dict[Path, str], repo_root: Path) -> RenderResult:
+    if page.kind == "get-involved" and page.source.name == "LICENSE":
+        return render_license_page(page, repo_root)
+    raw = page.source.read_text(encoding="utf-8")
+    existing_fm, stripped, fm_lines = split_frontmatter(raw)
+    title, body, title_lines = extract_title(stripped)
+    if title is None:
+        return RenderResult(
+            "", None, [f"{page.source}: missing a level-1 heading to use as the title"]
+        )
+    start_line = fm_lines + title_lines + 1
+    body, violations = rewrite_links_in_body(body, page.source, site_paths, repo_root, start_line)
+    edit_url = build_edit_url(page.source, repo_root)
+    frontmatter = build_frontmatter(page, title, body, existing_fm, edit_url)
+    dumped = yaml.safe_dump(
+        frontmatter, sort_keys=False, allow_unicode=True, default_flow_style=False
+    )
+    content = f"---\n{dumped}---\n\n{body}"
+    twin = f"# {title}\n\n{body}" if page.twin is not None else None
+    return RenderResult(content, twin, violations)
+
+
+def write_page(page: Page, result: RenderResult) -> None:
+    page.dest.parent.mkdir(parents=True, exist_ok=True)
+    page.dest.write_text(result.content, encoding="utf-8")
+    if page.twin is not None and result.twin is not None:
+        page.twin.parent.mkdir(parents=True, exist_ok=True)
+        page.twin.write_text(result.twin, encoding="utf-8")
+
+
+def run(repo_root: Path) -> int:
+    docs_dir = repo_root / "docs"
+    content_dir = repo_root / "site" / "src" / "content" / "docs"
+    public_dir = repo_root / "site" / "public"
+    sections = build_sections(docs_dir, content_dir)
+    named_sections = build_named_file_sections(repo_root, content_dir)
+    clean_destinations(sections, named_sections, public_dir)
+    pages = discover_pages(sections, named_sections, public_dir)
+    site_paths = {page.source: page.slug for page in pages}
+    violations: list[str] = []
+    for page in pages:
+        result = render_page(page, site_paths, repo_root)
+        violations.extend(result.violations)
+        if result.content:
+            write_page(page, result)
+    for violation in violations:
+        print(violation, file=sys.stderr)
+    return 1 if violations else 0
+
+
+def main() -> int:
+    repo_root = Path(__file__).resolve().parent.parent
+    return run(repo_root)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/site/astro.config.mjs b/site/astro.config.mjs
new file mode 100644
index 0000000..1ce46f2
--- /dev/null
+++ b/site/astro.config.mjs
@@ -0,0 +1,123 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+import starlightLlmsTxt from 'starlight-llms-txt';
+import { remarkGithubAlerts } from './src/plugins/remark-github-alerts.mjs';
+import excludeDesignRecordFromLlmsFull, {
+	PAGE_SEPARATOR,
+} from './src/plugins/exclude-design-record-from-llms-full.mjs';
+
+const description = 'Bidirectional sync between Markdown-in-git and Confluence/SharePoint.';
+
+// This is a *project* site: it deploys under a subpath of
+// https://schubergphilis.github.io (https://schubergphilis.github.io/mdd/), so
+// it sets `base`. Content under `site/src/content/docs/{guide,articles,spec,
+// research}/` is synced from `docs/**.md` by `scripts/sync-docs.py`
+// (`mise run docs-sync`); those directories are gitignored build input, not
+// source. See S06's "Content pipeline".
+export default defineConfig({
+	site: 'https://schubergphilis.github.io',
+	base: '/mdd/',
+	markdown: {
+		// Maps GitHub alert blockquotes (`> [!NOTE]`) onto Starlight asides so
+		// guide pages render sensibly both on GitHub and on the site. See the
+		// plugin's own comment for how it piggybacks on Starlight's built-in
+		// directive-to-aside conversion.
+		remarkPlugins: [remarkGithubAlerts],
+	},
+	integrations: [
+		excludeDesignRecordFromLlmsFull(),
+		starlight({
+			title: 'mdd',
+			description,
+			favicon: '/favicon.png',
+			// The wordmark is a dark slate that disappears against the dark nav
+			// bar, so dark mode gets a recoloured variant. Regenerate it with
+			// `mise run derive-dark-logo` if the source logo changes.
+			logo: {
+				light: './src/assets/mdd-logo.png',
+				dark: './src/assets/mdd-logo-dark.png',
+				alt: 'mdd',
+				replacesTitle: false,
+			},
+			pagefind: true,
+			pagination: true,
+			lastUpdated: true,
+			customCss: ['./src/styles/custom.css'],
+			components: {
+				Footer: './src/components/Footer.astro',
+			},
+			social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/schubergphilis/mdd' }],
+			// No global editLink. Starlight would append the page's path inside
+			// the content collection, but synced pages do not live there in the
+			// repository: a guide page keeps a numeric filename prefix, and the
+			// Get involved pages come from the repository root. sync-docs.py
+			// knows the real source path and writes a per-page `editUrl`.
+			// The one hand-authored page below carries its own.
+			// Explicitly ordered rather than one big autogenerated tree: the
+			// operator guide leads, articles are second, and the spec/research
+			// design record is demoted into a single collapsed group at the
+			// bottom. See S06's "Audience first" and "Demoting the design
+			// record".
+			sidebar: [
+				{ label: 'Guide', items: [{ autogenerate: { directory: 'guide' } }] },
+				{ label: 'Articles', items: [{ autogenerate: { directory: 'articles' } }] },
+				{
+					label: 'Design record',
+					collapsed: true,
+					items: [
+						// The introduction to this section, not a guide page — it
+						// explains what specs and research notes are and why they
+						// describe intent rather than current behaviour.
+						{ slug: 'design-record' },
+						{
+							label: 'Specs',
+							collapsed: true,
+							items: [{ autogenerate: { directory: 'spec' } }],
+						},
+						{
+							label: 'Research notes',
+							collapsed: true,
+							items: [{ autogenerate: { directory: 'research' } }],
+						},
+					],
+				},
+				// Last, and deliberately so: the project's governance and legal
+				// documents. These files stay at the repository root and are
+				// synced from there, so the site republishes them rather than
+				// owning them.
+				{
+					label: 'Get involved',
+					collapsed: true,
+					items: [{ autogenerate: { directory: 'get-involved' } }],
+				},
+			],
+			plugins: [
+				starlightLlmsTxt({
+					projectName: 'mdd',
+					description,
+					// The spec and research corpus is design record, not user-facing
+					// documentation (see S06's "Demoting the design record"); it
+					// should not surface in agent-facing output any more than in
+					// Pagefind search.
+					//
+					// NOTE (deviation, checked against starlight-llms-txt@0.11.0's
+					// source in node_modules): this `exclude` option is only read by
+					// the llms-small.txt route; llms-full.txt's generator call does
+					// not accept an exclude list at all, so excluded pages would
+					// otherwise still appear there. `excludeDesignRecordFromLlmsFull`
+					// above closes that gap by post-processing the built file. Keep
+					// this option set regardless - correct today for llms-small.txt,
+					// and it'll cover llms-full.txt too for free if a future release
+					// forwards it there.
+					exclude: ['spec/**', 'research/**'],
+					// A marker no real page content will contain, so the
+					// `astro:build:done` integration above can split
+					// `llms-full.txt` back into pages exactly. See that file's
+					// comment for why a blank-line/heading heuristic isn't safe.
+					pageSeparator: PAGE_SEPARATOR,
+				}),
+			],
+		}),
+	],
+});
diff --git a/site/bun.lock b/site/bun.lock
new file mode 100644
index 0000000..c2ce2d9
--- /dev/null
+++ b/site/bun.lock
@@ -0,0 +1,1129 @@
+{
+  "lockfileVersion": 1,
+  "configVersion": 1,
+  "workspaces": {
+    "": {
+      "name": "mdd-site",
+      "dependencies": {
+        "@astrojs/starlight": "^0.41.0",
+        "@fontsource/merriweather": "^5.2.14",
+        "@fontsource/merriweather-sans": "^5.2.7",
+        "astro": "^7.0.0",
+        "sharp": "^0.35.3",
+        "starlight-llms-txt": "^0.11.0",
+        "unist-util-visit": "^5.1.0",
+      },
+      "devDependencies": {
+        "@astrojs/check": "^0.9.9",
+        "typescript": "^6",
+      },
+    },
+  },
+  "packages": {
+    "@astrojs/check": ["@astrojs/check@0.9.10", "", { "dependencies": { "@astrojs/language-server": "^2.16.7", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^18.0.0" }, "peerDependencies": { "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "astro-check": "./bin/astro-check.js" } }, "sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ=="],
+
+    "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="],
+
+    "@astrojs/compiler-binding": ["@astrojs/compiler-binding@0.3.2", "", { "optionalDependencies": { "@astrojs/compiler-binding-darwin-arm64": "0.3.2", "@astrojs/compiler-binding-darwin-x64": "0.3.2", "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.2", "@astrojs/compiler-binding-linux-arm64-musl": "0.3.2", "@astrojs/compiler-binding-linux-x64-gnu": "0.3.2", "@astrojs/compiler-binding-linux-x64-musl": "0.3.2", "@astrojs/compiler-binding-wasm32-wasi": "0.3.2", "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.2", "@astrojs/compiler-binding-win32-x64-msvc": "0.3.2" } }, "sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ=="],
+
+    "@astrojs/compiler-binding-darwin-arm64": ["@astrojs/compiler-binding-darwin-arm64@0.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g=="],
+
+    "@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag=="],
+
+    "@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ=="],
+
+    "@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw=="],
+
+    "@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q=="],
+
+    "@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ=="],
+
+    "@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.3.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.2.0" }, "cpu": "none" }, "sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw=="],
+
+    "@astrojs/compiler-binding-win32-arm64-msvc": ["@astrojs/compiler-binding-win32-arm64-msvc@0.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q=="],
+
+    "@astrojs/compiler-binding-win32-x64-msvc": ["@astrojs/compiler-binding-win32-x64-msvc@0.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA=="],
+
+    "@astrojs/compiler-rs": ["@astrojs/compiler-rs@0.3.2", "", { "dependencies": { "@astrojs/compiler-binding": "0.3.2" } }, "sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg=="],
+
+    "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.2", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ=="],
+
+    "@astrojs/language-server": ["@astrojs/language-server@2.16.13", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.71", "volar-service-emmet": "0.0.71", "volar-service-html": "0.0.71", "volar-service-prettier": "0.0.71", "volar-service-typescript": "0.0.71", "volar-service-typescript-twoslash-queries": "0.0.71", "volar-service-yaml": "0.0.71", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-ekOa+CYprEq5n4EJC1qTIAhLk49HZIUQuFwrEuF+3JK/pdMaYnWoREFUI2A0KEPOJiFA2kamBzKzbYljDvUxLg=="],
+
+    "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-FGfmK84zSNcrsBd0dl1gXE9JvZYElp8EXQa2jpHVAxG4deGKAp43wspxFupjADJX7MSsMRHwYCnfT6EyVmgeFQ=="],
+
+    "@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.3.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "satteri": "^0.9.1" } }, "sha512-CvWVEFAbay7YO+i9SaqDJubipA5ckiVB89QWoMJ5XC0m5CtFg8JwZ7Kau6X9sYY7FZURH0w2l03ISH2jOS/RDQ=="],
+
+    "@astrojs/mdx": ["@astrojs/mdx@7.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/markdown-remark": "7.2.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw=="],
+
+    "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="],
+
+    "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="],
+
+    "@astrojs/starlight": ["@astrojs/starlight@0.41.5", "", { "dependencies": { "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/mdx": "^7.0.0", "@astrojs/sitemap": "^3.7.2", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.44.0", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.3", "hast-util-select": "^6.0.4", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^0.30.21", "mdast-util-directive": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.5.2", "rehype": "^13.0.2", "rehype-format": "^5.0.1", "remark-directive": "^4.0.0", "satteri": "^0.9.1", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.2.0", "astro": "^7.0.2" }, "optionalPeers": ["@astrojs/markdown-remark"] }, "sha512-BNkyysByeqk9cXGNnd1fC1+kc9rrllo1Df/XD34wQg/I5HUX1Bc+F91aTktg17sapQs+mafzWSRgmURRyCNLKA=="],
+
+    "@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="],
+
+    "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="],
+
+    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
+
+    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+
+    "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
+
+    "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
+
+    "@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow=="],
+
+    "@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q=="],
+
+    "@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA=="],
+
+    "@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ=="],
+
+    "@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw=="],
+
+    "@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q=="],
+
+    "@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.9.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ=="],
+
+    "@bruits/satteri-win32-arm64-msvc": ["@bruits/satteri-win32-arm64-msvc@0.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg=="],
+
+    "@bruits/satteri-win32-x64-msvc": ["@bruits/satteri-win32-x64-msvc@0.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g=="],
+
+    "@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="],
+
+    "@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
+
+    "@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
+
+    "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="],
+
+    "@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
+
+    "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
+
+    "@emmetio/css-parser": ["@emmetio/css-parser@0.4.1", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ=="],
+
+    "@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="],
+
+    "@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="],
+
+    "@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="],
+
+    "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
+
+    "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
+
+    "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
+
+    "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
+
+    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
+
+    "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
+
+    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
+
+    "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
+
+    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
+
+    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
+
+    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
+
+    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
+
+    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
+
+    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
+
+    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
+
+    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
+
+    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
+
+    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
+
+    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
+
+    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
+
+    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
+
+    "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
+
+    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
+
+    "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
+
+    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
+
+    "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
+
+    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
+
+    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
+
+    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
+
+    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
+
+    "@expressive-code/core": ["@expressive-code/core@0.44.1", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-3dDo9N8D7hYrLNNMMWFovg3+aDUtnQm7c7z0GZc1c0LEFVBc0Q6lKG+tVT28gDadOvsgOANfCn35fgpe97Pmgg=="],
+
+    "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.44.1", "", { "dependencies": { "@expressive-code/core": "^0.44.1" } }, "sha512-HC/bdRao9225ApcgO/e3jn8ZOhldKO7ob1O/Tcipvtv7Vb5nMphZhMtD9uuywpvxkPYBHJi3504WhrKg05Dwqg=="],
+
+    "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.44.1", "", { "dependencies": { "@expressive-code/core": "^0.44.1", "shiki": "^4.0.2" } }, "sha512-YApiZt3buUzBwL5tqj8G+sYC5NjMjRCHgQwr9bmGl69rtcHy6fE9dooWUeKYB978fJT2BuxT5FeHcF47rA3SEg=="],
+
+    "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.44.1", "", { "dependencies": { "@expressive-code/core": "^0.44.1" } }, "sha512-B3BsJoJ8CFMlcIX9f+X9tcI3C4zPDO601+YuLi9GheSTNro7ZfqSjLptMQKBHOWZvxnAtY5zvIX7iO/qtBhNBg=="],
+
+    "@fontsource/merriweather": ["@fontsource/merriweather@5.3.0", "", {}, "sha512-9LpPsNTYzRALj468Uj1K9Usl5eXk991L/kKmcpdWbTF1na9NfMyAXMgUABlMReViU1MYBz9kJ8jEzEOtX9qGDg=="],
+
+    "@fontsource/merriweather-sans": ["@fontsource/merriweather-sans@5.3.0", "", {}, "sha512-l8E8wze9DdaDPMmRp8fm4ucMKqLl/rXksrGqxKO6YVtKtt15wRbkcVEmxYNffFMQnEp39G7hKiypRWfVYTbW5w=="],
+
+    "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
+
+    "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
+
+    "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
+
+    "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="],
+
+    "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
+
+    "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
+
+    "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
+
+    "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
+
+    "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="],
+
+    "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="],
+
+    "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
+
+    "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
+
+    "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
+
+    "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
+
+    "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
+
+    "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
+
+    "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="],
+
+    "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="],
+
+    "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
+
+    "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
+
+    "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
+
+    "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
+
+    "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
+
+    "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="],
+
+    "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="],
+
+    "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
+
+    "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
+
+    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+    "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
+
+    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.1", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ=="],
+
+    "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
+
+    "@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="],
+
+    "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="],
+
+    "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="],
+
+    "@pagefind/default-ui": ["@pagefind/default-ui@1.5.2", "", {}, "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg=="],
+
+    "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA=="],
+
+    "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw=="],
+
+    "@pagefind/linux-x64": ["@pagefind/linux-x64@1.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA=="],
+
+    "@pagefind/windows-arm64": ["@pagefind/windows-arm64@1.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g=="],
+
+    "@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="],
+
+    "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="],
+
+    "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA=="],
+
+    "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ=="],
+
+    "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg=="],
+
+    "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA=="],
+
+    "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg=="],
+
+    "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A=="],
+
+    "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg=="],
+
+    "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ=="],
+
+    "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw=="],
+
+    "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A=="],
+
+    "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.1", "", { "os": "none", "cpu": "arm64" }, "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw=="],
+
+    "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.2.1", "", { "dependencies": { "@emnapi/core": "2.0.0-alpha.3", "@emnapi/runtime": "2.0.0-alpha.3", "@napi-rs/wasm-runtime": "^1.2.0" } }, "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww=="],
+
+    "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw=="],
+
+    "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ=="],
+
+    "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
+    "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
+
+    "@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="],
+
+    "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="],
+
+    "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="],
+
+    "@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="],
+
+    "@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="],
+
+    "@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
+
+    "@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="],
+
+    "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
+
+    "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+
+    "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
+
+    "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
+
+    "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
+
+    "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+    "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
+
+    "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
+
+    "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+    "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="],
+
+    "@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="],
+
+    "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+    "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
+
+    "@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="],
+
+    "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
+
+    "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+    "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
+
+    "@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="],
+
+    "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
+
+    "@volar/language-server": ["@volar/language-server@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw=="],
+
+    "@volar/language-service": ["@volar/language-service@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw=="],
+
+    "@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
+
+    "@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
+
+    "@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="],
+
+    "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
+
+    "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
+
+    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+    "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
+
+    "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
+
+    "ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="],
+
+    "am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="],
+
+    "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
+    "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+    "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
+
+    "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
+
+    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+    "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+    "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
+
+    "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
+
+    "astro": ["astro@7.1.6", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.2", "@astrojs/internal-helpers": "0.10.2", "@astrojs/markdown-satteri": "0.3.5", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.2" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-83x9rYbHazMaZkYrAFRVZXSQx2moFkz0F7cjTDUF3GWfS0a3p2vZXG1ZdhV86rStHApQCodBJW+XTD37xISIrQ=="],
+
+    "astro-expressive-code": ["astro-expressive-code@0.44.1", "", { "dependencies": { "rehype-expressive-code": "^0.44.1", "url-extras": "^0.1.0" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, "sha512-DT1LnCqbHasBKlvzJ3m6LR4VI94wwx3W9EV/YbP1te4rqjOHsvsezHYuqb5MeLWLftXms/1FA9QBbwCo43DnJQ=="],
+
+    "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+    "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
+    "bcp-47": ["bcp-47@2.1.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw=="],
+
+    "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="],
+
+    "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
+
+    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+    "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+    "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+    "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+    "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+    "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
+    "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
+    "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
+
+    "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
+
+    "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+    "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
+
+    "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+    "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
+
+    "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
+
+    "cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
+
+    "cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
+
+    "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
+
+    "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
+
+    "css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
+
+    "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
+
+    "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
+
+    "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
+
+    "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
+
+    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+    "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
+
+    "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
+
+    "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+    "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
+
+    "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+    "devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
+
+    "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+    "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
+
+    "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
+
+    "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+    "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+    "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+    "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
+    "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
+
+    "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
+
+    "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+    "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+    "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
+
+    "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
+
+    "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
+
+    "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
+
+    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+    "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
+    "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
+
+    "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
+
+    "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
+    "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
+
+    "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
+
+    "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
+
+    "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+    "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
+
+    "expressive-code": ["expressive-code@0.44.1", "", { "dependencies": { "@expressive-code/core": "^0.44.1", "@expressive-code/plugin-frames": "^0.44.1", "@expressive-code/plugin-shiki": "^0.44.1", "@expressive-code/plugin-text-markers": "^0.44.1" } }, "sha512-GakidxhapWDzpKLqEaFQ8wGk6gAqEtPQibu8+yPBfnDLgev5Vdsh1pasTxnrXL/mzIknyqeTwhMHTghdaiUrTg=="],
+
+    "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+    "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
+
+    "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
+
+    "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="],
+
+    "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
+
+    "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+    "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
+
+    "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="],
+
+    "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="],
+
+    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+    "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
+
+    "get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="],
+
+    "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
+
+    "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
+
+    "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="],
+
+    "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="],
+
+    "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
+
+    "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
+
+    "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="],
+
+    "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="],
+
+    "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
+
+    "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="],
+
+    "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
+
+    "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="],
+
+    "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
+
+    "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="],
+
+    "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
+
+    "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
+
+    "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
+
+    "hast-util-to-mdast": ["hast-util-to-mdast@10.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-to-html": "^9.0.0", "hast-util-to-text": "^4.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "mdast-util-to-string": "^4.0.0", "rehype-minify-whitespace": "^6.0.0", "trim-trailing-lines": "^2.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ=="],
+
+    "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
+
+    "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
+
+    "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
+
+    "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+    "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
+
+    "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
+
+    "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
+
+    "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
+
+    "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
+
+    "i18next": ["i18next@26.3.6", "", { "peerDependencies": { "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA=="],
+
+    "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+
+    "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
+
+    "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+    "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+    "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+    "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
+
+    "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
+    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+    "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+    "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
+
+    "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+    "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
+
+    "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+    "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
+
+    "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
+
+    "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
+
+    "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
+
+    "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
+
+    "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
+
+    "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
+
+    "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
+
+    "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
+
+    "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
+
+    "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
+
+    "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
+
+    "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
+
+    "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
+    "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
+
+    "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+    "magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
+
+    "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
+
+    "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
+    "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
+
+    "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="],
+
+    "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
+    "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
+
+    "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
+
+    "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+    "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
+
+    "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+    "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+    "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
+    "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
+
+    "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
+
+    "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
+
+    "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+    "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+    "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+    "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+    "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
+    "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
+
+    "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+    "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+    "micromark-extension-directive": ["micromark-extension-directive@4.0.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg=="],
+
+    "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+    "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+    "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+    "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+    "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+    "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+    "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
+    "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
+
+    "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
+
+    "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
+
+    "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
+
+    "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
+
+    "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+    "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+    "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
+
+    "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+    "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+    "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+    "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+    "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+    "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+    "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+    "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+    "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+    "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+    "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
+
+    "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+    "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+    "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+    "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+    "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+    "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+    "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+    "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+    "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
+
+    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+    "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
+
+    "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
+
+    "neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="],
+
+    "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
+
+    "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
+
+    "node-mock-http": ["node-mock-http@1.0.5", "", {}, "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw=="],
+
+    "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
+
+    "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
+
+    "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
+
+    "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
+
+    "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
+
+    "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
+
+    "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
+
+    "p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="],
+
+    "p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="],
+
+    "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
+
+    "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
+
+    "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="],
+
+    "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+    "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
+
+    "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+    "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
+
+    "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
+
+    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+    "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
+
+    "postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="],
+
+    "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
+
+    "postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="],
+
+    "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
+
+    "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
+
+    "process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="],
+
+    "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
+
+    "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
+
+    "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
+    "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
+
+    "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
+
+    "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
+
+    "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
+
+    "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
+
+    "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
+
+    "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+
+    "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
+
+    "rehype-expressive-code": ["rehype-expressive-code@0.44.1", "", { "dependencies": { "expressive-code": "^0.44.1" } }, "sha512-+VZgs7Evw4LXRN3owpoBNSTpYuW6GeOdjqcUT1TuY8o/4MGPtbd0EU7Bgrju7X8KrQ6SslOBAuGWJ5fV5TriJQ=="],
+
+    "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="],
+
+    "rehype-minify-whitespace": ["rehype-minify-whitespace@6.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0" } }, "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw=="],
+
+    "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
+
+    "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
+
+    "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
+
+    "rehype-remark": ["rehype-remark@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "hast-util-to-mdast": "^10.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ=="],
+
+    "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
+
+    "remark-directive": ["remark-directive@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" } }, "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA=="],
+
+    "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
+    "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
+
+    "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+    "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
+
+    "remark-smartypants": ["remark-smartypants@3.0.3", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA=="],
+
+    "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
+    "request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
+
+    "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+    "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
+    "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
+
+    "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
+
+    "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
+
+    "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
+
+    "rolldown": ["rolldown@1.2.1", "", { "dependencies": { "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.1", "@rolldown/binding-darwin-arm64": "1.2.1", "@rolldown/binding-darwin-x64": "1.2.1", "@rolldown/binding-freebsd-x64": "1.2.1", "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", "@rolldown/binding-linux-arm64-gnu": "1.2.1", "@rolldown/binding-linux-arm64-musl": "1.2.1", "@rolldown/binding-linux-ppc64-gnu": "1.2.1", "@rolldown/binding-linux-s390x-gnu": "1.2.1", "@rolldown/binding-linux-x64-gnu": "1.2.1", "@rolldown/binding-linux-x64-musl": "1.2.1", "@rolldown/binding-openharmony-arm64": "1.2.1", "@rolldown/binding-wasm32-wasi": "1.2.1", "@rolldown/binding-win32-arm64-msvc": "1.2.1", "@rolldown/binding-win32-x64-msvc": "1.2.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw=="],
+
+    "satteri": ["satteri@0.9.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.9.5", "@bruits/satteri-darwin-x64": "0.9.5", "@bruits/satteri-linux-arm64-gnu": "0.9.5", "@bruits/satteri-linux-arm64-musl": "0.9.5", "@bruits/satteri-linux-x64-gnu": "0.9.5", "@bruits/satteri-linux-x64-musl": "0.9.5", "@bruits/satteri-wasm32-wasi": "0.9.5", "@bruits/satteri-win32-arm64-msvc": "0.9.5", "@bruits/satteri-win32-x64-msvc": "0.9.5" } }, "sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w=="],
+
+    "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
+
+    "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+    "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
+
+    "shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
+
+    "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
+
+    "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="],
+
+    "smol-toml": ["smol-toml@1.7.1", "", {}, "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ=="],
+
+    "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
+
+    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+    "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+    "starlight-llms-txt": ["starlight-llms-txt@0.11.0", "", { "dependencies": { "@astrojs/mdx": "^7.0.0", "@types/hast": "^3.0.4", "@types/micromatch": "^4.0.10", "github-slugger": "^2.0.0", "hast-util-select": "^6.0.4", "micromatch": "^4.0.8", "rehype-parse": "^9.0.1", "rehype-remark": "^10.0.1", "remark-gfm": "^4.0.1", "remark-stringify": "^11.0.0", "unified": "^11.0.5", "unist-util-remove": "^4.0.0" }, "peerDependencies": { "@astrojs/starlight": ">=0.41.0", "astro": "^7.0.0" } }, "sha512-83TU5zPgJhL9fXIWAOWjQjyQ6EKocshEjJyA4zOJjOqu/oxQsrTpYGYxjHLyfZe4LRD52N9eCuykSkaIYwUyDw=="],
+
+    "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
+
+    "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
+
+    "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+    "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+
+    "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
+
+    "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
+
+    "svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
+
+    "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
+
+    "tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="],
+
+    "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
+
+    "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+    "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+    "trim-trailing-lines": ["trim-trailing-lines@2.1.0", "", {}, "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="],
+
+    "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
+    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+    "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
+
+    "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
+
+    "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
+
+    "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
+
+    "ultrahtml": ["ultrahtml@1.7.0", "", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="],
+
+    "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
+
+    "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+    "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+    "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
+
+    "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
+
+    "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+    "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="],
+
+    "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+    "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
+
+    "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
+
+    "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
+
+    "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+    "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+    "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
+
+    "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+    "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
+
+    "url-extras": ["url-extras@0.1.0", "", {}, "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw=="],
+
+    "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
+
+    "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+    "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
+
+    "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+    "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="],
+
+    "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
+
+    "volar-service-css": ["volar-service-css@0.0.71", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw=="],
+
+    "volar-service-emmet": ["volar-service-emmet@0.0.71", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA=="],
+
+    "volar-service-html": ["volar-service-html@0.0.71", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw=="],
+
+    "volar-service-prettier": ["volar-service-prettier@0.0.71", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg=="],
+
+    "volar-service-typescript": ["volar-service-typescript@0.0.71", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw=="],
+
+    "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.71", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A=="],
+
+    "volar-service-yaml": ["volar-service-yaml@0.0.71", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.23.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q=="],
+
+    "vscode-css-languageservice": ["vscode-css-languageservice@6.3.10", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA=="],
+
+    "vscode-html-languageservice": ["vscode-html-languageservice@5.6.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="],
+
+    "vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="],
+
+    "vscode-jsonrpc": ["vscode-jsonrpc@9.0.1", "", {}, "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw=="],
+
+    "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
+
+    "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.2", "", { "dependencies": { "vscode-jsonrpc": "9.0.1", "vscode-languageserver-types": "3.18.0" } }, "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg=="],
+
+    "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
+
+    "vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
+
+    "vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
+
+    "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
+
+    "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+
+    "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
+
+    "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
+
+    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+    "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
+
+    "yaml-language-server": ["yaml-language-server@1.23.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-i18n": "^4.2.0", "prettier": "^3.8.1", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.8.3" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A=="],
+
+    "yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
+
+    "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
+
+    "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
+
+    "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+
+    "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
+    "@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+    "@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@2.0.0-alpha.3", "", { "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g=="],
+
+    "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@2.0.0-alpha.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA=="],
+
+    "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
+
+    "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+    "astro/magic-string": ["magic-string@1.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g=="],
+
+    "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+    "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
+
+    "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+    "estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+    "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+    "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+    "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
+
+    "vscode-css-languageservice/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
+
+    "vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
+
+    "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+    "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
+
+    "yaml-language-server/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
+
+    "@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@2.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ=="],
+
+    "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
+
+    "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
+
+    "vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
+
+    "vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
+  }
+}
diff --git a/site/package.json b/site/package.json
new file mode 100644
index 0000000..ad0a5e9
--- /dev/null
+++ b/site/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "mdd-site",
+  "type": "module",
+  "version": "0.0.1",
+  "scripts": {
+    "dev": "astro dev",
+    "build": "astro build",
+    "preview": "astro preview",
+    "astro": "astro"
+  },
+  "dependencies": {
+    "@astrojs/starlight": "^0.41.0",
+    "@fontsource/merriweather": "^5.2.14",
+    "@fontsource/merriweather-sans": "^5.2.7",
+    "astro": "^7.0.0",
+    "sharp": "^0.35.3",
+    "starlight-llms-txt": "^0.11.0",
+    "unist-util-visit": "^5.1.0"
+  },
+  "devDependencies": {
+    "@astrojs/check": "^0.9.9",
+    "typescript": "^6"
+  }
+}
diff --git a/site/public/favicon.png b/site/public/favicon.png
new file mode 100644
index 0000000..8fb5e72
Binary files /dev/null and b/site/public/favicon.png differ
diff --git a/site/src/assets/mdd-logo-dark.png b/site/src/assets/mdd-logo-dark.png
new file mode 100644
index 0000000..807d743
Binary files /dev/null and b/site/src/assets/mdd-logo-dark.png differ
diff --git a/site/src/assets/mdd-logo.png b/site/src/assets/mdd-logo.png
new file mode 100644
index 0000000..0a37a84
Binary files /dev/null and b/site/src/assets/mdd-logo.png differ
diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro
new file mode 100644
index 0000000..73d8fb5
--- /dev/null
+++ b/site/src/components/Footer.astro
@@ -0,0 +1,26 @@
+---
+// Overrides Starlight's default Footer to add the site's build date.
+//
+// S06 is explicit that this is the *build* date, not the commit sha: a sha
+// implies the pages were verified against that commit, which they were not.
+// `lastUpdated` (Starlight's own, git-derived, still shown via the default
+// Footer below) says when a given page's content last changed; this says
+// when the site itself was produced. `new Date()` runs once, at build time,
+// in Astro's frontmatter script, so every page gets the same value from a
+// single static build.
+import Default from '@astrojs/starlight/components/Footer.astro';
+
+const buildDate = new Date().toISOString().slice(0, 10);
+---
+
+
+

Site built {buildDate}.

+ + diff --git a/site/src/content.config.ts b/site/src/content.config.ts new file mode 100644 index 0000000..d9ee8c9 --- /dev/null +++ b/site/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/site/src/content/docs/_drafts/.gitkeep b/site/src/content/docs/_drafts/.gitkeep new file mode 100644 index 0000000..0bf768c --- /dev/null +++ b/site/src/content/docs/_drafts/.gitkeep @@ -0,0 +1,4 @@ +# Article drafts land here, as _.md. +# The underscore on the FILENAME is what keeps them out of the build: +# docsLoader() globs **/[^_]*.{md,mdx}, which matches the filename, not the +# directory. Promotion is `git mv` to docs/articles/.md. diff --git a/site/src/content/docs/index.mdx b/site/src/content/docs/index.mdx new file mode 100644 index 0000000..743dc27 --- /dev/null +++ b/site/src/content/docs/index.mdx @@ -0,0 +1,56 @@ +--- +title: mdd +description: Bidirectional sync between Markdown-in-git and Confluence/SharePoint. +template: splash +# The only hand-authored page on the site, so the only one whose source really +# does live here. Every other page gets an editUrl from scripts/sync-docs.py. +editUrl: https://github.com/schubergphilis/mdd/edit/main/site/src/content/docs/index.mdx +hero: + tagline: Bidirectional sync between Markdown-in-git and Confluence/SharePoint. + actions: + - text: Start + link: /mdd/guide/install/ + icon: right-arrow + variant: primary + # Internal link, so a right arrow. `external`'s up-and-out arrow means + # "leaves this site", which the design record does not. + - text: Design record + link: /mdd/design-record/ + icon: right-arrow + variant: minimal +--- + +import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; + +`mdd` does near-lossless roundtrips between a git repository of Markdown and +remote document management systems, so engineers and AI agents can work with +Markdown while everyone else keeps using the tools they know. + + + `mdd` is written almost entirely by AI agents under human review. There is + no independent security review and no stable 1.0 release yet. Push + operations (like `mdd confluence sync-space`) can clobber production + systems. See the [Safety](/mdd/guide/safety/) page before you sync anything + that matters. + + +## Start here + + + + + + + diff --git a/site/src/plugins/exclude-design-record-from-llms-full.mjs b/site/src/plugins/exclude-design-record-from-llms-full.mjs new file mode 100644 index 0000000..4f09984 --- /dev/null +++ b/site/src/plugins/exclude-design-record-from-llms-full.mjs @@ -0,0 +1,61 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +// starlight-llms-txt@0.11.0's `exclude` option (configured alongside this +// integration in astro.config.mjs, as `starlightLlmsTxt({ exclude: [...] })`) +// is read only by its llms-small.txt route: `llms-small.txt.ts` passes +// `exclude: starlightLllmsTxtContext.exclude` into `generateLlmsTxt`, but +// `llms-full.txt.ts` calls the same `generateLlmsTxt` with no exclude/include +// option at all (verified against the package's source under +// `site/node_modules/starlight-llms-txt/`). So the design record still ends +// up in `llms-full.txt`. No plugin option closes that gap today - this +// integration re-opens the built file afterwards and drops the sections that +// came from a `spec/` or `research/` page, matched by the page's rendered +// `

` title (the same string `generateLlmsTxt` uses as that section's `# ` +// heading). Keep the `exclude` option configured too - it is already correct +// for `llms-small.txt`, and becomes correct here as well if a future plugin +// release forwards it to `llms-full.txt`. +// +// A first version of this split on a blank line followed by a `# ` heading - +// the boundary `generateLlmsTxt` puts between pages. That broke on a guide +// page whose body quotes a converted file's contents inside a fenced code +// block: the quoted content included a line that happened to look exactly +// like a page boundary, so the split (and then the title-based filter) took +// unrelated real pages down with the excluded ones. `pageSeparator` (below, +// also passed to `starlightLlmsTxt`) sidesteps that: it is a marker no real +// page content will ever contain, so splitting on it is exact. +export const PAGE_SEPARATOR = '\n\n\n\n'; + +export default function excludeDesignRecordFromLlmsFull() { + return { + name: 'exclude-design-record-from-llms-full', + hooks: { + 'astro:build:done': ({ pages, dir }) => { + const root = fileURLToPath(dir); + const excludedTitles = new Set(); + for (const { pathname } of pages) { + if (!pathname.startsWith('spec/') && !pathname.startsWith('research/')) continue; + const htmlPath = `${root}${pathname}index.html`; + if (!existsSync(htmlPath)) continue; + const match = /]*>(.*?)<\/h1>/s.exec(readFileSync(htmlPath, 'utf8')); + if (match) excludedTitles.add(match[1].replace(/<[^>]+>/g, '').trim()); + } + + // `llms-small.txt` is already correctly filtered by the plugin's + // own `exclude` option; it only needs the separator cleaned up. + const smallPath = `${root}llms-small.txt`; + if (existsSync(smallPath)) { + writeFileSync(smallPath, readFileSync(smallPath, 'utf8').replaceAll(PAGE_SEPARATOR, '\n\n')); + } + + const fullPath = `${root}llms-full.txt`; + if (!existsSync(fullPath)) return; + const kept = readFileSync(fullPath, 'utf8') + .split(PAGE_SEPARATOR) + .filter((section) => !excludedTitles.has(section.slice(2, section.indexOf('\n')).trim())) + .join('\n\n'); + writeFileSync(fullPath, kept); + }, + }, + }; +} diff --git a/site/src/plugins/remark-github-alerts.mjs b/site/src/plugins/remark-github-alerts.mjs new file mode 100644 index 0000000..31af4c9 --- /dev/null +++ b/site/src/plugins/remark-github-alerts.mjs @@ -0,0 +1,68 @@ +// Maps GitHub's alert blockquote syntax (`> [!NOTE]` and friends) onto +// Starlight asides, so a guide page renders natively on GitHub *and* as a +// coloured aside on the site. Starlight has no native support for GitHub +// alerts because the two variant sets do not map one-to-one; the mapping +// below is this project's explicit choice (see S06's "Markdown dialect for +// guide pages"): +// +// [!NOTE] -> note +// [!TIP] -> tip +// [!IMPORTANT] -> caution +// [!WARNING] -> caution +// [!CAUTION] -> danger +// +// Rather than hand-building the `