diff --git a/.devia/01_ARCHITECTURE.md b/.devia/01_ARCHITECTURE.md index cab0f50..4ed9d5b 100644 --- a/.devia/01_ARCHITECTURE.md +++ b/.devia/01_ARCHITECTURE.md @@ -53,7 +53,7 @@ Content is data. Code reads it; code never encodes what a rule says. | No runtime dependencies | The tool that preaches `ARC-004` cannot carry a tree of its own | `.cursor/rules/devia.mdc`, `CONTRIBUTING.md` | | Own YAML subset parser | Frontmatter and impact maps only; devia writes the files it reads | `src/lib/yaml.mjs` header | | Rule IDs are stable and never reused | Citations in old reports must keep resolving | `GOVERNANCE.md`, `rules/LIFECYCLE.md` | -| The standard is vendored into `.devia/standard/` | Agents read it offline, with no package manager and no network | `src/lib/vendor.mjs` | +| Pinning the standard into `.devia/standard/` is opt-in | 391 copied files against 17 of memory buries what the folder is for, and every sync becomes a 391-file diff. `devia rules` reads the standard; `devia sync` pins it for offline agents and audits | `src/commands/init.mjs`, `.devia/11_GAPS.md` G8 | | The CLI version and the standard version move separately | A CLI fix must not force an adopter to re-pin the corpus | `src/lib/version.mjs` | | `check` scans what git carries, not what the disk holds | A P0 failure on an ignored build artefact is a false positive that teaches people to ignore the gate | `src/lib/git.mjs` | | Design rule IDs carried over unchanged | Consolidation must not invalidate existing citations | `MIGRATION.md` | @@ -63,5 +63,6 @@ Content is data. Code reads it; code never encodes what a rule says. ## Current vs target -This repository vendors nothing into its own `.devia/standard/`: it *is* the standard, so its -memory points at the repository files directly. Every other adopter gets the vendored copy. +This repository pins nothing into its own `.devia/standard/`: it *is* the standard, so its +memory points at the repository files directly. Adopters are in the same position by default, +and pin a copy only when they ask for one. diff --git a/.devia/02_SURFACES.md b/.devia/02_SURFACES.md index 41a472e..498b068 100644 --- a/.devia/02_SURFACES.md +++ b/.devia/02_SURFACES.md @@ -7,12 +7,12 @@ | Command | Purpose | Implementation | Exit code | |---|---|---|---| -| `devia init` | Create `.devia/`, vendor the standard, install adapters | `src/commands/init.mjs` | 0, or 2 on a bad profile or a detected root away from the cwd | +| `devia init` | Create `.devia/` and install adapters; `--vendor` also pins the standard | `src/commands/init.mjs` | 0, or 2 on a bad profile or a detected root away from the cwd | | `devia validate` | Memory integrity | `src/commands/validate.mjs` | 1 on any FAIL | | `devia check` | Readiness gates | `src/commands/check.mjs` | 1 on any P0 FAIL | | `devia doctor` | Adoption, drift, staleness | `src/commands/doctor.mjs` | 1 when there is no `.devia/` | | `devia rules` | Query the registry | `src/commands/rules.mjs` | 1 when `--id` is unknown | -| `devia sync` | Refresh the vendored standard | `src/commands/sync.mjs` | 1 without `.devia/` | +| `devia sync` | Pin the standard, or refresh a pinned copy | `src/commands/sync.mjs` | 1 without `.devia/` | | `devia skills` | Install adapters and the skill pack, per repository or `--global` | `src/commands/skills.mjs` | 2 on a bad action | | `devia gap` / `devia debt` | Registry lines | `src/commands/registry.mjs` | 1 when the id is unknown | diff --git a/.devia/04_PERMISSIONS.md b/.devia/04_PERMISSIONS.md index 9caf568..4e054ce 100644 --- a/.devia/04_PERMISSIONS.md +++ b/.devia/04_PERMISSIONS.md @@ -30,7 +30,7 @@ content stays. `--force` overrides both, and says which paths it took. | Operation | Where | Guard | |---|---|---| -| `rm -rf .devia/standard` before re-vendoring | `init`, `sync` | Only that one directory, which the tool owns | +| `rm -rf .devia/standard` before re-pinning | `init --vendor`, `sync` | Only that one directory, which the tool owns | | Overwriting memory files | `init --force` | Off by default, warned about in the output | | Removing a registry line | `gap`/`debt close` | Moves the line to the closed table, never deletes it | diff --git a/.devia/05_FLOWS.md b/.devia/05_FLOWS.md index d80cc4b..8687ec8 100644 --- a/.devia/05_FLOWS.md +++ b/.devia/05_FLOWS.md @@ -6,12 +6,12 @@ | Journey | Steps | Covered by | |---|---|---| -| Adopt | `npm i -D @schneiderjoseph/devia` → `devia init` → memory, adapters and vendored standard exist | `tests/cli.test.mjs` "init creates the memory…" | +| Adopt | `npm i -D @schneiderjoseph/devia` → `devia init` → memory and adapters exist, nothing pinned | `tests/cli.test.mjs` "init creates the memory…" | | Re-run safely | `devia init` on a repository that already has a filled memory keeps every decision | "init does not overwrite a filled memory" | | Verify | `devia validate` → structure, config, impact map, registry ids, placeholders | "validate reports placeholders…", "validate detects a reused registry id" | | Gate | `devia check` → P0 failures block, exit code 1 | "check blocks on P0 and explains why", "check finds a committed secret" | | Record | `devia gap add` / `devia debt add` / `debt close` → monotone ids, nothing deleted | "gap and debt lines get monotone ids…" | -| Upgrade | `devia sync` → vendored standard refreshed, pin updated, memory untouched | "sync refreshes the vendored standard" | +| Upgrade | `devia sync` → standard pinned or refreshed, pin updated, memory untouched | "sync pins the standard on demand" | | Cite | `devia rules --id SEC-001` → the full rule text | "rules can be queried by id and by filter" | ## Failure behaviour diff --git a/.devia/11_GAPS.md b/.devia/11_GAPS.md index 7205a35..3c728cb 100644 --- a/.devia/11_GAPS.md +++ b/.devia/11_GAPS.md @@ -22,3 +22,4 @@ Add one with `npx devia gap add "question"`. |---|---|---| | G6 | Should devia install its contract at user level for every agent, or stay per-repository outside Claude Code? | devia is for every agent — user-level install built for Claude Code, Codex, Cursor and Gemini in 0.3.0 | | G1 | Should `devia check` grow ecosystem-specific gates (Python, Go, Rust) or stay deliberately generic? | Reframed: the failure was not the ecosystem but the assumption that the manifest sits at the repository root — fixed in 0.4.0. Ecosystem-specific gates remain out of scope | +| G8 | Should devia pin a copy of the standard into every adopter repository by default? | No — opt-in via devia sync. Measured on a real repository: 391 pinned files against 17 of memory, tripling a 211-file project and turning every sync into a 391-file diff | diff --git a/.devia/README.md b/.devia/README.md index 534e4b0..944e3de 100644 --- a/.devia/README.md +++ b/.devia/README.md @@ -29,8 +29,9 @@ Machine files: [`devia.json`](devia.json) (profile, maturity, pinned version) an This repository **is** the standard, so its memory points at the repository files directly — [`../standard/`](../standard/), [`../rules/`](../rules/README.md) — and vendors nothing into -`.devia/standard/`. Every other adopter gets the vendored, version-pinned copy instead: the same -text for every agent, offline, no network and no package manager required. +`.devia/standard/`. Adopters are in the same position by default: they read the standard with +`devia rules`, and pin a version-locked copy with `devia sync` when an agent must read it +offline or an audit needs the exact wording. ## The two registries @@ -45,7 +46,7 @@ text for every agent, offline, no network and no package manager required. npx devia validate # structure, registries, placeholders npx devia doctor # is the memory older than the code? npx devia check # readiness gates -npx devia sync # refresh the vendored standard after an upgrade +npx devia sync # pin the standard under .devia/standard/, or refresh it ``` `.devia/` is committed. It is part of the repository, not a local scratch pad. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bc2d1a..14b1511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 0.5.0 — 2026-09-09 + +The standard is unchanged: `VERSION` stays at 0.1.0. + +### Pinning the standard is now opt-in + +`devia init` used to copy the whole standard into `.devia/standard/`. Measured on a real +repository: **391 pinned files against 17 of memory** — a folder whose purpose is to be read by +a human and an agent, in which 96% of the files were a copy nobody wrote. On a 211-file project +it tripled the repository, and every `devia sync` produced a 391-file diff in which a real +memory change was invisible. + +- `devia init` writes the memory and the adapters, and pins nothing: **17 files, 57 kB** +- `devia init --vendor` pins the copy for those who want it up front +- `devia sync` pins it on demand and refreshes it afterwards — that is now its first job, not + only its maintenance one +- `doctor` reports an unpinned standard as `INFO`, not a `WARN` to clear: the default is not a + defect +- The memory templates, the agent adapters and the skill read the rules with + `npx devia rules --id ` / `--domain ` instead of linking into a copy that may not + exist. A pinned copy is mentioned as what it is: optional + +Nothing to do when upgrading. An existing `.devia/standard/` is left alone, `devia sync` keeps +refreshing it, and only new `devia init` runs behave differently. Recorded as G8. + +### A P0 blocker comes from the priority cell, never from prose + +`MEM-DEBT-P0` matched `P0` anywhere in a debt row. A P1 line reading "becomes P0 once the +payment module ships" failed the gate, so a project with no P0 debt was told it was blocked by +one. The check now reads the priority cell. Found by writing a real project's debt registry. + ## 0.4.0 — 2026-09-09 The standard is unchanged: `VERSION` stays at 0.1.0, no adopter needs `devia sync`. diff --git a/MIGRATION.md b/MIGRATION.md index 305a257..3def568 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -58,5 +58,6 @@ Then: (undecided) and `.devia/12_DEBT.md` (decided, not built) — the split matters. 4. Replace `node scripts/production-check.mjs` in CI with `npx devia check`. -Nothing in the old repos is lost: the full text of both is vendored here, and `devia init` -vendors it again into the project under `.devia/standard/`. +Nothing in the old repos is lost: the full text of both ships with this package, readable with +`npx devia rules`, and `npx devia sync` pins a copy under `.devia/standard/` when a project +wants one on disk. diff --git a/README.md b/README.md index c0ba76d..f3b7b2f 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ npx devia doctor # adoption + staleness diagnosis ├── 14_INDEX.md # where to find what ├── impact-map.yaml # change type → files that must be updated ├── devia.json # profile, modules, maturity target, pinned version -└── standard/ # vendored, version-pinned copy of the standard +└── standard/ # optional: `devia sync` pins a copy of the standard here ``` Plus adapters so every agent gets the same contract: `AGENTS.md` (universal), `CLAUDE.md`, diff --git a/package.json b/package.json index 33aab45..2b142f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@schneiderjoseph/devia", - "version": "0.4.0", + "version": "0.5.0", "description": "One standard, one memory: engineering and design rules plus living project memory for AI coding agents", "type": "module", "license": "MIT", diff --git a/skills/devia/SKILL.md b/skills/devia/SKILL.md index 859831e..49558f3 100644 --- a/skills/devia/SKILL.md +++ b/skills/devia/SKILL.md @@ -40,8 +40,9 @@ the difference between a task and a guess (`AGT-002`). ## Step 2 — work under the rules -The rules are pinned in `.devia/standard/rules/` with stable IDs. The ones that stop most bad -changes: +The rules have stable IDs and are read with `npx devia rules --id `, or `--domain ` +for a whole area. A project that ran `devia sync` also has them on disk under +`.devia/standard/rules/`. The ones that stop most bad changes: | If you are about to… | Rule | |---|---| diff --git a/src/commands/check.mjs b/src/commands/check.mjs index 2a28817..e9b5277 100644 --- a/src/commands/check.mjs +++ b/src/commands/check.mjs @@ -154,9 +154,16 @@ function makeChecks(root, ctx) { run: () => { const text = read(path.join(root, ".devia", "12_DEBT.md")); if (text === null) return { kind: "SKIP", detail: "no debt registry" }; - const rows = (text.match(/^\|\s*D\d+\s*\|.*$/gm) || []).filter( - (r) => /\bP0\b/.test(r) && !/TODO\(devia\)/.test(r) - ); + // The priority is a cell, not a word somewhere in the row. Matching the whole line made + // a P1 line reading "becomes P0 once the payment module ships" fail the gate — a P0 + // blocker invented out of prose, on a project that had none. + const rows = (text.match(/^\|\s*D\d+\s*\|.*$/gm) || []).filter((row) => { + if (/TODO\(devia\)/.test(row)) return false; + return row + .split("|") + .slice(1, -1) + .some((cell) => cell.trim().toUpperCase() === "P0"); + }); return rows.length ? { kind: "FAIL", detail: `${rows.length} P0 debt line(s) open` } : { kind: "PASS" }; diff --git a/src/commands/doctor.mjs b/src/commands/doctor.mjs index 000848e..500b950 100644 --- a/src/commands/doctor.mjs +++ b/src/commands/doctor.mjs @@ -64,12 +64,11 @@ ${color.bold("devia doctor")} — adoption, drift and staleness const vendored = walk(path.join(deviaDir, "standard")).length; if (path.resolve(root) === path.resolve(packageRoot)) { status("SKIP", "vendored standard", "this repository is the standard"); + } else if (vendored) { + status("PASS", `standard pinned: ${vendored} files`); } else { - status( - vendored ? "PASS" : "WARN", - `vendored standard: ${vendored} files`, - vendored ? "" : "run `devia sync`" - ); + // Not pinning is the default, so absence is a fact to report, never a warning to clear. + status("INFO", "standard not pinned", "`devia rules` reads it; `devia sync` pins a copy"); } // Adapters diff --git a/src/commands/init.mjs b/src/commands/init.mjs index e0638ac..036f511 100644 --- a/src/commands/init.mjs +++ b/src/commands/init.mjs @@ -84,7 +84,7 @@ ${color.bold("devia init")} — create .devia/ in this repository --profile ${Object.keys(PROFILES).join(" | ")} --force overwrite existing memory files (dangerous: they hold your decisions) --no-agents do not write the agent adapters - --no-vendor do not vendor the standard into .devia/standard/ + --vendor pin a copy of the standard into .devia/standard/ (~390 files) --yes accept a detected root that is not the current directory `.trim()); return 0; @@ -163,17 +163,20 @@ ${color.bold("devia init")} — create .devia/ in this repository status("SKIP", "devia.json kept", "use --force to regenerate"); } - // 3. Vendored standard - if (flags.vendor === false || flags["no-vendor"]) { - status("SKIP", "standard not vendored", "--no-vendor"); - } else { + // 3. Pinned standard — opt-in. Vendoring writes ~390 files a project did not author, which + // buries the memory it is supposed to serve: on a real repository the ratio was 17 files of + // memory to 391 of copy, and every `sync` produced a 391-file diff. The rules stay reachable + // through `devia rules`, and `devia sync` pins the copy for whoever needs it offline. + if (flags.vendor && flags.vendor !== "false") { const files = vendorStandard(path.join(deviaDir, "standard"), { by: "devia init", cli, standard: version, date: vars.DATE, }); - status("PASS", `standard vendored: ${files} files`, `v${version}`); + status("PASS", `standard pinned: ${files} files`, `v${version}`); + } else { + status("SKIP", "standard not pinned", "`devia sync` writes .devia/standard/ when you need it"); } // 4. Agent adapters diff --git a/templates/agents/AGENTS.md b/templates/agents/AGENTS.md index 029871b..1140ed5 100644 --- a/templates/agents/AGENTS.md +++ b/templates/agents/AGENTS.md @@ -31,5 +31,5 @@ npx devia check # readiness gates Update `.devia/` in the same change (see `.devia/impact-map.yaml`), then report what you changed **and what you did not verify**. -Full standard: [`.devia/standard/AGENTS.md`](.devia/standard/AGENTS.md) · -rules by ID: [`.devia/standard/rules/README.md`](.devia/standard/rules/README.md) +Rules by ID: `npx devia rules --id SEC-001`, or by domain: `npx devia rules --domain database`. +A pinned copy lives under `.devia/standard/` only if this project ran `devia sync`. diff --git a/templates/agents/CLAUDE.md b/templates/agents/CLAUDE.md index 5a08fdc..690f107 100644 --- a/templates/agents/CLAUDE.md +++ b/templates/agents/CLAUDE.md @@ -31,4 +31,5 @@ npx devia check Report the checks that ran, the rule IDs involved, and what you did **not** verify. -Full contract: `.devia/standard/AGENTS.md`. Rules by ID: `.devia/standard/rules/README.md`. +Rules by ID: `npx devia rules --id SEC-001`, or by domain: `npx devia rules --domain database`. +A pinned copy lives under `.devia/standard/` only if this project ran `devia sync`. diff --git a/templates/project/14_INDEX.md b/templates/project/14_INDEX.md index c325459..a51a1b4 100644 --- a/templates/project/14_INDEX.md +++ b/templates/project/14_INDEX.md @@ -38,13 +38,17 @@ |---|---|---| | TODO(devia) | | | -## Standard (vendored) +## Standard -| Need | Where | +The standard is read through the CLI, not copied into this repository. + +| Need | Command | |---|---| -| Work contract | [`standard/AGENTS.md`](standard/AGENTS.md) | -| Rule by ID | [`standard/rules/README.md`](standard/rules/README.md) | -| Engineering policy | [`standard/standard/engineering/README.md`](standard/standard/engineering/README.md) | -| Design policy | [`standard/standard/design/README.md`](standard/standard/design/README.md) | -| Checklists | [`standard/checklists/README.md`](standard/checklists/README.md) | -| Memory doctrine | [`standard/MEMORY.md`](standard/MEMORY.md) | +| A rule by ID | `npx devia rules --id SEC-001` | +| Every rule in a domain | `npx devia rules --domain database --priority P0` | +| Work contract | `AGENTS.md` at this repository's root | +| Readiness gates | `npx devia check` | + +`npx devia sync` pins a version-locked copy under `standard/` when an agent must read it +offline, or when an audit needs the exact wording you built against. Add the paths here if you +do. diff --git a/templates/project/AGENTS.md b/templates/project/AGENTS.md index 093d27e..f67498b 100644 --- a/templates/project/AGENTS.md +++ b/templates/project/AGENTS.md @@ -8,8 +8,8 @@ This file is your contract. Violating it is a failed task. 1. Read [`10_NEVER_ALWAYS.md`](10_NEVER_ALWAYS.md) — what this project has already banned 2. Read [`00_OVERVIEW.md`](00_OVERVIEW.md) — what this project is 3. Read the memory file for the surface you are about to change ([`14_INDEX.md`](14_INDEX.md)) -4. Read the standard section for the domain: [`standard/AGENTS.md`](standard/AGENTS.md), - [`standard/rules/README.md`](standard/rules/README.md) +4. Read the rules for what you are touching: `npx devia rules --domain `, or + `npx devia rules --id ` for one Then work. Then update this memory in the same change. @@ -48,4 +48,4 @@ TODO(devia): add this project's own commands — install, dev, test, lint, migra - Not verified: ... ``` -Full contract, routing table and hard stops: [`standard/AGENTS.md`](standard/AGENTS.md). +Full contract, routing table and hard stops: the `AGENTS.md` at this repository's root. diff --git a/templates/project/README.md b/templates/project/README.md index c3c813a..92f4471 100644 --- a/templates/project/README.md +++ b/templates/project/README.md @@ -27,8 +27,11 @@ Created by `devia init` (devia {{DEVIA_VERSION}}, {{DATE}}). Machine files: [`devia.json`](devia.json) (profile, maturity, pinned version) and [`impact-map.yaml`](impact-map.yaml) (change type → files to update). -The standard itself is vendored, version-pinned, under [`standard/`](standard/) — the same text -for every agent, offline, no network and no package manager required. +The standard itself is not copied in here. Read it with `npx devia rules --id SEC-001` or +`npx devia rules --domain database`, which is the same text for every agent. If you need it on +disk — an agent with no network, or an audit that must show the exact wording you built against +— `npx devia sync` pins a version-locked copy under `standard/`, and `14_INDEX.md` then points +at it. ## The two registries @@ -43,7 +46,7 @@ for every agent, offline, no network and no package manager required. npx devia validate # structure, registries, placeholders npx devia doctor # is the memory older than the code? npx devia check # readiness gates -npx devia sync # refresh the vendored standard after an upgrade +npx devia sync # pin the standard under standard/, or refresh a pinned copy ``` `.devia/` is committed. It is part of the repository, not a local scratch pad. diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index 469cd9a..402cb49 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -115,6 +115,30 @@ test("check blocks on P0 and explains why", () => { } }); +test("a P0 blocker comes from the priority cell, never from prose", () => { + const dir = scratch(); + try { + devia(["init", "--root", dir], dir); + const file = path.join(dir, ".devia", "12_DEBT.md"); + const of = () => { + const out = devia(["check", "--root", dir, "--json"], dir, { allowFailure: true }).out; + return JSON.parse(out).results.find((r) => r.id === "MEM-DEBT-P0"); + }; + + // A P1 line that merely mentions P0 in its text is not a P0 blocker. + fs.appendFileSync(file, "\n| D1 | TST-001 | app | Becomes P0 once payments ship | P1 | |\n"); + assert.equal(of().kind, "PASS"); + + // A line whose priority cell is P0 blocks. + fs.appendFileSync(file, "| D2 | SEC-001 | app | No authorization on write | P0 | |\n"); + const blocked = of(); + assert.equal(blocked.kind, "FAIL"); + assert.match(blocked.detail, /1 P0 debt line/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + test("check finds a committed secret", () => { const dir = scratch(); try { @@ -292,12 +316,17 @@ test("rules can be queried by id and by filter", () => { } }); -test("sync refreshes the vendored standard", () => { +test("sync pins the standard on demand, and refreshes a pinned copy", () => { const dir = scratch(); try { devia(["init", "--root", dir], dir); const pinned = path.join(dir, ".devia", "standard", "rules", "memory", "MEM-001.md"); - assert.ok(fs.existsSync(pinned)); + assert.ok(!fs.existsSync(pinned), "init pins nothing"); + + // sync is the opt-in gesture: it writes the copy that init deliberately did not. + devia(["sync", "--root", dir], dir); + assert.ok(fs.existsSync(pinned), "sync pins the standard"); + fs.writeFileSync(pinned, "tampered\n"); devia(["sync", "--root", dir], dir); assert.match(fs.readFileSync(pinned, "utf8"), /Undecided is never coded/); @@ -311,7 +340,9 @@ test("sync refreshes the vendored standard", () => { test("the links inside a materialised .devia resolve", () => { const dir = scratch(); try { - devia(["init", "--root", dir], dir); + // --vendor, so the walk covers the pinned tree too: a link that resolves in this repository + // and not in the copy is a broken link shipped to every adopter who pins one. + devia(["init", "--root", dir, "--vendor"], dir); const deviaDir = path.join(dir, ".devia"); const files = []; @@ -339,7 +370,40 @@ test("the links inside a materialised .devia resolve", () => { } assert.deepEqual(broken, []); // Guard against the walk silently collapsing back to the memory files alone. - assert.ok(files.length > 300, `expected the vendored standard to be walked, saw ${files.length}`); + assert.ok(files.length > 300, `expected the pinned standard to be walked, saw ${files.length}`); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +// Pinning ~390 files a project did not author buries the memory it serves, so it is opt-in. The +// memory must therefore stand on its own: no link into a copy that was never written. +test("init does not pin the standard, and the memory still resolves without it", () => { + const dir = scratch(); + try { + const out = devia(["init", "--root", dir], dir).out; + const deviaDir = path.join(dir, ".devia"); + assert.ok(!fs.existsSync(path.join(deviaDir, "standard")), "nothing pinned by default"); + assert.match(out, /standard not pinned/); + + const files = fs.readdirSync(deviaDir).filter((f) => f.endsWith(".md")); + const linkRe = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g; + const broken = []; + for (const f of files) { + const text = fs.readFileSync(path.join(deviaDir, f), "utf8"); + let m; + while ((m = linkRe.exec(text))) { + const target = m[1].split("#")[0].split("?")[0]; + if (!target || /^(https?:|mailto:|tel:)/.test(target)) continue; + if (!fs.existsSync(path.resolve(deviaDir, target))) broken.push(`${f} -> ${target}`); + } + } + assert.deepEqual(broken, [], "the memory may not link into a standard nobody pinned"); + + // doctor reports the absence as a fact, not as something to fix. + const doctor = devia(["doctor", "--root", dir], dir).out; + assert.match(doctor, /standard not pinned/); + assert.doesNotMatch(doctor, /WARN\s+standard/); } finally { fs.rmSync(dir, { recursive: true, force: true }); }