diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a4b2b..d824de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to ProDocs are documented here. The project follows ## [Unreleased] +## [1.2.1] - 2026-08-09 + +### Fixed + +- generate source and authored-knowledge links relative to nested audience + views instead of the output root; +- use section-specific empty states so a populated knowledge graph is not + described as having no authored knowledge; +- make `prodocs check` reject missing, escaping, malformed, or broken local + links in generated Markdown artifacts. + ## [1.2.0] - 2026-08-09 ### Added @@ -190,7 +201,8 @@ All notable changes to ProDocs are documented here. The project follows - Project-root containment for configured sources and generated output. - Versioned JSON Schemas for configuration and knowledge graph artifacts. -[Unreleased]: https://github.com/boyeesu/prodocs/compare/v1.2.0...HEAD +[Unreleased]: https://github.com/boyeesu/prodocs/compare/v1.2.1...HEAD +[1.2.1]: https://github.com/boyeesu/prodocs/compare/v1.2.0...v1.2.1 [1.2.0]: https://github.com/boyeesu/prodocs/compare/v1.1.2...v1.2.0 [1.1.2]: https://github.com/boyeesu/prodocs/compare/v1.1.1...v1.1.2 [1.1.1]: https://github.com/boyeesu/prodocs/compare/v1.1.0...v1.1.1 diff --git a/README.md b/README.md index 0f4a2c1..acea1cf 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ least 24 characters. | `prodocs doctor` | Require warning-free identity, evidence, freshness, integrations, and knowledge readiness | | `prodocs tutorial` | Create a safe, complete getting-started project | | `prodocs sync` | Incrementally index evidence and render all views | -| `prodocs check` | Fail when generated knowledge is stale | +| `prodocs check` | Fail when generated knowledge is stale or contains broken local links | | `prodocs status` | Show evidence, index, and knowledge health | | `prodocs context` | Return bounded task-shaped context | | `prodocs impact` | Map git changes through the knowledge graph | diff --git a/docs/MATURITY.md b/docs/MATURITY.md index b670464..f1b26c0 100644 --- a/docs/MATURITY.md +++ b/docs/MATURITY.md @@ -1,7 +1,7 @@ # Code maturity assessment Assessment date: 2026-08-02 -Target: ProDocs `1.2.0`, Node.js CLI and JavaScript library +Target: ProDocs `1.2.1`, Node.js CLI and JavaScript library ## Executive summary diff --git a/docs/SUPPLY_CHAIN.md b/docs/SUPPLY_CHAIN.md index fdc161e..568e6d2 100644 --- a/docs/SUPPLY_CHAIN.md +++ b/docs/SUPPLY_CHAIN.md @@ -5,7 +5,7 @@ ## Metadata - **Scan Date**: 2026-08-09 14:31 WAT -- **Project**: ProDocs 1.2.0 +- **Project**: ProDocs 1.2.1 - **Repositories Scanned**: 3 direct dependency repositories - **Total Dependencies**: 6 production packages excluding ProDocs - **Scan Duration**: approximately 2 minutes diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 8b5c2b8..26db6f2 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -53,6 +53,8 @@ The production gate runs all of the following: fail before any operation is applied; - Kourti Tech moves from 8 passes and 4 warnings to 12 passes and no warnings, with 20 entrypoints, 46/46 owned files, and supported starter knowledge. +- generated audience views resolve every local source and knowledge link from + their actual nested location, and `prodocs check` fails closed on link drift. CI repeats compatible gates across Node.js 20, 22, and 24, with Linux, macOS, and Windows coverage. Pull requests also generate a machine-readable impact diff --git a/docs/prodocs/CODE_MAP.md b/docs/prodocs/CODE_MAP.md index 6cefcdb..643c076 100644 --- a/docs/prodocs/CODE_MAP.md +++ b/docs/prodocs/CODE_MAP.md @@ -21,7 +21,7 @@ not prose claims. | [`src/adoption-inference.js`](../../src/adoption-inference.js) | JavaScript | 215 | @boyeesu | executeFile (value, L7)
SOURCE_EXTENSIONS (value, L8)
cleanText (function, L10)
lineFor (function, L17)
fieldFromSource (function, L21)
productNameFromHeading (function, L31)
descriptionFromReadme (function, L37)
optionalFile (function, L55) | | [`src/adoption.js`](../../src/adoption.js) | JavaScript | 438 | @boyeesu | CONFIG_PATH (value, L19)
CODEOWNERS_PATHS (value, L20)
adoptionHash (function, L26)
quoteYaml (function, L32)
slug (function, L36)
featureDocument (function, L45)
existingFile (function, L71)
configOperation (function, L82) | | [`src/benchmark.js`](../../src/benchmark.js) | JavaScript | 40 | @boyeesu | timed (function, L5)
benchmarkProject (function, L11) | -| [`src/cli.js`](../../src/cli.js) | JavaScript | 594 | @boyeesu | hasFlag (function, L40)
valueAfter (function, L44)
valuesAfter (function, L49)
integerAfter (function, L57)
help (function, L67)
rootFrom (function, L114)
readManifest (function, L121)
writeRepositoryJson (function, L135) | +| [`src/cli.js`](../../src/cli.js) | JavaScript | 602 | @boyeesu | hasFlag (function, L44)
valueAfter (function, L48)
valuesAfter (function, L53)
integerAfter (function, L61)
help (function, L71)
rootFrom (function, L118)
readManifest (function, L125)
writeRepositoryJson (function, L139) | | [`src/collectors/artifacts.js`](../../src/collectors/artifacts.js) | JavaScript | 114 | @boyeesu | HTTP_METHODS (value, L4)
lineFor (function, L15)
openApiCollector (value, L20)
databaseSchemaCollector (value, L92) | | [`src/collectors/contract.js`](../../src/collectors/contract.js) | JavaScript | 315 | @boyeesu | COLLECTOR_ID (value, L3)
SEVERITIES (value, L4)
compareStrings (function, L6)
requireNonEmptyString (function, L12)
requireString (function, L19)
requirePositiveInteger (function, L26)
uniqueSortedStrings (function, L33)
normalizeSymbol (function, L46) | | [`src/collectors/index.js`](../../src/collectors/index.js) | JavaScript | 27 | @boyeesu | defaultCollectorRegistry (value, L17)
collectSourceEvidence (function, L24) | @@ -32,6 +32,7 @@ not prose claims. | [`src/context.js`](../../src/context.js) | JavaScript | 362 | @boyeesu | HASH_PATTERN (value, L1)
isObject (function, L3)
fail (function, L7)
normalizedPath (function, L11)
normalizeRequestedPaths (function, L19)
matchesRequestedPath (function, L49)
priority (function, L58)
selectContext (function, L67) | | [`src/doctor.js`](../../src/doctor.js) | JavaScript | 252 | @boyeesu | executeFile (value, L10)
INTEGRATIONS (value, L11)
check (function, L20)
exists (function, L24)
gitRepository (function, L34)
diagnoseProject (function, L47)
summarize (function, L238) | | [`src/evaluate.js`](../../src/evaluate.js) | JavaScript | 109 | @boyeesu | readEvaluationSuite (function, L4)
evaluateContextSuite (function, L22) | +| [`src/generated-links.js`](../../src/generated-links.js) | JavaScript | 90 | @boyeesu | ROOT_MARKDOWN (value, L5)
audiencePath (function, L12)
generatedMarkdownPaths (function, L16)
localTargets (function, L23)
decodedPath (function, L34)
validateGeneratedLinks (function, L43)
documentationCheckState (function, L73) | | [`src/history.js`](../../src/history.js) | JavaScript | 46 | @boyeesu | executeFile (value, L4)
REF_PATTERN (value, L5)
readHistoricalGraph (function, L7) | | [`src/hooks.js`](../../src/hooks.js) | JavaScript | 68 | @boyeesu | executeFile (value, L7)
PRE_PUSH (value, L9)
git (function, L21)
installGitHooks (function, L33) | | [`src/impact.js`](../../src/impact.js) | JavaScript | 177 | @boyeesu | executeFile (value, L5)
REF_PATTERN (value, L6)
git (function, L8)
requireRef (function, L23)
resolveImpactRange (function, L30)
parseNameStatus (function, L54)
nodeMatchesChange (function, L72)
expandAffected (function, L80) | @@ -45,7 +46,7 @@ not prose claims. | [`src/policy.js`](../../src/policy.js) | JavaScript | 90 | @boyeesu | violation (function, L1)
evaluatePolicies (function, L11) | | [`src/proposals.js`](../../src/proposals.js) | JavaScript | 145 | @boyeesu | proposalHash (function, L12)
validateProposal (function, L18)
allowedKnowledgePath (function, L66)
applyProposal (function, L73)
readProposal (function, L139) | | [`src/providers.js`](../../src/providers.js) | JavaScript | 125 | @boyeesu | endpointUrl (function, L3)
templateProposal (function, L17)
openAiCompatibleProposal (function, L34) | -| [`src/render.js`](../../src/render.js) | JavaScript | 372 | @boyeesu | GENERATED_NOTICE (value, L6)
escapeCell (function, L9)
escapeHeading (function, L24)
sourceLink (function, L31)
languageSummary (function, L42)
renderSystemOverview (function, L49)
renderCodeMap (function, L118)
knowledgeNodes (function, L143) | +| [`src/render.js`](../../src/render.js) | JavaScript | 378 | @boyeesu | GENERATED_NOTICE (value, L6)
escapeCell (function, L9)
escapeHeading (function, L24)
sourceLink (function, L31)
languageSummary (function, L42)
renderSystemOverview (function, L49)
renderCodeMap (function, L118)
knowledgeNodes (function, L143) | | [`src/runbooks.js`](../../src/runbooks.js) | JavaScript | 119 | @boyeesu | publicStep (function, L7)
runbookPlan (function, L17)
execute (function, L37)
verifyRunbook (function, L71) | | [`src/safe-fs.js`](../../src/safe-fs.js) | JavaScript | 124 | @boyeesu | NO_FOLLOW (value, L6)
readRegularFile (function, L8)
createFileExclusive (function, L66)
atomicWriteFile (function, L80)
atomicWriteBuffer (function, L84)
atomicWrite (function, L91) | | [`src/scanner.js`](../../src/scanner.js) | JavaScript | 576 | @boyeesu | normalizeSource (function, L27)
isExcluded (function, L31)
matchesGlob (function, L42)
isIncluded (function, L111)
walk (function, L116)
discoverFiles (function, L159)
inferEntrypoint (function, L194)
languageForPath (function, L214) | @@ -54,14 +55,14 @@ not prose claims. | [`src/tutorial.js`](../../src/tutorial.js) | JavaScript | 90 | @boyeesu | FILES (value, L7)
createTutorial (function, L59) | | [`test/adoption.test.js`](../../test/adoption.test.js) | JavaScript | 395 | @boyeesu | executeFile (value, L19)
cli (value, L20)
fixture (function, L22) | | [`test/cli-roadmap.test.js`](../../test/cli-roadmap.test.js) | JavaScript | 428 | @boyeesu | executeFile (value, L11)
cli (value, L12)
execute (function, L14)
fullProject (function, L34) | -| [`test/cli.test.js`](../../test/cli.test.js) | JavaScript | 97 | @boyeesu | cli (value, L8)
execute (function, L10) | +| [`test/cli.test.js`](../../test/cli.test.js) | JavaScript | 106 | @boyeesu | cli (value, L8)
execute (function, L10) | | [`test/collectors.test.js`](../../test/collectors.test.js) | JavaScript | 336 | @boyeesu | fixtures (value, L12) | | [`test/config.test.js`](../../test/config.test.js) | JavaScript | 151 | @boyeesu | — | | [`test/context.test.js`](../../test/context.test.js) | JavaScript | 174 | @boyeesu | hashes (value, L11)
node (function, L19)
edge (function, L39)
graph (function, L51) | | [`test/module-resolution.test.js`](../../test/module-resolution.test.js) | JavaScript | 102 | @boyeesu | temporaryRoot (function, L11) | | [`test/package.test.js`](../../test/package.test.js) | JavaScript | 221 | @boyeesu | npmCli (value, L8)
execute (function, L20)
executeNpm (function, L49) | | [`test/paths.test.js`](../../test/paths.test.js) | JavaScript | 44 | @boyeesu | — | -| [`test/render.test.js`](../../test/render.test.js) | JavaScript | 64 | @boyeesu | graph (function, L9) | +| [`test/render.test.js`](../../test/render.test.js) | JavaScript | 106 | @boyeesu | graph (function, L10) | | [`test/roadmap.test.js`](../../test/roadmap.test.js) | JavaScript | 817 | @boyeesu | executeFile (value, L45)
project (function, L47)
config (function, L57) | | [`test/safe-fs.test.js`](../../test/safe-fs.test.js) | JavaScript | 48 | @boyeesu | — | | [`test/scanner.test.js`](../../test/scanner.test.js) | JavaScript | 297 | @boyeesu | fixture (function, L9) | diff --git a/docs/prodocs/KNOWLEDGE_HEALTH.md b/docs/prodocs/KNOWLEDGE_HEALTH.md index 12accdf..7d0a851 100644 --- a/docs/prodocs/KNOWLEDGE_HEALTH.md +++ b/docs/prodocs/KNOWLEDGE_HEALTH.md @@ -12,7 +12,7 @@ ## Unsupported knowledge -_No authored knowledge is available._ +_No unsupported authored knowledge._ ## Contradictions diff --git a/docs/prodocs/SYSTEM_OVERVIEW.md b/docs/prodocs/SYSTEM_OVERVIEW.md index 90f7697..76bd773 100644 --- a/docs/prodocs/SYSTEM_OVERVIEW.md +++ b/docs/prodocs/SYSTEM_OVERVIEW.md @@ -8,12 +8,12 @@ | Signal | Value | | --- | --- | -| Source files | 59 | -| Lines indexed | 10525 | -| Symbols indexed | 337 | -| Internal relationships | 461 | -| Languages | JavaScript (58), TypeScript (1) | -| Evidence snapshot | `724e8446e923` | +| Source files | 60 | +| Lines indexed | 10680 | +| Symbols indexed | 344 | +| Internal relationships | 472 | +| Languages | JavaScript (59), TypeScript (1) | +| Evidence snapshot | `f2eed986944f` | ## Entrypoints @@ -25,15 +25,15 @@ | File | Language | Connections | Owner | | --- | --- | ---: | --- | -| [`src/cli.js`](../../src/cli.js) | JavaScript | 33 | @boyeesu | +| [`src/cli.js`](../../src/cli.js) | JavaScript | 34 | @boyeesu | | [`src/scanner.js`](../../src/scanner.js) | JavaScript | 31 | @boyeesu | | [`test/roadmap.test.js`](../../test/roadmap.test.js) | JavaScript | 31 | @boyeesu | -| [`src/safe-fs.js`](../../src/safe-fs.js) | JavaScript | 26 | @boyeesu | +| [`src/safe-fs.js`](../../src/safe-fs.js) | JavaScript | 27 | @boyeesu | | [`src/security.js`](../../src/security.js) | JavaScript | 24 | @boyeesu | | [`src/constants.js`](../../src/constants.js) | JavaScript | 23 | @boyeesu | | [`src/mcp.js`](../../src/mcp.js) | JavaScript | 22 | @boyeesu | | [`src/adoption-command.js`](../../src/adoption-command.js) | JavaScript | 21 | @boyeesu | -| [`src/paths.js`](../../src/paths.js) | JavaScript | 20 | @boyeesu | +| [`src/paths.js`](../../src/paths.js) | JavaScript | 21 | @boyeesu | | [`src/adoption.js`](../../src/adoption.js) | JavaScript | 19 | @boyeesu | ## How to use this map @@ -42,4 +42,4 @@ - Agents should read `knowledge.json` or run `prodocs context --path --json`. - CI should run `prodocs check` to reject stale generated documentation. -Generated at 2026-08-09T15:30:18.325Z. +Generated at 2026-08-09T16:12:03.140Z. diff --git a/docs/prodocs/knowledge.json b/docs/prodocs/knowledge.json index 6b923ef..718b4fd 100644 --- a/docs/prodocs/knowledge.json +++ b/docs/prodocs/knowledge.json @@ -1,17 +1,17 @@ { "schemaVersion": 2, - "generatedAt": "2026-08-09T15:30:18.325Z", - "sourceHash": "724e8446e92338fad5e7e4efc85f4c6ec1ba9a7c1a1a04badec21984cc0a48f0", - "knowledgeHash": "046c1331399460fce8f6f74c6732c69ce3538cc9aa5d3294fe3e53eb8a3c05a7", - "inputHash": "2d02c593e300f02694296c2fe23d187cc173da74d5704e656f24c7942c1a09c2", + "generatedAt": "2026-08-09T16:12:03.140Z", + "sourceHash": "f2eed986944f4ae9dd30a76ffeb04d12f35a5e47d295b6a759e6f5bbcd184209", + "knowledgeHash": "b70212a7c9b8dfe1fccd5e6e358486e880d242b662f45fd2f317cdf154530f8b", + "inputHash": "0146a924e489595fe86a3ca481782523de4b887355f711e8e3a3e7aa74c5c245", "root": ".", "stats": { - "files": 59, - "lines": 10525, - "symbols": 337, - "edges": 461, + "files": 60, + "lines": 10680, + "symbols": 344, + "edges": 472, "languages": { - "JavaScript": 58, + "JavaScript": 59, "TypeScript": 1 }, "knowledge": { @@ -23,7 +23,7 @@ }, "index": { "backend": "sqlite", - "entries": 59 + "entries": 60 } }, "nodes": [ @@ -697,8 +697,8 @@ "type": "file", "path": "src/cli.js", "language": "JavaScript", - "contentHash": "f8e0572516d9ff7228b9b02c9c40f7e58c9157da7ef13798591b4c06ce96fa6a", - "lines": 594, + "contentHash": "e7d5aae1d495176358a9c61c1f37ac5fdb4b6a7861855827a650eea53785ef31", + "lines": 602, "owner": "@boyeesu", "entrypoint": true, "publicSurface": true, @@ -707,152 +707,152 @@ { "name": "hasFlag", "kind": "function", - "line": 40 + "line": 44 }, { "name": "valueAfter", "kind": "function", - "line": 44 + "line": 48 }, { "name": "valuesAfter", "kind": "function", - "line": 49 + "line": 53 }, { "name": "integerAfter", "kind": "function", - "line": 57 + "line": 61 }, { "name": "help", "kind": "function", - "line": 67 + "line": 71 }, { "name": "rootFrom", "kind": "function", - "line": 114 + "line": 118 }, { "name": "readManifest", "kind": "function", - "line": 121 + "line": 125 }, { "name": "writeRepositoryJson", "kind": "function", - "line": 135 + "line": 139 }, { "name": "init", "kind": "function", - "line": 147 + "line": 151 }, { "name": "doctor", "kind": "function", - "line": 163 + "line": 167 }, { "name": "tutorial", "kind": "function", - "line": 179 + "line": 183 }, { "name": "sync", "kind": "function", - "line": 191 + "line": 195 }, { "name": "freshness", "kind": "function", - "line": 215 + "line": 219 }, { "name": "check", "kind": "function", - "line": 232 + "line": 236 }, { "name": "status", "kind": "function", - "line": 259 + "line": 267 }, { "name": "context", "kind": "function", - "line": 281 + "line": 289 }, { "name": "impact", "kind": "function", - "line": 320 + "line": 328 }, { "name": "policy", "kind": "function", - "line": 348 + "line": 356 }, { "name": "proposal", "kind": "function", - "line": 362 + "line": 370 }, { "name": "propose", "kind": "function", - "line": 388 + "line": 396 }, { "name": "evaluate", "kind": "function", - "line": 415 + "line": 423 }, { "name": "benchmark", "kind": "function", - "line": 435 + "line": 443 }, { "name": "plugin", "kind": "function", - "line": 454 + "line": 462 }, { "name": "hooks", "kind": "function", - "line": 463 + "line": 471 }, { "name": "view", "kind": "function", - "line": 477 + "line": 485 }, { "name": "history", "kind": "function", - "line": 488 + "line": 496 }, { "name": "runbook", "kind": "function", - "line": 500 + "line": 508 }, { "name": "capabilities", "kind": "function", - "line": 527 + "line": 535 }, { "name": "serve", "kind": "function", - "line": 546 + "line": 554 }, { "name": "run", "kind": "function", - "line": 555 + "line": 563 } ], "trust": { @@ -1188,7 +1188,7 @@ "type": "file", "path": "src/constants.js", "language": "JavaScript", - "contentHash": "d7d3cccc5b72ffa31922cddf59bfeff33d814c19952df7d28f84ca5f274fdad1", + "contentHash": "c1ddfcc11f8706f5c763108af62f38d76af51504386440fdb530efd6f1abd6bb", "lines": 115, "owner": "@boyeesu", "entrypoint": false, @@ -1385,6 +1385,59 @@ "instructionSignals": [] } }, + { + "id": "file:src/generated-links.js", + "type": "file", + "path": "src/generated-links.js", + "language": "JavaScript", + "contentHash": "fd28ca6f0300e9f3c104ae918031f88ad74807a5e0845c0e9d0c6ddd8b807080", + "lines": 90, + "owner": "@boyeesu", + "entrypoint": false, + "publicSurface": false, + "role": "source", + "symbols": [ + { + "name": "ROOT_MARKDOWN", + "kind": "value", + "line": 5 + }, + { + "name": "audiencePath", + "kind": "function", + "line": 12 + }, + { + "name": "generatedMarkdownPaths", + "kind": "function", + "line": 16 + }, + { + "name": "localTargets", + "kind": "function", + "line": 23 + }, + { + "name": "decodedPath", + "kind": "function", + "line": 34 + }, + { + "name": "validateGeneratedLinks", + "kind": "function", + "line": 43 + }, + { + "name": "documentationCheckState", + "kind": "function", + "line": 73 + } + ], + "trust": { + "repositoryContent": "untrusted", + "instructionSignals": [] + } + }, { "id": "file:src/history.js", "type": "file", @@ -2014,8 +2067,8 @@ "type": "file", "path": "src/render.js", "language": "JavaScript", - "contentHash": "d9ee8c7f4c426828892f8d2c71e39f185c82e5b76208ad1558384e0d0d9893ff", - "lines": 372, + "contentHash": "60cc2715129e8c040f0cb23fccc3ca99db1fd5ac53c117f1e04bf9ccf94397d2", + "lines": 378, "owner": "@boyeesu", "entrypoint": false, "publicSurface": false, @@ -2069,22 +2122,22 @@ { "name": "featureMap", "kind": "function", - "line": 165 + "line": 170 }, { "name": "knowledgeHealth", "kind": "function", - "line": 196 + "line": 201 }, { "name": "renderAudienceView", "kind": "function", - "line": 235 + "line": 240 }, { "name": "writeArtifacts", "kind": "function", - "line": 316 + "line": 322 } ], "trust": { @@ -2476,8 +2529,8 @@ "type": "file", "path": "test/cli.test.js", "language": "JavaScript", - "contentHash": "329a878c9d7fa19af9ca121a597b5f21240cdf17c926584885572e175396b544", - "lines": 97, + "contentHash": "f1e1d332d66c4843248ec2df03de56c8efb6f554aa5e4f8fa23681777f73a3f5", + "lines": 106, "owner": "@boyeesu", "entrypoint": false, "publicSurface": false, @@ -2655,8 +2708,8 @@ "type": "file", "path": "test/render.test.js", "language": "JavaScript", - "contentHash": "2a101220e6179792fd10aad6256aee372184d31400cda0cd68f93d8dfd6631f5", - "lines": 64, + "contentHash": "7f0ece4e40204e0a004ef1d14960bbf9a1f215b6f9cfe6776d7a4b001ad1ea4f", + "lines": 106, "owner": "@boyeesu", "entrypoint": false, "publicSurface": false, @@ -2665,7 +2718,7 @@ { "name": "graph", "kind": "function", - "line": 9 + "line": 10 } ], "trust": { @@ -2789,7 +2842,7 @@ "path": "src/cli.js", "symbol": "run", "supported": true, - "contentHash": "f8e0572516d9ff7228b9b02c9c40f7e58c9157da7ef13798591b4c06ce96fa6a" + "contentHash": "e7d5aae1d495176358a9c61c1f37ac5fdb4b6a7861855827a650eea53785ef31" }, { "path": "src/index-store.js", @@ -3148,7 +3201,7 @@ "path": "src/render.js", "symbol": "renderAudienceView", "supported": true, - "contentHash": "d9ee8c7f4c426828892f8d2c71e39f185c82e5b76208ad1558384e0d0d9893ff" + "contentHash": "60cc2715129e8c040f0cb23fccc3ca99db1fd5ac53c117f1e04bf9ccf94397d2" }, { "path": "src/server.js", @@ -3245,7 +3298,7 @@ "path": "src/cli.js", "symbol": "run", "supported": true, - "contentHash": "f8e0572516d9ff7228b9b02c9c40f7e58c9157da7ef13798591b4c06ce96fa6a" + "contentHash": "e7d5aae1d495176358a9c61c1f37ac5fdb4b6a7861855827a650eea53785ef31" } ], "audiences": [ @@ -3425,6 +3478,15 @@ "reference": "src" } }, + { + "type": "affects", + "from": "claim:local-first", + "to": "file:src/generated-links.js", + "evidence": { + "source": "docs/knowledge/claims/local-first.md", + "reference": "src" + } + }, { "type": "affects", "from": "claim:local-first", @@ -3740,6 +3802,15 @@ "reference": "src" } }, + { + "type": "affects", + "from": "decision:deterministic-core", + "to": "file:src/generated-links.js", + "evidence": { + "source": "docs/knowledge/decisions/deterministic-core.md", + "reference": "src" + } + }, { "type": "affects", "from": "decision:deterministic-core", @@ -4145,6 +4216,15 @@ "reference": "src" } }, + { + "type": "affects", + "from": "feature:evidence-kernel", + "to": "file:src/generated-links.js", + "evidence": { + "source": "docs/knowledge/features/evidence-kernel.md", + "reference": "src" + } + }, { "type": "affects", "from": "feature:evidence-kernel", @@ -4478,6 +4558,15 @@ "reference": "src" } }, + { + "type": "affects", + "from": "invariant:untrusted-repository", + "to": "file:src/generated-links.js", + "evidence": { + "source": "docs/knowledge/invariants/untrusted-repository.md", + "reference": "src" + } + }, { "type": "affects", "from": "invariant:untrusted-repository", @@ -4793,6 +4882,15 @@ "reference": "src" } }, + { + "type": "affects", + "from": "runbook:production-verification", + "to": "file:src/generated-links.js", + "evidence": { + "source": "docs/knowledge/runbooks/production-verification.md", + "reference": "src" + } + }, { "type": "affects", "from": "runbook:production-verification", @@ -5279,6 +5377,15 @@ "specifier": "./evaluate.js" } }, + { + "type": "imports", + "from": "file:src/cli.js", + "to": "file:src/generated-links.js", + "evidence": { + "source": "src/cli.js", + "specifier": "./generated-links.js" + } + }, { "type": "imports", "from": "file:src/cli.js", @@ -5567,6 +5674,24 @@ "specifier": "./safe-fs.js" } }, + { + "type": "imports", + "from": "file:src/generated-links.js", + "to": "file:src/paths.js", + "evidence": { + "source": "src/generated-links.js", + "specifier": "./paths.js" + } + }, + { + "type": "imports", + "from": "file:src/generated-links.js", + "to": "file:src/safe-fs.js", + "evidence": { + "source": "src/generated-links.js", + "specifier": "./safe-fs.js" + } + }, { "type": "imports", "from": "file:src/hooks.js", @@ -6143,6 +6268,15 @@ "specifier": "../src/constants.js" } }, + { + "type": "imports", + "from": "file:test/render.test.js", + "to": "file:src/generated-links.js", + "evidence": { + "source": "test/render.test.js", + "specifier": "../src/generated-links.js" + } + }, { "type": "imports", "from": "file:test/render.test.js", @@ -6539,6 +6673,15 @@ "reference": "src/evaluate.js" } }, + { + "type": "owns", + "from": "owner:@boyeesu", + "to": "file:src/generated-links.js", + "evidence": { + "source": ".github/CODEOWNERS", + "reference": "src/generated-links.js" + } + }, { "type": "owns", "from": "owner:@boyeesu", @@ -7268,6 +7411,15 @@ "reference": "../src/constants.js" } }, + { + "type": "tests", + "from": "file:test/render.test.js", + "to": "file:src/generated-links.js", + "evidence": { + "source": "test/render.test.js", + "reference": "../src/generated-links.js" + } + }, { "type": "tests", "from": "file:test/render.test.js", diff --git a/docs/prodocs/manifest.json b/docs/prodocs/manifest.json index 7e1bebb..554973e 100644 --- a/docs/prodocs/manifest.json +++ b/docs/prodocs/manifest.json @@ -1,16 +1,16 @@ { "schemaVersion": 2, - "sourceHash": "724e8446e92338fad5e7e4efc85f4c6ec1ba9a7c1a1a04badec21984cc0a48f0", - "knowledgeHash": "046c1331399460fce8f6f74c6732c69ce3538cc9aa5d3294fe3e53eb8a3c05a7", - "inputHash": "2d02c593e300f02694296c2fe23d187cc173da74d5704e656f24c7942c1a09c2", - "generatedAt": "2026-08-09T15:30:18.325Z", + "sourceHash": "f2eed986944f4ae9dd30a76ffeb04d12f35a5e47d295b6a759e6f5bbcd184209", + "knowledgeHash": "b70212a7c9b8dfe1fccd5e6e358486e880d242b662f45fd2f317cdf154530f8b", + "inputHash": "0146a924e489595fe86a3ca481782523de4b887355f711e8e3a3e7aa74c5c245", + "generatedAt": "2026-08-09T16:12:03.140Z", "stats": { - "files": 59, - "lines": 10525, - "symbols": 337, - "edges": 461, + "files": 60, + "lines": 10680, + "symbols": 344, + "edges": 472, "languages": { - "JavaScript": 58, + "JavaScript": 59, "TypeScript": 1 }, "knowledge": { @@ -22,7 +22,7 @@ }, "index": { "backend": "sqlite", - "entries": 59 + "entries": 60 } } } diff --git a/docs/prodocs/views/coding-agents.md b/docs/prodocs/views/coding-agents.md index 49f9064..4f2317f 100644 --- a/docs/prodocs/views/coding-agents.md +++ b/docs/prodocs/views/coding-agents.md @@ -6,23 +6,23 @@ ## Entrypoints -- [`src/cli.js`](../../src/cli.js) -- [`src/collectors/index.js`](../../src/collectors/index.js) -- [`src/server.js`](../../src/server.js) +- [`src/cli.js`](../../../src/cli.js) +- [`src/collectors/index.js`](../../../src/collectors/index.js) +- [`src/server.js`](../../../src/server.js) ## Decisions and invariants -- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../knowledge/decisions/deterministic-core.md) -- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../knowledge/invariants/untrusted-repository.md) +- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../../knowledge/decisions/deterministic-core.md) +- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../../knowledge/invariants/untrusted-repository.md) ## Relevant authored knowledge -- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../knowledge/decisions/deterministic-core.md) -- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../knowledge/features/agent-interface.md) -- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../knowledge/features/automatic-adoption.md) -- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../knowledge/features/change-intelligence.md) -- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../knowledge/features/evidence-kernel.md) -- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../knowledge/invariants/untrusted-repository.md) +- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../../knowledge/decisions/deterministic-core.md) +- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../../knowledge/features/agent-interface.md) +- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../../knowledge/features/automatic-adoption.md) +- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../../knowledge/features/change-intelligence.md) +- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../../knowledge/features/evidence-kernel.md) +- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../../knowledge/invariants/untrusted-repository.md) -Evidence snapshot: `724e8446e923`; knowledge snapshot: -`046c13313994`. +Evidence snapshot: `f2eed986944f`; knowledge snapshot: +`b70212a7c9b8`. diff --git a/docs/prodocs/views/operations.md b/docs/prodocs/views/operations.md index 6e3e8a5..e01f506 100644 --- a/docs/prodocs/views/operations.md +++ b/docs/prodocs/views/operations.md @@ -6,9 +6,9 @@ ## Operational runbooks -- **Verify ProDocs for production** (`runbook:production-verification`, supported) — [source](../knowledge/runbooks/production-verification.md) +- **Verify ProDocs for production** (`runbook:production-verification`, supported) — [source](../../knowledge/runbooks/production-verification.md) Verification steps require an explicit content-bound approval hash before execution. -Evidence snapshot: `724e8446e923`; knowledge snapshot: -`046c13313994`. +Evidence snapshot: `f2eed986944f`; knowledge snapshot: +`b70212a7c9b8`. diff --git a/docs/prodocs/views/product.md b/docs/prodocs/views/product.md index eef6ac3..ffe0381 100644 --- a/docs/prodocs/views/product.md +++ b/docs/prodocs/views/product.md @@ -6,16 +6,16 @@ ## Product capabilities -- **Adoption and continuous assurance** (`feature:adoption-assurance`, supported) — [source](../knowledge/features/adoption-assurance.md) -- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../knowledge/features/agent-interface.md) -- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../knowledge/features/automatic-adoption.md) -- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../knowledge/features/change-intelligence.md) -- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../knowledge/features/evidence-kernel.md) -- **Audience-specific product knowledge** (`feature:product-views`, supported) — [source](../knowledge/features/product-views.md) +- **Adoption and continuous assurance** (`feature:adoption-assurance`, supported) — [source](../../knowledge/features/adoption-assurance.md) +- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../../knowledge/features/agent-interface.md) +- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../../knowledge/features/automatic-adoption.md) +- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../../knowledge/features/change-intelligence.md) +- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../../knowledge/features/evidence-kernel.md) +- **Audience-specific product knowledge** (`feature:product-views`, supported) — [source](../../knowledge/features/product-views.md) ## Evidence-backed behavior -- **Core workflows remain local-first** (`claim:local-first`, supported) — [source](../knowledge/claims/local-first.md) +- **Core workflows remain local-first** (`claim:local-first`, supported) — [source](../../knowledge/claims/local-first.md) -Evidence snapshot: `724e8446e923`; knowledge snapshot: -`046c13313994`. +Evidence snapshot: `f2eed986944f`; knowledge snapshot: +`b70212a7c9b8`. diff --git a/docs/prodocs/views/security.md b/docs/prodocs/views/security.md index a7a7a8f..f877aa1 100644 --- a/docs/prodocs/views/security.md +++ b/docs/prodocs/views/security.md @@ -6,8 +6,8 @@ ## Security decisions and invariants -- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../knowledge/decisions/deterministic-core.md) -- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../knowledge/invariants/untrusted-repository.md) +- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../../knowledge/decisions/deterministic-core.md) +- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../../knowledge/invariants/untrusted-repository.md) ## Trust signals @@ -15,5 +15,5 @@ - 1 indexed path(s) contain instruction-like text requiring review. - 0 authored item(s) need evidence. -Evidence snapshot: `724e8446e923`; knowledge snapshot: -`046c13313994`. +Evidence snapshot: `f2eed986944f`; knowledge snapshot: +`b70212a7c9b8`. diff --git a/docs/prodocs/views/support.md b/docs/prodocs/views/support.md index 2d56e98..0ae24e0 100644 --- a/docs/prodocs/views/support.md +++ b/docs/prodocs/views/support.md @@ -6,17 +6,17 @@ ## Customer-facing behavior -- **Adoption and continuous assurance** (`feature:adoption-assurance`, supported) — [source](../knowledge/features/adoption-assurance.md) -- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../knowledge/features/agent-interface.md) -- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../knowledge/features/automatic-adoption.md) -- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../knowledge/features/change-intelligence.md) -- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../knowledge/features/evidence-kernel.md) -- **Audience-specific product knowledge** (`feature:product-views`, supported) — [source](../knowledge/features/product-views.md) -- **Core workflows remain local-first** (`claim:local-first`, supported) — [source](../knowledge/claims/local-first.md) +- **Adoption and continuous assurance** (`feature:adoption-assurance`, supported) — [source](../../knowledge/features/adoption-assurance.md) +- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../../knowledge/features/agent-interface.md) +- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../../knowledge/features/automatic-adoption.md) +- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../../knowledge/features/change-intelligence.md) +- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../../knowledge/features/evidence-kernel.md) +- **Audience-specific product knowledge** (`feature:product-views`, supported) — [source](../../knowledge/features/product-views.md) +- **Core workflows remain local-first** (`claim:local-first`, supported) — [source](../../knowledge/claims/local-first.md) ## Support runbooks -- **Verify ProDocs for production** (`runbook:production-verification`, supported) — [source](../knowledge/runbooks/production-verification.md) +- **Verify ProDocs for production** (`runbook:production-verification`, supported) — [source](../../knowledge/runbooks/production-verification.md) -Evidence snapshot: `724e8446e923`; knowledge snapshot: -`046c13313994`. +Evidence snapshot: `f2eed986944f`; knowledge snapshot: +`b70212a7c9b8`. diff --git a/docs/prodocs/views/technical.md b/docs/prodocs/views/technical.md index 044c63c..8d623f7 100644 --- a/docs/prodocs/views/technical.md +++ b/docs/prodocs/views/technical.md @@ -6,27 +6,27 @@ ## Entrypoints -- [`src/cli.js`](../../src/cli.js) -- [`src/collectors/index.js`](../../src/collectors/index.js) -- [`src/server.js`](../../src/server.js) +- [`src/cli.js`](../../../src/cli.js) +- [`src/collectors/index.js`](../../../src/collectors/index.js) +- [`src/server.js`](../../../src/server.js) ## Decisions and invariants -- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../knowledge/decisions/deterministic-core.md) -- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../knowledge/invariants/untrusted-repository.md) +- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../../knowledge/decisions/deterministic-core.md) +- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../../knowledge/invariants/untrusted-repository.md) ## Relevant authored knowledge -- **Core workflows remain local-first** (`claim:local-first`, supported) — [source](../knowledge/claims/local-first.md) -- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../knowledge/decisions/deterministic-core.md) -- **Adoption and continuous assurance** (`feature:adoption-assurance`, supported) — [source](../knowledge/features/adoption-assurance.md) -- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../knowledge/features/agent-interface.md) -- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../knowledge/features/automatic-adoption.md) -- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../knowledge/features/change-intelligence.md) -- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../knowledge/features/evidence-kernel.md) -- **Audience-specific product knowledge** (`feature:product-views`, supported) — [source](../knowledge/features/product-views.md) -- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../knowledge/invariants/untrusted-repository.md) -- **Verify ProDocs for production** (`runbook:production-verification`, supported) — [source](../knowledge/runbooks/production-verification.md) - -Evidence snapshot: `724e8446e923`; knowledge snapshot: -`046c13313994`. +- **Core workflows remain local-first** (`claim:local-first`, supported) — [source](../../knowledge/claims/local-first.md) +- **Deterministic core with optional model providers** (`decision:deterministic-core`, supported) — [source](../../knowledge/decisions/deterministic-core.md) +- **Adoption and continuous assurance** (`feature:adoption-assurance`, supported) — [source](../../knowledge/features/adoption-assurance.md) +- **Agent-neutral context and MCP interface** (`feature:agent-interface`, supported) — [source](../../knowledge/features/agent-interface.md) +- **Evidence-backed automatic repository adoption** (`feature:automatic-adoption`, supported) — [source](../../knowledge/features/automatic-adoption.md) +- **Change intelligence** (`feature:change-intelligence`, supported) — [source](../../knowledge/features/change-intelligence.md) +- **Evidence-backed knowledge kernel** (`feature:evidence-kernel`, supported) — [source](../../knowledge/features/evidence-kernel.md) +- **Audience-specific product knowledge** (`feature:product-views`, supported) — [source](../../knowledge/features/product-views.md) +- **Repository content is untrusted data** (`invariant:untrusted-repository`, supported) — [source](../../knowledge/invariants/untrusted-repository.md) +- **Verify ProDocs for production** (`runbook:production-verification`, supported) — [source](../../knowledge/runbooks/production-verification.md) + +Evidence snapshot: `f2eed986944f`; knowledge snapshot: +`b70212a7c9b8`. diff --git a/package-lock.json b/package-lock.json index 8e2d6f7..5a3b0e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@danielesuga/prodocs", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@danielesuga/prodocs", - "version": "1.2.0", + "version": "1.2.1", "license": "Apache-2.0", "dependencies": { "@babel/parser": "7.28.5", diff --git a/package.json b/package.json index 86e88b6..6cf571b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@danielesuga/prodocs", - "version": "1.2.0", + "version": "1.2.1", "description": "Evidence-backed documentation and context infrastructure for codebases and coding agents.", "type": "module", "author": "ProDocs contributors", diff --git a/src/cli.js b/src/cli.js index dc2bb5e..91ae7e9 100644 --- a/src/cli.js +++ b/src/cli.js @@ -3,6 +3,10 @@ import path from "node:path"; import { runAdoptionCommand } from "./adoption-command.js"; import { loadConfig, writeDefaultConfig } from "./config.js"; import { buildContextPacket } from "./context.js"; +import { + documentationCheckState, + validateGeneratedLinks +} from "./generated-links.js"; import { benchmarkProject } from "./benchmark.js"; import { diagnoseProject } from "./doctor.js"; import { @@ -73,7 +77,7 @@ Core: prodocs doctor Diagnose production readiness and setup prodocs tutorial Create a safe getting-started project prodocs sync Incrementally index and refresh all views - prodocs check Fail when generated knowledge is stale + prodocs check Fail on stale docs or broken generated links prodocs status [--json] Show evidence and knowledge health prodocs context --path Return bounded task-shaped context @@ -231,8 +235,16 @@ async function freshness(root) { async function check(root, _args, json) { const result = await freshness(root); + const linkIssues = await validateGeneratedLinks(root, result.config); + const checkState = documentationCheckState( + result.fresh, + Boolean(result.manifest), + linkIssues + ); const output = { fresh: result.fresh, + validLinks: linkIssues.length === 0, + linkIssues, currentSourceHash: result.graph.sourceHash, documentedSourceHash: result.manifest?.sourceHash ?? null, currentKnowledgeHash: result.graph.knowledgeHash, @@ -242,18 +254,14 @@ async function check(root, _args, json) { knowledge: result.graph.stats.knowledge }; if (json) console.log(JSON.stringify(output, null, 2)); - else if (result.fresh) { + else if (checkState.ready) { console.log( `Documentation and authored knowledge are fresh (${result.graph.sourceHash.slice(0, 12)}).` ); } else { - console.error( - result.manifest - ? "Documentation is stale. Run `prodocs sync` and commit the result." - : "Documentation has not been generated. Run `prodocs sync`." - ); + console.error(checkState.message); } - if (!result.fresh) process.exitCode = 1; + if (!checkState.ready) process.exitCode = 1; } async function status(root, _args, json) { diff --git a/src/constants.js b/src/constants.js index 9c6bfbb..2aecc05 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,4 @@ -export const VERSION = "1.2.0"; +export const VERSION = "1.2.1"; export const DEFAULT_CONFIG = { $schema: diff --git a/src/generated-links.js b/src/generated-links.js new file mode 100644 index 0000000..bc5a797 --- /dev/null +++ b/src/generated-links.js @@ -0,0 +1,89 @@ +import path from "node:path"; +import { isInside, resolveOutputPath, resolveSourcePath } from "./paths.js"; +import { readRegularFile } from "./safe-fs.js"; + +const ROOT_MARKDOWN = [ + "SYSTEM_OVERVIEW.md", + "CODE_MAP.md", + "FEATURE_MAP.md", + "KNOWLEDGE_HEALTH.md" +]; + +function audiencePath(audience) { + return `views/${audience.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}.md`; +} + +export function generatedMarkdownPaths(config) { + return [ + ...ROOT_MARKDOWN, + ...config.documentation.audiences.map(audiencePath) + ]; +} + +function localTargets(markdown) { + return [...markdown.matchAll(/\]\(([^)\n]+)\)/g)] + .map((match) => match[1].trim()) + .filter( + (target) => + target && + !target.startsWith("#") && + !/^[a-z][a-z0-9+.-]*:/i.test(target) + ); +} + +function decodedPath(target) { + const withoutFragment = target.split(/[?#]/, 1)[0]; + try { + return decodeURIComponent(withoutFragment); + } catch { + throw new Error("invalid percent encoding"); + } +} + +export async function validateGeneratedLinks(root, config) { + const absoluteRoot = path.resolve(root); + const output = await resolveOutputPath(root, config.output); + const issues = []; + for (const artifact of generatedMarkdownPaths(config)) { + const file = path.join(output, artifact); + let markdown; + try { + markdown = (await readRegularFile(file, { maxBytes: 4 * 1024 * 1024 })) + .contents; + } catch (error) { + issues.push({ artifact, target: artifact, reason: error.message }); + continue; + } + for (const target of localTargets(markdown)) { + try { + const absoluteTarget = path.resolve(path.dirname(file), decodedPath(target)); + if (!isInside(absoluteRoot, absoluteTarget)) { + throw new Error("target escapes the project root"); + } + const repositoryPath = path.relative(absoluteRoot, absoluteTarget); + await resolveSourcePath(root, repositoryPath); + } catch (error) { + issues.push({ artifact, target, reason: error.message }); + } + } + } + return issues; +} + +export function documentationCheckState(fresh, hasManifest, issues) { + if (!fresh) { + return { + ready: false, + message: hasManifest + ? "Documentation is stale. Run `prodocs sync` and commit the result." + : "Documentation has not been generated. Run `prodocs sync`." + }; + } + if (issues.length > 0) { + return { + ready: false, + message: `Generated documentation contains ${issues.length} broken local link(s). Run \`prodocs sync\` and check again.` + }; + } + return { ready: true, message: null }; +} diff --git a/src/render.js b/src/render.js index 11d3ff0..683b6f8 100644 --- a/src/render.js +++ b/src/render.js @@ -150,14 +150,19 @@ function knowledgeNodes(graph, types = null) { ); } -function knowledgeList(nodes, config) { - if (nodes.length === 0) return "_No authored knowledge is available._"; +function knowledgeList( + nodes, + config, + output = config.output, + empty = "_No authored knowledge is available._" +) { + if (nodes.length === 0) return empty; return nodes .map((node) => { const support = node.evidence?.every((reference) => reference.supported) ? "supported" : "needs evidence"; - return `- **${escapeHeading(node.title)}** (\`${node.id}\`, ${support}) — [source](${sourceLink(config.output, node.path)})`; + return `- **${escapeHeading(node.title)}** (\`${node.id}\`, ${support}) — [source](${sourceLink(output, node.path)})`; }) .join("\n"); } @@ -218,7 +223,7 @@ function knowledgeHealth(graph, config) { ## Unsupported knowledge -${knowledgeList(unsupported, config)} +${knowledgeList(unsupported, config, config.output, "_No unsupported authored knowledge._")} ## Contradictions @@ -234,6 +239,7 @@ ${ export function renderAudienceView(graph, config, audience) { const normalized = String(audience).toLowerCase(); + const viewOutput = path.posix.join(config.output, "views"); const features = knowledgeNodes(graph, ["feature"]); const claims = knowledgeNodes(graph, ["claim"]); const decisions = knowledgeNodes(graph, ["decision", "invariant"]); @@ -247,26 +253,26 @@ export function renderAudienceView(graph, config, audience) { if (normalized === "product") { sections = `## Product capabilities -${knowledgeList(features, config)} +${knowledgeList(features, config, viewOutput, "_No authored product capabilities._")} ## Evidence-backed behavior -${knowledgeList(claims, config)}`; +${knowledgeList(claims, config, viewOutput, "_No authored behavioral claims._")}`; } else if (normalized === "support") { sections = `## Customer-facing behavior -${knowledgeList([...features, ...claims], config)} +${knowledgeList([...features, ...claims], config, viewOutput, "_No authored customer-facing behavior._")} ## Support runbooks -${knowledgeList(runbooks, config)}`; +${knowledgeList(runbooks, config, viewOutput, "_No authored support runbooks._")}`; } else if (normalized === "security") { const flagged = graph.nodes.filter( (node) => node.trust?.instructionSignals?.length > 0 ); sections = `## Security decisions and invariants -${knowledgeList(decisions, config)} +${knowledgeList(decisions, config, viewOutput, "_No authored security decisions or invariants._")} ## Trust signals @@ -276,7 +282,7 @@ ${knowledgeList(decisions, config)} } else if (normalized === "operations") { sections = `## Operational runbooks -${knowledgeList(runbooks, config)} +${knowledgeList(runbooks, config, viewOutput, "_No authored operational runbooks._")} Verification steps require an explicit content-bound approval hash before execution.`; } else { @@ -288,17 +294,17 @@ Verification steps require an explicit content-bound approval hash before execut ${entrypoints .map( (node) => - `- [\`${escapeCell(node.path)}\`](${sourceLink(config.output, node.path)})` + `- [\`${escapeCell(node.path)}\`](${sourceLink(viewOutput, node.path)})` ) .join("\n") || "_No entrypoints configured._"} ## Decisions and invariants -${knowledgeList(decisions, config)} +${knowledgeList(decisions, config, viewOutput, "_No authored decisions or invariants._")} ## Relevant authored knowledge -${knowledgeList(relevant, config)}`; +${knowledgeList(relevant, config, viewOutput, "_No authored knowledge targets this audience._")}`; } return `${GENERATED_NOTICE} diff --git a/test/cli.test.js b/test/cli.test.js index aee9286..4fe7825 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -27,6 +27,15 @@ test("init, sync, check, and stale detection form a complete loop", async (t) => assert.equal((await execute(["init"], root)).code, 0); assert.equal((await execute(["sync"], root)).code, 0); assert.equal((await execute(["check"], root)).code, 0); + await fs.appendFile( + path.join(root, "docs", "prodocs", "views", "technical.md"), + "\n[broken](../../missing.ts)\n" + ); + const brokenLinks = await execute(["check", "--json"], root); + assert.equal(brokenLinks.code, 1); + assert.equal(JSON.parse(brokenLinks.stdout).validLinks, false); + await execute(["sync"], root); + assert.equal((await execute(["check"], root)).code, 0); const firstManifest = await fs.readFile( path.join(root, "docs", "prodocs", "manifest.json"), "utf8" diff --git a/test/render.test.js b/test/render.test.js index 0fde230..454a4ac 100644 --- a/test/render.test.js +++ b/test/render.test.js @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import test from "node:test"; import { DEFAULT_CONFIG } from "../src/constants.js"; +import { validateGeneratedLinks } from "../src/generated-links.js"; import { writeArtifacts } from "../src/render.js"; function graph() { @@ -11,6 +12,7 @@ function graph() { schemaVersion: 1, generatedAt: "2026-07-29T00:00:00.000Z", sourceHash: "a".repeat(64), + knowledgeHash: "d".repeat(64), inputHash: "b".repeat(64), root: ".", stats: { @@ -18,7 +20,8 @@ function graph() { lines: 1, symbols: 0, edges: 0, - languages: { JavaScript: 1 } + languages: { JavaScript: 1 }, + knowledge: { total: 1, supported: 1, unsupported: 0, contradictions: 0 } }, nodes: [ { @@ -31,6 +34,16 @@ function graph() { owner: "", entrypoint: true, symbols: [] + }, + { + id: "feature:example", + type: "feature", + path: "docs/knowledge/features/example.md", + title: "Example feature", + status: "active", + audiences: ["product", "technical"], + customerImpact: "Example impact.", + evidence: [{ path: "src/a file.js", supported: true }] } ], edges: [] @@ -46,10 +59,19 @@ test("rendered links follow custom output directories and encode paths", async ( documentation: { productName: "", oneLineDescription: "A description.", - audiences: [] + audiences: ["product", "technical", "support", "security", "operations"] } }; + await fs.mkdir(path.join(root, "src")); + await fs.mkdir(path.join(root, "docs", "knowledge", "features"), { + recursive: true + }); + await fs.writeFile(path.join(root, "src", "a file.js"), "export {};\n"); + await fs.writeFile( + path.join(root, "docs", "knowledge", "features", "example.md"), + "# Example\n" + ); await writeArtifacts(root, config, graph()); const overview = await fs.readFile( path.join(root, ".generated", "docs", "SYSTEM_OVERVIEW.md"), @@ -60,4 +82,24 @@ test("rendered links follow custom output directories and encode paths", async ( assert.match(overview, /<Product>/); assert.match(overview, /<team>/); assert.doesNotMatch(overview, //); + + const product = await fs.readFile( + path.join(root, ".generated", "docs", "views", "product.md"), + "utf8" + ); + assert.match(product, /\.\.\/\.\.\/\.\.\/docs\/knowledge\/features\/example\.md/); + assert.match(product, /_No authored behavioral claims\._/); + const emptyStates = [ + ["support.md", "_No authored support runbooks._"], + ["security.md", "_No authored security decisions or invariants._"], + ["operations.md", "_No authored operational runbooks._"] + ]; + for (const [file, expected] of emptyStates) { + const view = await fs.readFile( + path.join(root, ".generated", "docs", "views", file), + "utf8" + ); + assert.match(view, new RegExp(expected.replaceAll(".", "\\."))); + } + assert.deepEqual(await validateGeneratedLinks(root, config), []); });