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), []);
});