From 9c878c94a7144c8050d8fb6bbdfce35023113d65 Mon Sep 17 00:00:00 2001 From: Felirami Date: Mon, 27 Jul 2026 14:22:46 +0000 Subject: [PATCH 1/2] feat: generate versioned capability contract Co-authored-by: Felirami Signed-off-by: Felirami --- docs/capabilities/evidence.json | 43 ++++ docs/capabilities/v0.11.2.json | 175 ++++++++++++++++ docs/capabilities/v0.11.2.md | 74 +++++++ package.json | 2 + scripts/generate-capability-contract.mjs | 247 +++++++++++++++++++++++ test/capability-contract.test.js | 71 +++++++ 6 files changed, 612 insertions(+) create mode 100644 docs/capabilities/evidence.json create mode 100644 docs/capabilities/v0.11.2.json create mode 100644 docs/capabilities/v0.11.2.md create mode 100755 scripts/generate-capability-contract.mjs create mode 100644 test/capability-contract.test.js diff --git a/docs/capabilities/evidence.json b/docs/capabilities/evidence.json new file mode 100644 index 0000000..353ab32 --- /dev/null +++ b/docs/capabilities/evidence.json @@ -0,0 +1,43 @@ +{ + "asOf": "2026-07-27", + "release": { + "tag": "v0.11.2", + "publishedAt": "2026-07-24T06:22:17Z", + "source": "https://api.github.com/repos/arcabotai/clawfix/releases/tags/v0.11.2", + "assets": [ + "clawfix-tui-linux-x64-baseline.tar.gz", + "clawfix-tui-linux-x64.tar.gz", + "TUI-SHA256SUMS" + ] + }, + "platforms": { + "portableCli": [ + "macOS", + "Linux", + "WSL" + ], + "standaloneTui": [ + "Linux x64", + "Linux x64 baseline" + ] + }, + "validation": { + "supportedOpenClawVersions": [], + "supportStatement": "No broad OpenClaw compatibility range has been validated or asserted.", + "testedOpenClawVersions": [ + { + "version": "2026.6.11", + "scope": "Real-machine diagnostic and guarded-repair scenarios in the Blaxel lab.", + "source": "REVIEW.md" + } + ] + }, + "outcomes": { + "source": "https://clawfix.dev/api/stats", + "observedAt": "2026-07-27", + "totalDiagnoses": 205, + "recordedSuccesses": 5, + "unknown": 200, + "interpretation": "Unknown outcomes are missing follow-up, not failures. This snapshot does not establish a repair success rate." + } +} diff --git a/docs/capabilities/v0.11.2.json b/docs/capabilities/v0.11.2.json new file mode 100644 index 0000000..998dfee --- /dev/null +++ b/docs/capabilities/v0.11.2.json @@ -0,0 +1,175 @@ +{ + "schemaVersion": 1, + "generatedFrom": { + "packageMetadata": [ + "package.json", + "cli/package.json" + ], + "detectorCatalog": "src/known-issues.js", + "repairCatalog": "cli/core/repair-catalog.js", + "evidenceSnapshot": "docs/capabilities/evidence.json" + }, + "evidenceAsOf": "2026-07-27", + "release": { + "version": "0.11.2", + "tag": "v0.11.2", + "publishedAt": "2026-07-24T06:22:17Z", + "package": "clawfix", + "nodeEngine": ">=22.0.0", + "releaseMetadataSource": "https://api.github.com/repos/arcabotai/clawfix/releases/tags/v0.11.2", + "releaseAssets": [ + "clawfix-tui-linux-x64-baseline.tar.gz", + "clawfix-tui-linux-x64.tar.gz", + "TUI-SHA256SUMS" + ] + }, + "interfaces": { + "installer": { + "preparation": [ + "Download https://clawfix.dev/install to install-clawfix.sh.", + "Inspect the script and compare its SHA-256 with https://clawfix.dev/install/sha256.", + "Run bash install-clawfix.sh." + ], + "run": "clawfix", + "delivers": "Portable plain readline CLI installed under ~/.clawfix with a ~/.local/bin/clawfix launcher.", + "platforms": [ + "macOS", + "Linux", + "WSL" + ] + }, + "npx": { + "run": "npx clawfix@0.11.2", + "delivers": "Portable plain readline CLI.", + "platforms": [ + "macOS", + "Linux", + "WSL" + ] + }, + "standaloneTui": { + "bundledWithNpm": false, + "delivers": "Separate chat-first OpenTUI release asset.", + "platforms": [ + "Linux x64", + "Linux x64 baseline" + ], + "assets": [ + "clawfix-tui-linux-x64-baseline.tar.gz", + "clawfix-tui-linux-x64.tar.gz" + ] + } + }, + "detectors": { + "deterministicCount": 49, + "ids": [ + "auto-update-enabled-warning", + "auto-update-restart-loop", + "browser-port-binding", + "browser-relay-handshake-spam", + "browser-relay-not-listening", + "browser-relay-outdated", + "browser-relay-wrong-port", + "codex-service-tier-not-fast", + "codex-session-store-permission", + "codex-shell-home-mismatch", + "config-reload-sigterm-cascade", + "context-overflow", + "discord-allowlist-empty", + "duplicate-plugin", + "filevault-blocks-reboot", + "gateway-extended-downtime", + "gateway-not-listening", + "gateway-not-running", + "gateway-watchdog-missing", + "gateway-zombie", + "ggml-metal-crash", + "heartbeat-no-model-override", + "high-token-usage", + "invalid-gh-token-override", + "large-workspace-files", + "launchd-corrupted-state", + "macos-app-metadata-upgrade", + "matrix-sync-timeout-spam", + "mem0-graph-free", + "missing-agents-md", + "native-codex-timeout-boundary", + "no-compaction-config", + "no-context-pruning", + "no-hybrid-search", + "no-memory-files", + "no-memory-flush", + "no-soul", + "openclaw-node-engine-mismatch", + "orphan-tool-calls", + "oversized-error-log", + "pi-backed-openai-codex-route", + "plaintext-secrets-in-config", + "port-conflict", + "provider-prefix-unregistered", + "session-transcript-not-indexed", + "stale-bundled-plugin-load-paths", + "stale-plist-env-secrets", + "stale-self-paired-node", + "state-dir-migration" + ] + }, + "executableRepairs": { + "count": 5, + "items": [ + { + "id": "auto-update-enabled-warning", + "title": "Disable OpenClaw auto-update", + "risk": "low" + }, + { + "id": "gateway-loopback-no-auth", + "title": "Require a token on the gateway", + "risk": "medium" + }, + { + "id": "gateway-not-running", + "title": "Restart the OpenClaw gateway", + "risk": "low" + }, + { + "id": "no-hybrid-search", + "title": "Enable hybrid memory search", + "risk": "low" + }, + { + "id": "no-memory-flush", + "title": "Enable memory flush on compaction", + "risk": "low" + } + ], + "safetyBoundary": "Repairs are deterministic catalog entries with preview, apply, verify, and rollback contracts." + }, + "aiBoundary": { + "optional": true, + "serverControlled": true, + "purpose": "Explain unmatched diagnostic evidence and provide advisory analysis.", + "modelOutputCanBecomeExecutableShell": false, + "novelAiFindingsAutomaticallyRepairable": false + }, + "compatibility": { + "supportedOpenClawVersions": [], + "supportStatement": "No broad OpenClaw compatibility range has been validated or asserted.", + "testedOpenClawVersions": [ + { + "version": "2026.6.11", + "scope": "Real-machine diagnostic and guarded-repair scenarios in the Blaxel lab.", + "source": "REVIEW.md" + } + ] + }, + "measuredOutcomes": { + "source": "https://clawfix.dev/api/stats", + "observedAt": "2026-07-27", + "totalDiagnoses": 205, + "recordedSuccesses": 5, + "unknown": 200, + "interpretation": "Unknown outcomes are missing follow-up, not failures. This snapshot does not establish a repair success rate." + }, + "claimBoundary": "ClawFix diagnoses OpenClaw in one command and can apply only the supported repairs listed in this contract." +} diff --git a/docs/capabilities/v0.11.2.md b/docs/capabilities/v0.11.2.md new file mode 100644 index 0000000..630a25f --- /dev/null +++ b/docs/capabilities/v0.11.2.md @@ -0,0 +1,74 @@ +# ClawFix 0.11.2 capability contract + +Evidence snapshot: **2026-07-27**. This file is generated; edit `evidence.json` or shipped source, then run `npm run capabilities:generate`. + +## Product boundary + +ClawFix diagnoses OpenClaw in one command and can apply only the supported repairs listed in this contract. + +- Deterministic detectors: **49** +- Executable reviewed repairs: **5** +- Node.js: `>=22.0.0` + +## Interfaces delivered + +### Installer + +- Download https://clawfix.dev/install to install-clawfix.sh. +- Inspect the script and compare its SHA-256 with https://clawfix.dev/install/sha256. +- Run bash install-clawfix.sh. + +Run `clawfix`. It delivers: Portable plain readline CLI installed under ~/.clawfix with a ~/.local/bin/clawfix launcher. + +### npm + +Run `npx clawfix@0.11.2`. It delivers: Portable plain readline CLI. + +### Standalone TUI + +The TUI is **not bundled with npm**. ClawFix 0.11.2 publishes these separate assets: + +- `clawfix-tui-linux-x64-baseline.tar.gz` +- `clawfix-tui-linux-x64.tar.gz` + +Portable CLI platforms documented for this release: macOS, Linux, WSL. +Standalone TUI platforms evidenced by the release assets: Linux x64, Linux x64 baseline. + +## Executable repair catalog + +| ID | Action | Risk | +|---|---|---| +| `auto-update-enabled-warning` | Disable OpenClaw auto-update | low | +| `gateway-loopback-no-auth` | Require a token on the gateway | medium | +| `gateway-not-running` | Restart the OpenClaw gateway | low | +| `no-hybrid-search` | Enable hybrid memory search | low | +| `no-memory-flush` | Enable memory flush on compaction | low | + +Repairs are deterministic catalog entries with preview, apply, verify, and rollback contracts. + +## AI boundary + +AI analysis is optional and controlled by the selected server. It may explain unmatched evidence, but model output never becomes executable shell and a novel AI finding is not automatically repairable. + +## OpenClaw compatibility evidence + +No broad OpenClaw compatibility range has been validated or asserted. + +- OpenClaw 2026.6.11: Real-machine diagnostic and guarded-repair scenarios in the Blaxel lab. ([source](../../REVIEW.md)) + +## Measured outcomes + +The [public stats snapshot](https://clawfix.dev/api/stats) recorded: + +- Total diagnoses: **205** +- Recorded successes: **5** +- Unknown outcomes: **200** + +Unknown outcomes are missing follow-up, not failures. This snapshot does not establish a repair success rate. + +## Sources + +- Release metadata: https://api.github.com/repos/arcabotai/clawfix/releases/tags/v0.11.2 +- Detector catalog: [`src/known-issues.js`](../../src/known-issues.js) +- Repair catalog: [`cli/core/repair-catalog.js`](../../cli/core/repair-catalog.js) +- Machine-readable contract: [`v0.11.2.json`](./v0.11.2.json) diff --git a/package.json b/package.json index c6af663..be1dd63 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "start": "node --env-file-if-exists=.env src/server.js", "dev": "node --env-file-if-exists=.env --watch src/server.js", "test": "node scripts/run-node-tests.mjs", + "capabilities:generate": "node scripts/generate-capability-contract.mjs", + "capabilities:check": "node scripts/generate-capability-contract.mjs --check", "hash:script": "node scripts/update-script-hash.mjs", "test:tui": "bun test --cwd cli/tui", "prove:remediation": "node scripts/prove-remediation.mjs .", diff --git a/scripts/generate-capability-contract.mjs b/scripts/generate-capability-contract.mjs new file mode 100755 index 0000000..511369e --- /dev/null +++ b/scripts/generate-capability-contract.mjs @@ -0,0 +1,247 @@ +#!/usr/bin/env node + +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { repairCatalog } from '../cli/core/repair-catalog.js'; +import { KNOWN_ISSUES } from '../src/known-issues.js'; + +const ROOT = new URL('../', import.meta.url); +const OUTPUT_DIR = new URL('docs/capabilities/', ROOT); + +async function readJson(relativePath) { + return JSON.parse(await readFile(new URL(relativePath, ROOT), 'utf8')); +} + +function assertUnique(label, values) { + const unique = new Set(values); + if (unique.size !== values.length) { + throw new Error(`${label} contains duplicate ids`); + } +} + +export async function buildCapabilityContract() { + const [rootPackage, cliPackage, evidence] = await Promise.all([ + readJson('package.json'), + readJson('cli/package.json'), + readJson('docs/capabilities/evidence.json'), + ]); + + if (rootPackage.version !== cliPackage.version) { + throw new Error( + `root version ${rootPackage.version} does not match CLI version ${cliPackage.version}`, + ); + } + if (evidence.release.tag !== `v${cliPackage.version}`) { + throw new Error( + `evidence tag ${evidence.release.tag} does not match CLI version ${cliPackage.version}`, + ); + } + if (evidence.outcomes.recordedSuccesses + evidence.outcomes.unknown + !== evidence.outcomes.totalDiagnoses) { + throw new Error('outcome counts do not add up to totalDiagnoses'); + } + + const detectorIds = KNOWN_ISSUES.map(issue => issue.id).sort(); + const repairs = Object.values(repairCatalog) + .map(entry => ({ + id: entry.id, + title: entry.title, + risk: entry.risk, + })) + .sort((left, right) => left.id.localeCompare(right.id)); + + assertUnique('detector catalog', detectorIds); + assertUnique('repair catalog', repairs.map(repair => repair.id)); + + const version = cliPackage.version; + return { + schemaVersion: 1, + generatedFrom: { + packageMetadata: ['package.json', 'cli/package.json'], + detectorCatalog: 'src/known-issues.js', + repairCatalog: 'cli/core/repair-catalog.js', + evidenceSnapshot: 'docs/capabilities/evidence.json', + }, + evidenceAsOf: evidence.asOf, + release: { + version, + tag: evidence.release.tag, + publishedAt: evidence.release.publishedAt, + package: cliPackage.name, + nodeEngine: cliPackage.engines.node, + releaseMetadataSource: evidence.release.source, + releaseAssets: evidence.release.assets, + }, + interfaces: { + installer: { + preparation: [ + 'Download https://clawfix.dev/install to install-clawfix.sh.', + 'Inspect the script and compare its SHA-256 with https://clawfix.dev/install/sha256.', + 'Run bash install-clawfix.sh.', + ], + run: 'clawfix', + delivers: 'Portable plain readline CLI installed under ~/.clawfix with a ~/.local/bin/clawfix launcher.', + platforms: evidence.platforms.portableCli, + }, + npx: { + run: `npx ${cliPackage.name}@${version}`, + delivers: 'Portable plain readline CLI.', + platforms: evidence.platforms.portableCli, + }, + standaloneTui: { + bundledWithNpm: false, + delivers: 'Separate chat-first OpenTUI release asset.', + platforms: evidence.platforms.standaloneTui, + assets: evidence.release.assets.filter(asset => asset.endsWith('.tar.gz')), + }, + }, + detectors: { + deterministicCount: detectorIds.length, + ids: detectorIds, + }, + executableRepairs: { + count: repairs.length, + items: repairs, + safetyBoundary: 'Repairs are deterministic catalog entries with preview, apply, verify, and rollback contracts.', + }, + aiBoundary: { + optional: true, + serverControlled: true, + purpose: 'Explain unmatched diagnostic evidence and provide advisory analysis.', + modelOutputCanBecomeExecutableShell: false, + novelAiFindingsAutomaticallyRepairable: false, + }, + compatibility: evidence.validation, + measuredOutcomes: evidence.outcomes, + claimBoundary: 'ClawFix diagnoses OpenClaw in one command and can apply only the supported repairs listed in this contract.', + }; +} + +export function serializeCapabilityContract(contract) { + return `${JSON.stringify(contract, null, 2)}\n`; +} + +function markdownList(values) { + return values.map(value => `- ${value}`).join('\n'); +} + +export function renderCapabilityMarkdown(contract) { + const repairRows = contract.executableRepairs.items + .map(repair => `| \`${repair.id}\` | ${repair.title} | ${repair.risk} |`) + .join('\n'); + const testedVersions = contract.compatibility.testedOpenClawVersions + .map(item => `- OpenClaw ${item.version}: ${item.scope} ([source](../../${item.source}))`) + .join('\n'); + + return `# ClawFix ${contract.release.version} capability contract + +Evidence snapshot: **${contract.evidenceAsOf}**. This file is generated; edit \`evidence.json\` or shipped source, then run \`npm run capabilities:generate\`. + +## Product boundary + +${contract.claimBoundary} + +- Deterministic detectors: **${contract.detectors.deterministicCount}** +- Executable reviewed repairs: **${contract.executableRepairs.count}** +- Node.js: \`${contract.release.nodeEngine}\` + +## Interfaces delivered + +### Installer + +${markdownList(contract.interfaces.installer.preparation)} + +Run \`${contract.interfaces.installer.run}\`. It delivers: ${contract.interfaces.installer.delivers} + +### npm + +Run \`${contract.interfaces.npx.run}\`. It delivers: ${contract.interfaces.npx.delivers} + +### Standalone TUI + +The TUI is **not bundled with npm**. ClawFix ${contract.release.version} publishes these separate assets: + +${markdownList(contract.interfaces.standaloneTui.assets.map(asset => `\`${asset}\``))} + +Portable CLI platforms documented for this release: ${contract.interfaces.npx.platforms.join(', ')}. +Standalone TUI platforms evidenced by the release assets: ${contract.interfaces.standaloneTui.platforms.join(', ')}. + +## Executable repair catalog + +| ID | Action | Risk | +|---|---|---| +${repairRows} + +${contract.executableRepairs.safetyBoundary} + +## AI boundary + +AI analysis is optional and controlled by the selected server. It may explain unmatched evidence, but model output never becomes executable shell and a novel AI finding is not automatically repairable. + +## OpenClaw compatibility evidence + +${contract.compatibility.supportStatement} + +${testedVersions || '- No real-machine OpenClaw version evidence is recorded.'} + +## Measured outcomes + +The [public stats snapshot](${contract.measuredOutcomes.source}) recorded: + +- Total diagnoses: **${contract.measuredOutcomes.totalDiagnoses}** +- Recorded successes: **${contract.measuredOutcomes.recordedSuccesses}** +- Unknown outcomes: **${contract.measuredOutcomes.unknown}** + +${contract.measuredOutcomes.interpretation} + +## Sources + +- Release metadata: ${contract.release.releaseMetadataSource} +- Detector catalog: [\`src/known-issues.js\`](../../src/known-issues.js) +- Repair catalog: [\`cli/core/repair-catalog.js\`](../../cli/core/repair-catalog.js) +- Machine-readable contract: [\`v${contract.release.version}.json\`](./v${contract.release.version}.json) +`; +} + +export async function generateCapabilityContract({ check = false } = {}) { + const contract = await buildCapabilityContract(); + const outputs = [ + { + url: new URL(`v${contract.release.version}.json`, OUTPUT_DIR), + content: serializeCapabilityContract(contract), + }, + { + url: new URL(`v${contract.release.version}.md`, OUTPUT_DIR), + content: renderCapabilityMarkdown(contract), + }, + ]; + + if (check) { + for (const output of outputs) { + const actual = await readFile(output.url, 'utf8').catch(() => ''); + if (actual !== output.content) { + throw new Error( + `${fileURLToPath(output.url)} is stale; run npm run capabilities:generate`, + ); + } + } + return contract; + } + + await mkdir(OUTPUT_DIR, { recursive: true }); + await Promise.all(outputs.map(output => writeFile(output.url, output.content, 'utf8'))); + return contract; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + generateCapabilityContract({ check: process.argv.includes('--check') }) + .then(contract => { + const verb = process.argv.includes('--check') ? 'Verified' : 'Generated'; + console.log(`${verb} capability contract v${contract.release.version}`); + }) + .catch(error => { + console.error(`Capability contract generation failed: ${error.message}`); + process.exitCode = 1; + }); +} diff --git a/test/capability-contract.test.js b/test/capability-contract.test.js new file mode 100644 index 0000000..5b41636 --- /dev/null +++ b/test/capability-contract.test.js @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { repairCatalog } from '../cli/core/repair-catalog.js'; +import { + buildCapabilityContract, + renderCapabilityMarkdown, + serializeCapabilityContract, +} from '../scripts/generate-capability-contract.mjs'; +import { KNOWN_ISSUES } from '../src/known-issues.js'; + +test('capability contract derives release, detector, and repair truth from shipped source', async () => { + const contract = await buildCapabilityContract(); + const expectedRepairs = Object.keys(repairCatalog).sort(); + + assert.equal(contract.release.version, '0.11.2'); + assert.equal(contract.release.nodeEngine, '>=22.0.0'); + assert.equal(contract.detectors.deterministicCount, KNOWN_ISSUES.length); + assert.deepEqual( + contract.detectors.ids, + KNOWN_ISSUES.map(issue => issue.id).sort(), + ); + assert.equal(contract.executableRepairs.count, expectedRepairs.length); + assert.deepEqual( + contract.executableRepairs.items.map(repair => repair.id), + expectedRepairs, + ); +}); + +test('capability contract states the delivered interfaces and AI boundary without overclaiming', async () => { + const contract = await buildCapabilityContract(); + + assert.equal(contract.interfaces.npx.run, 'npx clawfix@0.11.2'); + assert.match(contract.interfaces.npx.delivers, /plain readline/i); + assert.equal(contract.interfaces.standaloneTui.bundledWithNpm, false); + assert.deepEqual(contract.interfaces.standaloneTui.assets, [ + 'clawfix-tui-linux-x64-baseline.tar.gz', + 'clawfix-tui-linux-x64.tar.gz', + ]); + assert.equal(contract.aiBoundary.modelOutputCanBecomeExecutableShell, false); + assert.equal(contract.aiBoundary.novelAiFindingsAutomaticallyRepairable, false); + assert.deepEqual(contract.compatibility.supportedOpenClawVersions, []); +}); + +test('measured outcomes preserve unknowns and never manufacture a success rate', async () => { + const contract = await buildCapabilityContract(); + + assert.equal(contract.measuredOutcomes.totalDiagnoses, 205); + assert.equal(contract.measuredOutcomes.recordedSuccesses, 5); + assert.equal(contract.measuredOutcomes.unknown, 200); + assert.match(contract.measuredOutcomes.interpretation, /not failures/i); + assert.equal('successRate' in contract.measuredOutcomes, false); +}); + +test('checked-in capability artifacts exactly match the generator', async () => { + const contract = await buildCapabilityContract(); + const [json, markdown] = await Promise.all([ + readFile( + new URL(`../docs/capabilities/v${contract.release.version}.json`, import.meta.url), + 'utf8', + ), + readFile( + new URL(`../docs/capabilities/v${contract.release.version}.md`, import.meta.url), + 'utf8', + ), + ]); + + assert.equal(json, serializeCapabilityContract(contract)); + assert.equal(markdown, renderCapabilityMarkdown(contract)); +}); From 6e9de0febd002e76c69e80649367a81a8fe13c1d Mon Sep 17 00:00:00 2001 From: Felirami Date: Mon, 27 Jul 2026 14:44:59 +0000 Subject: [PATCH 2/2] docs: link versioned capability contract Co-authored-by: Felirami Signed-off-by: Felirami --- README.md | 2 +- test/capability-contract.test.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 057c3b7..d2be35c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ClawFix scans locally, redacts recognized secrets, and matches failures against deterministic rules. Optional AI analysis can explain unmatched problems when it is configured on the selected server. Model output never becomes executable shell. -[Quick start](#quick-start) · [How it works](#how-it-works) · [Security](#security--transparency) · [Self-hosting](#self-hosting) · [Contributing](#contributing) +[Quick start](#quick-start) · [How it works](#how-it-works) · [Security](#security--transparency) · [Versioned capability contract](docs/capabilities/v0.11.2.md) · [Self-hosting](#self-hosting) · [Contributing](#contributing) ## Quick Start diff --git a/test/capability-contract.test.js b/test/capability-contract.test.js index 5b41636..f7fda56 100644 --- a/test/capability-contract.test.js +++ b/test/capability-contract.test.js @@ -69,3 +69,14 @@ test('checked-in capability artifacts exactly match the generator', async () => assert.equal(json, serializeCapabilityContract(contract)); assert.equal(markdown, renderCapabilityMarkdown(contract)); }); + +test('root README links to the generated current-version capability contract', async () => { + const contract = await buildCapabilityContract(); + const readme = await readFile(new URL('../README.md', import.meta.url), 'utf8'); + const expectedLink = [ + '[Versioned capability contract]', + `(docs/capabilities/v${contract.release.version}.md)`, + ].join(''); + + assert.ok(readme.includes(expectedLink), `README must link to ${expectedLink}`); +});