@interf/compiler 0.33.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +122 -226
  2. package/dist/cli/commands/agents.js +1 -32
  3. package/dist/cli/commands/benchmark.d.ts +2 -3
  4. package/dist/cli/commands/benchmark.js +1 -31
  5. package/dist/cli/commands/build-plan.js +26 -50
  6. package/dist/cli/commands/build.d.ts +2 -3
  7. package/dist/cli/commands/build.js +1 -31
  8. package/dist/cli/commands/graphs.js +177 -32
  9. package/dist/cli/commands/mcp.d.ts +1 -0
  10. package/dist/cli/commands/mcp.js +223 -126
  11. package/dist/cli/commands/project.js +10 -36
  12. package/dist/cli/commands/reset.d.ts +2 -3
  13. package/dist/cli/commands/reset.js +1 -22
  14. package/dist/cli/commands/runs.js +86 -33
  15. package/dist/cli/commands/status.js +3 -24
  16. package/dist/cli/commands/traces.js +1 -29
  17. package/dist/cli/commands/wizard.js +17 -29
  18. package/dist/cli/lib/http-client.d.ts +39 -0
  19. package/dist/cli/lib/http-client.js +73 -0
  20. package/dist/packages/build-plans/authoring/brief.d.ts +25 -4
  21. package/dist/packages/build-plans/authoring/build-plan-authoring.d.ts +42 -1
  22. package/dist/packages/build-plans/authoring/build-plan-authoring.js +470 -63
  23. package/dist/packages/build-plans/authoring/build-plan-edit-session.d.ts +9 -0
  24. package/dist/packages/build-plans/authoring/build-plan-edit-session.js +27 -10
  25. package/dist/packages/build-plans/authoring/build-plan-improvement.js +62 -8
  26. package/dist/packages/build-plans/authoring/lib/build-plan-edit-utils.d.ts +1 -0
  27. package/dist/packages/build-plans/package/build-plan-definitions.d.ts +0 -1
  28. package/dist/packages/build-plans/package/build-plan-definitions.js +5 -3
  29. package/dist/packages/build-plans/package/build-plan-stage-runner.d.ts +1 -0
  30. package/dist/packages/build-plans/package/build-plan-stage-runner.js +2 -1
  31. package/dist/packages/build-plans/package/builtin-build-plan.d.ts +2 -2
  32. package/dist/packages/build-plans/package/builtin-build-plan.js +3 -3
  33. package/dist/packages/build-plans/package/context-interface.d.ts +3 -0
  34. package/dist/packages/build-plans/package/context-interface.js +5 -5
  35. package/dist/packages/build-plans/package/interf-build-plan-package.js +22 -22
  36. package/dist/packages/build-plans/package/local-build-plans.d.ts +10 -5
  37. package/dist/packages/build-plans/package/local-build-plans.js +57 -32
  38. package/dist/packages/contracts/index.d.ts +4 -3
  39. package/dist/packages/contracts/index.js +2 -1
  40. package/dist/packages/contracts/lib/context-graph-layer.d.ts +161 -0
  41. package/dist/packages/contracts/lib/context-graph-layer.js +216 -0
  42. package/dist/packages/contracts/lib/project-paths.d.ts +7 -0
  43. package/dist/packages/contracts/lib/project-paths.js +9 -0
  44. package/dist/packages/contracts/lib/project-schema.d.ts +264 -1
  45. package/dist/packages/contracts/lib/project-schema.js +38 -13
  46. package/dist/packages/contracts/lib/schema.d.ts +556 -23
  47. package/dist/packages/contracts/lib/schema.js +279 -18
  48. package/dist/packages/contracts/utils/filesystem.d.ts +1 -0
  49. package/dist/packages/contracts/utils/filesystem.js +29 -1
  50. package/dist/packages/projects/lib/schema.d.ts +6 -8
  51. package/dist/packages/projects/lib/schema.js +3 -1
  52. package/dist/packages/projects/source-config.d.ts +0 -5
  53. package/dist/packages/projects/source-config.js +9 -22
  54. package/dist/packages/runtime/actions/fields.d.ts +4 -0
  55. package/dist/packages/runtime/actions/form-builders.js +79 -31
  56. package/dist/packages/runtime/actions/form-validators.js +9 -3
  57. package/dist/packages/runtime/actions/helpers.js +3 -3
  58. package/dist/packages/runtime/actions/registry.d.ts +1 -1
  59. package/dist/packages/runtime/actions/registry.js +1 -1
  60. package/dist/packages/runtime/actions/requests.d.ts +1 -1
  61. package/dist/packages/runtime/actions/requests.js +12 -6
  62. package/dist/packages/runtime/actions/schemas.d.ts +7 -0
  63. package/dist/packages/runtime/actions/schemas.js +1 -0
  64. package/dist/packages/runtime/agent-handoff.js +8 -7
  65. package/dist/packages/runtime/agents/lib/execution-profile.d.ts +14 -0
  66. package/dist/packages/runtime/agents/lib/execution-profile.js +23 -0
  67. package/dist/packages/runtime/agents/lib/execution.js +14 -8
  68. package/dist/packages/runtime/agents/lib/executors.d.ts +1 -0
  69. package/dist/packages/runtime/agents/lib/executors.js +11 -2
  70. package/dist/packages/runtime/agents/lib/logs.d.ts +10 -0
  71. package/dist/packages/runtime/agents/lib/logs.js +32 -8
  72. package/dist/packages/runtime/agents/lib/preflight.js +4 -1
  73. package/dist/packages/runtime/agents/lib/render.d.ts +18 -0
  74. package/dist/packages/runtime/agents/lib/render.js +44 -18
  75. package/dist/packages/runtime/agents/lib/shell-templates.js +105 -63
  76. package/dist/packages/runtime/agents/lib/shells.d.ts +29 -0
  77. package/dist/packages/runtime/agents/lib/shells.js +158 -32
  78. package/dist/packages/runtime/agents/lib/source-context-scan.d.ts +10 -0
  79. package/dist/packages/runtime/agents/lib/source-context-scan.js +388 -0
  80. package/dist/packages/runtime/agents/lib/status.js +1 -14
  81. package/dist/packages/runtime/agents/lib/string-utils.d.ts +16 -0
  82. package/dist/packages/runtime/agents/lib/string-utils.js +36 -0
  83. package/dist/packages/runtime/agents/lib/types.d.ts +1 -0
  84. package/dist/packages/runtime/agents/providers/codex.js +2 -0
  85. package/dist/packages/runtime/agents/role-executors.js +2 -1
  86. package/dist/packages/runtime/auth/session-store.js +11 -3
  87. package/dist/packages/runtime/benchmark-question-draft.d.ts +3 -0
  88. package/dist/packages/runtime/benchmark-question-draft.js +57 -28
  89. package/dist/packages/runtime/build/artifact-status.d.ts +1 -1
  90. package/dist/packages/runtime/build/artifact-status.js +1 -1
  91. package/dist/packages/runtime/build/build-evidence.d.ts +2 -1
  92. package/dist/packages/runtime/build/build-evidence.js +11 -5
  93. package/dist/packages/runtime/build/build-pipeline.js +89 -5
  94. package/dist/packages/runtime/build/build-stage-plan.js +3 -1
  95. package/dist/packages/runtime/build/build-stage-runner.js +169 -32
  96. package/dist/packages/runtime/build/build-target.d.ts +3 -0
  97. package/dist/packages/runtime/build/build-target.js +25 -1
  98. package/dist/packages/runtime/build/check-evaluator.d.ts +1 -1
  99. package/dist/packages/runtime/build/check-evaluator.js +655 -4
  100. package/dist/packages/runtime/build/context-graph-paths.d.ts +13 -0
  101. package/dist/packages/runtime/build/context-graph-paths.js +27 -0
  102. package/dist/packages/runtime/build/index.d.ts +2 -2
  103. package/dist/packages/runtime/build/index.js +2 -2
  104. package/dist/packages/runtime/build/inspect-map.d.ts +10 -0
  105. package/dist/packages/runtime/build/inspect-map.js +270 -0
  106. package/dist/packages/runtime/build/lib/schema.d.ts +246 -53
  107. package/dist/packages/runtime/build/lib/schema.js +173 -15
  108. package/dist/packages/runtime/build/native-entrypoint.d.ts +2 -0
  109. package/dist/packages/runtime/build/native-entrypoint.js +286 -0
  110. package/dist/packages/runtime/build/runtime-contracts.js +9 -3
  111. package/dist/packages/runtime/build/runtime-log-paths.d.ts +3 -0
  112. package/dist/packages/runtime/build/runtime-log-paths.js +16 -0
  113. package/dist/packages/runtime/build/runtime-prompt.js +6 -4
  114. package/dist/packages/runtime/build/runtime-runs.js +63 -10
  115. package/dist/packages/runtime/build/runtime-types.d.ts +4 -1
  116. package/dist/packages/runtime/build/runtime.d.ts +3 -1
  117. package/dist/packages/runtime/build/runtime.js +3 -1
  118. package/dist/packages/runtime/build/source-files.js +11 -2
  119. package/dist/packages/runtime/build/source-inventory.d.ts +1 -0
  120. package/dist/packages/runtime/build/source-inventory.js +246 -7
  121. package/dist/packages/runtime/build/source-manifest.d.ts +11 -0
  122. package/dist/packages/runtime/build/source-manifest.js +30 -2
  123. package/dist/packages/runtime/build/stage-evidence.js +80 -11
  124. package/dist/packages/runtime/build/stage-manifest.d.ts +45 -0
  125. package/dist/packages/runtime/build/stage-manifest.js +1125 -0
  126. package/dist/packages/runtime/build/stage-reuse.js +12 -0
  127. package/dist/packages/runtime/build/stage-session.d.ts +81 -0
  128. package/dist/packages/runtime/build/stage-session.js +308 -0
  129. package/dist/packages/runtime/build/state-io.js +10 -11
  130. package/dist/packages/runtime/build/state-view.js +1 -1
  131. package/dist/packages/runtime/build/state.d.ts +1 -1
  132. package/dist/packages/runtime/build/state.js +1 -1
  133. package/dist/packages/runtime/build/summary-coverage-index.d.ts +21 -0
  134. package/dist/packages/runtime/build/summary-coverage-index.js +189 -0
  135. package/dist/packages/runtime/build/traces.js +3 -3
  136. package/dist/packages/runtime/build/validate-context-graph.d.ts +1 -1
  137. package/dist/packages/runtime/build/validate-context-graph.js +5 -5
  138. package/dist/packages/runtime/build/validate.d.ts +1 -1
  139. package/dist/packages/runtime/build/validate.js +1 -1
  140. package/dist/packages/runtime/client.d.ts +3 -3
  141. package/dist/packages/runtime/client.js +8 -13
  142. package/dist/packages/runtime/context-checks.js +13 -0
  143. package/dist/packages/runtime/context-graph-scaffold.js +2 -1
  144. package/dist/packages/runtime/context-graph-semantic-graph.d.ts +9 -0
  145. package/dist/packages/runtime/context-graph-semantic-graph.js +416 -0
  146. package/dist/packages/runtime/execution/lib/schema.d.ts +34 -31
  147. package/dist/packages/runtime/index.d.ts +2 -2
  148. package/dist/packages/runtime/index.js +1 -1
  149. package/dist/packages/runtime/native-run-handlers.d.ts +38 -0
  150. package/dist/packages/runtime/native-run-handlers.js +52 -33
  151. package/dist/packages/runtime/plan-artifact-contract.js +1 -1
  152. package/dist/packages/runtime/project-source-state.d.ts +4 -4
  153. package/dist/packages/runtime/project-source-state.js +5 -2
  154. package/dist/packages/runtime/project-store.d.ts +5 -0
  155. package/dist/packages/runtime/project-store.js +30 -3
  156. package/dist/packages/runtime/requested-artifacts.js +1 -1
  157. package/dist/packages/runtime/run-observability.js +9 -4
  158. package/dist/packages/runtime/runtime-action-proposals.js +3 -3
  159. package/dist/packages/runtime/runtime-build-plans.js +47 -3
  160. package/dist/packages/runtime/runtime-build-runs.js +9 -16
  161. package/dist/packages/runtime/runtime-caches.d.ts +26 -0
  162. package/dist/packages/runtime/runtime-caches.js +47 -0
  163. package/dist/packages/runtime/runtime-jobs.js +6 -6
  164. package/dist/packages/runtime/runtime-project-mutations.js +1 -0
  165. package/dist/packages/runtime/runtime-project-reads.d.ts +4 -1
  166. package/dist/packages/runtime/runtime-project-reads.js +229 -36
  167. package/dist/packages/runtime/runtime-proposal-helpers.js +6 -6
  168. package/dist/packages/runtime/runtime-resource-builders.d.ts +4 -2
  169. package/dist/packages/runtime/runtime-resource-builders.js +16 -14
  170. package/dist/packages/runtime/runtime-status.d.ts +14 -0
  171. package/dist/packages/runtime/runtime-status.js +15 -0
  172. package/dist/packages/runtime/runtime-verify-runs.js +6 -5
  173. package/dist/packages/runtime/runtime.d.ts +439 -22
  174. package/dist/packages/runtime/runtime.js +16 -2
  175. package/dist/packages/runtime/schemas/actions.d.ts +24 -0
  176. package/dist/packages/runtime/schemas/agents.d.ts +28 -0
  177. package/dist/packages/runtime/schemas/agents.js +33 -0
  178. package/dist/packages/runtime/schemas/build-plans.d.ts +181 -8
  179. package/dist/packages/runtime/schemas/build-plans.js +36 -2
  180. package/dist/packages/runtime/schemas/context-graphs.d.ts +1522 -0
  181. package/dist/packages/runtime/schemas/context-graphs.js +110 -0
  182. package/dist/packages/runtime/schemas/files.d.ts +7 -347
  183. package/dist/packages/runtime/schemas/files.js +1 -24
  184. package/dist/packages/runtime/schemas/index.d.ts +1 -0
  185. package/dist/packages/runtime/schemas/index.js +1 -0
  186. package/dist/packages/runtime/schemas/jobs.js +4 -0
  187. package/dist/packages/runtime/schemas/projects.d.ts +48 -21
  188. package/dist/packages/runtime/schemas/projects.js +34 -10
  189. package/dist/packages/runtime/schemas/runs.d.ts +1009 -240
  190. package/dist/packages/runtime/schemas/runs.js +17 -0
  191. package/dist/packages/runtime/service/openapi.js +1 -0
  192. package/dist/packages/runtime/service/operations.d.ts +1666 -145
  193. package/dist/packages/runtime/service/operations.js +147 -17
  194. package/dist/packages/runtime/service/routes.d.ts +11 -3
  195. package/dist/packages/runtime/service/routes.js +11 -3
  196. package/dist/packages/runtime/service/server-app-boot.js +2 -2
  197. package/dist/packages/runtime/service/server-helpers.d.ts +11 -0
  198. package/dist/packages/runtime/service/server-helpers.js +19 -0
  199. package/dist/packages/runtime/service/server-routes-action-proposals.js +4 -2
  200. package/dist/packages/runtime/service/server-routes-agents.js +19 -85
  201. package/dist/packages/runtime/service/server-routes-build-plans.js +14 -11
  202. package/dist/packages/runtime/service/server-routes-project-context.js +102 -7
  203. package/dist/packages/runtime/service/server-routes-project-jobs.js +19 -12
  204. package/dist/packages/runtime/service/server-routes-project-runs.js +5 -2
  205. package/dist/packages/runtime/service/server-routes-projects.js +6 -2
  206. package/dist/packages/runtime/service/server-routes-runs.js +11 -4
  207. package/dist/packages/runtime/verify/lib/schema.js +12 -0
  208. package/dist/packages/runtime/verify/test-file-guard.d.ts +2 -0
  209. package/dist/packages/runtime/verify/test-file-guard.js +29 -0
  210. package/dist/packages/runtime/verify/verify-execution.d.ts +7 -0
  211. package/dist/packages/runtime/verify/verify-execution.js +109 -35
  212. package/dist/packages/runtime/verify/verify-paths.d.ts +1 -0
  213. package/dist/packages/runtime/verify/verify-paths.js +4 -0
  214. package/dist/packages/runtime/verify/verify-specs.js +49 -39
  215. package/dist/packages/runtime/wire-schemas.d.ts +1 -1
  216. package/dist/packages/runtime/wire-schemas.js +1 -1
  217. package/package.json +2 -8
  218. package/public-repo/CONTRIBUTING.md +10 -3
  219. package/public-repo/README.md +122 -226
  220. package/public-repo/build-plans/interf-default/README.md +15 -12
  221. package/public-repo/build-plans/interf-default/build/stages/entrypoint/SKILL.md +74 -0
  222. package/public-repo/build-plans/interf-default/build/stages/knowledge/SKILL.md +95 -0
  223. package/public-repo/build-plans/interf-default/build/stages/summarize/SKILL.md +38 -5
  224. package/public-repo/build-plans/interf-default/build-plan.json +27 -23
  225. package/public-repo/build-plans/interf-default/build-plan.schema.json +24 -20
  226. package/public-repo/build-plans/interf-default/use/query/SKILL.md +8 -7
  227. package/public-repo/openapi/local-service.openapi.json +11637 -4213
  228. package/public-repo/skills/interf/SKILL.md +174 -134
  229. package/dist/packages/runtime/build/runtime-paths.d.ts +0 -8
  230. package/dist/packages/runtime/build/runtime-paths.js +0 -26
  231. package/dist/packages/runtime/build/state-paths.d.ts +0 -7
  232. package/dist/packages/runtime/build/state-paths.js +0 -22
  233. package/public-repo/build-plans/interf-default/build/stages/shape/SKILL.md +0 -34
  234. package/public-repo/build-plans/interf-default/build/stages/structure/SKILL.md +0 -28
@@ -0,0 +1,1125 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { basename, extname, join, relative } from "node:path";
4
+ import { ExpectedInputsFileSchema, GraphManifestSchema, MetricCountSchema, ReviewedInputSchema, ReviewedInputsFileSchema, ResourceRefSchema, StageManifestSchema, } from "../../contracts/lib/schema.js";
5
+ import { graphRelativePathPattern, knowledgeNoteKind, layerForPath, normalizeLayerPath, roleForLayerPath, roleForWritePath, } from "../../contracts/lib/context-graph-layer.js";
6
+ import { isFilesystemArtifact, listFilesRecursive } from "../../contracts/utils/filesystem.js";
7
+ import { parseJsonFrontmatter, readJsonFileWithSchema } from "../../contracts/utils/parse.js";
8
+ import { findBuildPlanContextArtifact, readBuildPlanContextFile, } from "./context-graph-schema.js";
9
+ import { buildPlanPackagePathForContextGraph, contextGraphRuntimeGraphManifestPath, contextGraphRuntimeStageExpectedInputsPath, contextGraphRuntimeStageManifestPath, contextGraphRuntimeStageReviewedInputsPath, contextGraphRuntimeStageRoot, } from "./context-graph-paths.js";
10
+ import { loadBuildStageInputs, loadContextGraphSourceManifest, } from "./source-manifest.js";
11
+ import { writeJsonAtomic, } from "./atomic-fs.js";
12
+ import { readExecutionStageLedgerHistory, } from "../runtime-persistence.js";
13
+ const WIKILINK_PATTERN = /\[\[([^\]\n]+)\]\]/g;
14
+ const MARKDOWN_LINK_PATTERN = /!?\[[^\]\n]*\]\(([^)\n]+)\)/g;
15
+ // Shared marker for the per-summary orphaned-summary `missing` reason a
16
+ // knowledge-role stage emits. The GraphManifest readiness rollup keys off it to
17
+ // collapse those per-summary entries into the single FINAL-state
18
+ // `knowledge:summary-backlinks` gate, so one logical orphan gap is counted once
19
+ // in readiness instead of twice (per-summary entry + metric gate).
20
+ const ORPHANED_SUMMARY_REASON = "is cited by a knowledge note's source_refs but never wikilinked; the summary is an orphaned island.";
21
+ // Single source for the "N knowledge note(s) link no summary" message shared by
22
+ // the `knowledge_summary_connectivity` metric detail (no trailing period, metric
23
+ // style) and the readiness `knowledge:summary-connectivity` gate reason (trailing
24
+ // period, sentence style). One formatter so the singular/plural agreement and
25
+ // wording can never drift between the two surfaces.
26
+ function formatDisconnectedNotesMessage(count, includePeriod) {
27
+ return `${count} knowledge note${count === 1 ? " links" : "s link"} no summary (disconnected from the source-backed layer)${includePeriod ? "." : ""}`;
28
+ }
29
+ // Path normalization + the graph-relative link pattern come from the central
30
+ // Context Graph layer model so this scanner and every other consumer share one
31
+ // canonical layer set. `normalizeRelativePath` is kept as a local alias so the
32
+ // many existing call sites below read unchanged.
33
+ const normalizeRelativePath = normalizeLayerPath;
34
+ function relativeToGraph(contextGraphPath, filePath) {
35
+ return normalizeRelativePath(relative(contextGraphPath, filePath));
36
+ }
37
+ function slug(value) {
38
+ const clean = value
39
+ .toLowerCase()
40
+ .replace(/['"]/g, "")
41
+ .replace(/[^a-z0-9]+/g, "-")
42
+ .replace(/^-+|-+$/g, "");
43
+ return clean.length > 0 ? clean : "resource";
44
+ }
45
+ // Resource ids stay <=90 slug chars, but the slug alone is NOT injective: two
46
+ // distinct keys whose first 90 slug chars are identical (e.g. sibling files
47
+ // `.../msg-XXXX-<long>/summary.md` and `.../manifest.md`, which only diverge
48
+ // after char ~90) would collapse to the same id. A non-unique id makes the
49
+ // StageManifest dedup guard reject an otherwise-valid manifest, failing the
50
+ // stage on every retry. When (and only when) the slug must be truncated, append
51
+ // a short stable hash of the FULL key so the id is collision-proof and still
52
+ // deterministic across runs (same path -> same id).
53
+ function resourceId(role, key) {
54
+ const full = slug(key);
55
+ if (full.length <= 90)
56
+ return `${role}:${full}`;
57
+ const hash = createHash("sha1").update(key).digest("hex").slice(0, 8);
58
+ return `${role}:${full.slice(0, 81)}-${hash}`;
59
+ }
60
+ function markdownTitle(path, frontmatter, body) {
61
+ const title = stringField(frontmatter.title) ?? stringField(frontmatter.label);
62
+ if (title)
63
+ return title;
64
+ const heading = body.split("\n").find((line) => /^#{1,3}\s+\S/.test(line));
65
+ if (heading)
66
+ return heading.replace(/^#{1,3}\s+/, "").trim();
67
+ return basename(path, extname(path));
68
+ }
69
+ function markdownSummary(frontmatter, body) {
70
+ const explicit = stringField(frontmatter.summary)
71
+ ?? stringField(frontmatter.abstract)
72
+ ?? stringField(frontmatter.description);
73
+ if (explicit)
74
+ return explicit.length > 240 ? `${explicit.slice(0, 237)}...` : explicit;
75
+ const paragraph = body
76
+ .split(/\n\s*\n/)
77
+ .map((block) => block.replace(/\s+/g, " ").trim())
78
+ .find((block) => block.length > 0 && !block.startsWith("#"));
79
+ if (!paragraph)
80
+ return null;
81
+ return paragraph.length > 240 ? `${paragraph.slice(0, 237)}...` : paragraph;
82
+ }
83
+ function stringField(value) {
84
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
85
+ }
86
+ function stringValues(value) {
87
+ if (Array.isArray(value))
88
+ return value.flatMap((item) => stringValues(item));
89
+ if (typeof value === "string")
90
+ return value.trim().length > 0 ? [value.trim()] : [];
91
+ if (value && typeof value === "object") {
92
+ const record = value;
93
+ return [
94
+ stringField(record.path),
95
+ stringField(record.locator),
96
+ stringField(record.source_path),
97
+ stringField(record.ref),
98
+ stringField(record.summary),
99
+ ].filter((item) => Boolean(item));
100
+ }
101
+ return [];
102
+ }
103
+ function sourceRefsFromFrontmatter(frontmatter) {
104
+ return Array.from(new Set([
105
+ ...stringValues(frontmatter.source_refs),
106
+ ...stringValues(frontmatter.source_ref),
107
+ ...stringValues(frontmatter.source_path),
108
+ ...stringValues(frontmatter.source),
109
+ ]));
110
+ }
111
+ function sourceRefsFromContent(content, frontmatter, knownSourceRefs) {
112
+ const refs = new Set(sourceRefsFromFrontmatter(frontmatter));
113
+ for (const sourceRef of knownSourceRefs) {
114
+ if (content.includes(sourceRef))
115
+ refs.add(sourceRef);
116
+ }
117
+ return [...refs];
118
+ }
119
+ function graphLinksFromFrontmatter(frontmatter) {
120
+ return [
121
+ ...stringValues(frontmatter.links),
122
+ ...stringValues(frontmatter.linked_notes),
123
+ ...stringValues(frontmatter.related),
124
+ ...stringValues(frontmatter.upstream),
125
+ ...stringValues(frontmatter.upstream_refs),
126
+ ];
127
+ }
128
+ function parseLinks(content, frontmatter) {
129
+ const links = [];
130
+ links.push(...graphLinksFromFrontmatter(frontmatter));
131
+ for (const match of content.matchAll(WIKILINK_PATTERN)) {
132
+ const raw = match[1]?.split("|")[0]?.split("#")[0]?.trim();
133
+ if (raw)
134
+ links.push(raw);
135
+ }
136
+ for (const match of content.matchAll(MARKDOWN_LINK_PATTERN)) {
137
+ const raw = match[1]?.split("#")[0]?.trim();
138
+ if (!raw || /^https?:\/\//i.test(raw))
139
+ continue;
140
+ links.push(raw);
141
+ }
142
+ for (const match of content.matchAll(graphRelativePathPattern())) {
143
+ const raw = match[1]?.split("#")[0]?.trim().replace(/[),.;:]+$/g, "");
144
+ if (raw)
145
+ links.push(raw);
146
+ }
147
+ return Array.from(new Set(links));
148
+ }
149
+ function collectMarkdownFiles(rootPath, relativeDir) {
150
+ const absoluteDir = join(rootPath, relativeDir);
151
+ if (!existsSync(absoluteDir))
152
+ return [];
153
+ const stat = statSync(absoluteDir);
154
+ if (stat.isFile()) {
155
+ return relativeDir.toLowerCase().endsWith(".md") ? [normalizeRelativePath(relativeDir)] : [];
156
+ }
157
+ if (!stat.isDirectory())
158
+ return [];
159
+ const files = [];
160
+ for (const entry of readdirSync(absoluteDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
161
+ if (entry.name.startsWith("."))
162
+ continue;
163
+ const childPath = normalizeRelativePath(join(relativeDir, entry.name));
164
+ if (entry.isDirectory()) {
165
+ files.push(...collectMarkdownFiles(rootPath, childPath));
166
+ }
167
+ else if (entry.isFile() && childPath.toLowerCase().endsWith(".md")) {
168
+ files.push(childPath);
169
+ }
170
+ }
171
+ return files;
172
+ }
173
+ // Note-kind label for a StageManifest ResourceRef. The layer (home / artifact /
174
+ // summary / knowledge / other) and the knowledge claim·entity sub-kind both come
175
+ // from the central `context-graph-layer` module so the `knowledge/claims/` /
176
+ // `claim-` conventions can never drift from the semantic-graph classifier. The
177
+ // dotted label vocabulary is this module's own; only the classification is
178
+ // shared. Precedence matches the original chain exactly: layer wins for
179
+ // home/artifact/summary, then claim/entity (recognized by leaf prefix even at
180
+ // the root), then any other knowledge note, then a plain markdown note.
181
+ function noteKindForPath(path) {
182
+ const layer = layerForPath(path);
183
+ if (layer === "home")
184
+ return "entrypoint.home";
185
+ if (layer === "artifacts")
186
+ return "entrypoint.artifact";
187
+ if (layer === "summaries")
188
+ return "summary.note";
189
+ const knowledgeKind = knowledgeNoteKind(path);
190
+ if (knowledgeKind === "claim")
191
+ return "knowledge.claim";
192
+ if (knowledgeKind === "entity")
193
+ return "knowledge.entity";
194
+ if (layer === "knowledge")
195
+ return "knowledge.note";
196
+ return "note.markdown";
197
+ }
198
+ // `roleForPath` and `layerRoleForWritePath` are the central Context Graph layer
199
+ // classifiers, imported as `roleForLayerPath` / `roleForWritePath`. They are the
200
+ // SINGLE source of truth every consumer (note role, stage role, disconnected /
201
+ // web checks, metrics) routes through — case-insensitive policing of where a
202
+ // note or artifact lands on disk, never derived from a stringly id. Local
203
+ // aliases keep the many call sites below reading unchanged.
204
+ const roleForPath = roleForLayerPath;
205
+ const layerRoleForWritePath = roleForWritePath;
206
+ /**
207
+ * Resolve a stage's artifact-id references (its `writes` or `reads`) to the set
208
+ * of layer roles they touch, by looking up each artifact's write *path* in the
209
+ * Build Plan schema. When a referenced token has no schema artifact (e.g. a
210
+ * plan that lists a bare layer path directly), the token is treated as a path
211
+ * so the layer is still recognized. Returns roles derived from disk location,
212
+ * with no reliance on stringly stage ids or artifact ids.
213
+ */
214
+ function stageArtifactRoles(contextGraphPath, artifactIds) {
215
+ const schema = readBuildPlanContextFile(buildPlanPackagePathForContextGraph(contextGraphPath));
216
+ const roles = new Set();
217
+ for (const artifactId of artifactIds) {
218
+ const artifact = schema ? findBuildPlanContextArtifact(schema, artifactId) : null;
219
+ roles.add(layerRoleForWritePath(artifact?.path ?? artifactId));
220
+ }
221
+ return roles;
222
+ }
223
+ function noteResource(contextGraphPath, path, stageId, knownSourceRefs = []) {
224
+ const content = readFileSync(join(contextGraphPath, path), "utf8");
225
+ const parsed = parseJsonFrontmatter(content);
226
+ const frontmatter = parsed?.frontmatter ?? {};
227
+ const body = parsed?.body ?? content;
228
+ const role = roleForPath(path);
229
+ const links = parseLinks(content, frontmatter)
230
+ .filter((link) => normalizeRelativePath(link).replace(/\.md$/i, "") !== normalizeRelativePath(path).replace(/\.md$/i, ""));
231
+ return ResourceRefSchema.parse({
232
+ id: resourceId(role, path),
233
+ role,
234
+ kind: noteKindForPath(path),
235
+ label: markdownTitle(path, frontmatter, body),
236
+ path,
237
+ ...(stageId ? { stage_id: stageId } : {}),
238
+ source_refs: sourceRefsFromContent(content, frontmatter, knownSourceRefs),
239
+ links,
240
+ metadata: {
241
+ ...(markdownSummary(frontmatter, body) ? { summary: markdownSummary(frontmatter, body) } : {}),
242
+ },
243
+ });
244
+ }
245
+ function knownSourceRefsForGraph(contextGraphPath) {
246
+ return loadContextGraphSourceManifest(contextGraphPath)?.files.map((file) => file.path) ?? [];
247
+ }
248
+ function listSummaryResources(contextGraphPath) {
249
+ const knownSourceRefs = knownSourceRefsForGraph(contextGraphPath);
250
+ return collectMarkdownFiles(contextGraphPath, "summaries")
251
+ .map((path) => noteResource(contextGraphPath, path, undefined, knownSourceRefs));
252
+ }
253
+ function listKnowledgeResources(contextGraphPath) {
254
+ const knownSourceRefs = knownSourceRefsForGraph(contextGraphPath);
255
+ return collectMarkdownFiles(contextGraphPath, "knowledge")
256
+ .map((path) => noteResource(contextGraphPath, path, undefined, knownSourceRefs));
257
+ }
258
+ function listEntrypointResources(contextGraphPath) {
259
+ const knownSourceRefs = knownSourceRefsForGraph(contextGraphPath);
260
+ const home = existsSync(join(contextGraphPath, "home.md")) ? ["home.md"] : [];
261
+ return [...home, ...collectMarkdownFiles(contextGraphPath, "artifacts")]
262
+ .map((path) => noteResource(contextGraphPath, path, undefined, knownSourceRefs));
263
+ }
264
+ function sourceFileResource(file) {
265
+ return ResourceRefSchema.parse({
266
+ id: resourceId("source", file.id),
267
+ role: "source",
268
+ kind: "source.file",
269
+ label: file.path,
270
+ locator: file.locator,
271
+ source_refs: [file.path],
272
+ required: true,
273
+ metadata: {
274
+ source_file_id: file.id,
275
+ path: file.path,
276
+ kind: file.kind,
277
+ ...(typeof file.page_count === "number" ? { page_count: file.page_count } : {}),
278
+ },
279
+ });
280
+ }
281
+ function sourceUnitResources(manifest) {
282
+ const resources = [];
283
+ for (const file of manifest.files) {
284
+ const units = file.inspectable_units.length > 0
285
+ ? file.inspectable_units
286
+ : [{ kind: "file", index: 1, label: "whole file" }];
287
+ for (const [index, unit] of units.entries()) {
288
+ const label = unit.label ?? `${unit.kind} ${unit.index ?? index + 1}`;
289
+ resources.push(ResourceRefSchema.parse({
290
+ id: resourceId("source-unit", `${file.id}-${unit.kind}-${unit.index ?? index + 1}`),
291
+ role: "source",
292
+ kind: `source.${unit.kind}`,
293
+ label: `${file.path} · ${label}`,
294
+ locator: file.locator,
295
+ source_refs: [file.path],
296
+ required: true,
297
+ metadata: {
298
+ source_file_id: file.id,
299
+ source_path: file.path,
300
+ expected_summary_folder: `summaries/${file.path}`,
301
+ expected_summary_path: `summaries/${file.path}/summary.md`,
302
+ unit_kind: unit.kind,
303
+ unit_index: unit.index ?? index + 1,
304
+ },
305
+ }));
306
+ }
307
+ }
308
+ return resources;
309
+ }
310
+ function loadExpectedInputs(contextGraphPath, stageId) {
311
+ const path = contextGraphRuntimeStageExpectedInputsPath(contextGraphPath, stageId);
312
+ const file = existsSync(path)
313
+ ? readJsonFileWithSchema(path, "stage expected inputs", ExpectedInputsFileSchema)
314
+ : null;
315
+ return file?.expected ?? [];
316
+ }
317
+ function loadReviewedInputs(contextGraphPath, stageId) {
318
+ const path = contextGraphRuntimeStageReviewedInputsPath(contextGraphPath, stageId);
319
+ if (!existsSync(path))
320
+ return null;
321
+ const file = readJsonFileWithSchema(path, "stage reviewed inputs", ReviewedInputsFileSchema);
322
+ return file?.reviewed ?? null;
323
+ }
324
+ export function persistStageReviewedInputsFromShell(options) {
325
+ const shellPath = join(options.shellRoot, "runtime", "reviewed-inputs.json");
326
+ if (!existsSync(shellPath)) {
327
+ return {
328
+ ok: false,
329
+ summary: `${options.stageId} did not write runtime/reviewed-inputs.json.`,
330
+ };
331
+ }
332
+ let file;
333
+ try {
334
+ file = readJsonFileWithSchema(shellPath, `${options.stageId} reviewed inputs`, ReviewedInputsFileSchema);
335
+ }
336
+ catch (error) {
337
+ return {
338
+ ok: false,
339
+ summary: `Invalid runtime/reviewed-inputs.json for ${options.stageId}: ${error instanceof Error ? error.message : String(error)}`,
340
+ };
341
+ }
342
+ if (!file || file.stage_id !== options.stageId) {
343
+ return {
344
+ ok: false,
345
+ summary: `runtime/reviewed-inputs.json stage_id must be ${options.stageId}.`,
346
+ };
347
+ }
348
+ mkdirSync(contextGraphRuntimeStageRoot(options.contextGraphPath, options.stageId), { recursive: true });
349
+ writeJsonAtomic(contextGraphRuntimeStageReviewedInputsPath(options.contextGraphPath, options.stageId), file);
350
+ return {
351
+ ok: true,
352
+ summary: `${options.stageId} reviewed ${file.reviewed.length} expected input${file.reviewed.length === 1 ? "" : "s"}.`,
353
+ };
354
+ }
355
+ function expectedInputsForStage(options) {
356
+ if (options.stage.id === "source") {
357
+ return options.manifest.files.map(sourceFileResource);
358
+ }
359
+ // Roles are derived from the artifact write/read *paths*, not from artifact
360
+ // ids or stage ids, so a custom plan cannot dodge layer policy by renaming.
361
+ const writeRoles = stageArtifactRoles(options.contextGraphPath, options.stage.writes);
362
+ const readRoles = stageArtifactRoles(options.contextGraphPath, options.stage.reads);
363
+ if (writeRoles.has("summary") || options.stage.contractType === "build-file-evidence") {
364
+ return sourceUnitResources(options.manifest);
365
+ }
366
+ if (writeRoles.has("entrypoint")) {
367
+ const knowledge = listKnowledgeResources(options.contextGraphPath);
368
+ return (knowledge.length > 0 ? knowledge : listSummaryResources(options.contextGraphPath))
369
+ .map((resource) => ({ ...resource, required: true }));
370
+ }
371
+ if (writeRoles.has("knowledge")) {
372
+ return listSummaryResources(options.contextGraphPath).map((resource) => ({
373
+ ...resource,
374
+ required: true,
375
+ }));
376
+ }
377
+ const expected = [];
378
+ if (options.stage.reads.includes("source"))
379
+ expected.push(...options.manifest.files.map(sourceFileResource));
380
+ if (readRoles.has("summary"))
381
+ expected.push(...listSummaryResources(options.contextGraphPath));
382
+ if (readRoles.has("knowledge"))
383
+ expected.push(...listKnowledgeResources(options.contextGraphPath));
384
+ return expected.map((resource) => ({ ...resource, required: true }));
385
+ }
386
+ export function writeStageExpectedInputs(options) {
387
+ const manifest = loadContextGraphSourceManifest(options.contextGraphPath);
388
+ if (!manifest)
389
+ return null;
390
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
391
+ const expected = expectedInputsForStage({
392
+ contextGraphPath: options.contextGraphPath,
393
+ manifest,
394
+ stage: options.stage,
395
+ });
396
+ const file = ExpectedInputsFileSchema.parse({
397
+ kind: "interf-stage-expected-inputs",
398
+ version: 1,
399
+ generated_at: generatedAt,
400
+ stage_id: options.stage.id,
401
+ expected,
402
+ });
403
+ mkdirSync(contextGraphRuntimeStageRoot(options.contextGraphPath, options.stage.id), { recursive: true });
404
+ writeJsonAtomic(contextGraphRuntimeStageExpectedInputsPath(options.contextGraphPath, options.stage.id), file);
405
+ return file;
406
+ }
407
+ function metric(key, label, value, total, unit, detail,
408
+ // Optional honest-guarantee tag rendered into MetricCount.metadata.guarantee.
409
+ // "coverage" = every Source file read/summarized (file-level, provable).
410
+ // "traceability" = every claim/summary/knowledge node linked (provable). Plain
411
+ // counts pass no guarantee. Verification (qa_match) is never a metric guarantee.
412
+ guarantee) {
413
+ return MetricCountSchema.parse({
414
+ key,
415
+ label,
416
+ value,
417
+ ...(typeof total === "number" ? { total } : {}),
418
+ ...(unit ? { unit } : {}),
419
+ primary: true,
420
+ ...(typeof total === "number" ? { issue_state: value >= total ? "pass" : "missing" } : {}),
421
+ ...(detail ? { detail } : {}),
422
+ ...(guarantee ? { metadata: { guarantee } } : {}),
423
+ });
424
+ }
425
+ function producedResourcesForStage(options) {
426
+ const schema = readBuildPlanContextFile(buildPlanPackagePathForContextGraph(options.contextGraphPath));
427
+ const resources = [];
428
+ for (const artifactId of options.stage.writes) {
429
+ const artifact = schema ? findBuildPlanContextArtifact(schema, artifactId) : null;
430
+ if (!artifact)
431
+ continue;
432
+ if (artifact.kind === "file") {
433
+ if (existsSync(join(options.contextGraphPath, artifact.path))) {
434
+ resources.push(noteResource(options.contextGraphPath, artifact.path, options.stage.id, knownSourceRefsForGraph(options.contextGraphPath)));
435
+ }
436
+ continue;
437
+ }
438
+ const root = join(options.contextGraphPath, artifact.path);
439
+ if (!existsSync(root))
440
+ continue;
441
+ for (const filePath of listFilesRecursive(root)) {
442
+ if (!filePath.toLowerCase().endsWith(".md"))
443
+ continue;
444
+ resources.push(noteResource(options.contextGraphPath, relativeToGraph(options.contextGraphPath, filePath), options.stage.id, knownSourceRefsForGraph(options.contextGraphPath)));
445
+ }
446
+ }
447
+ return Array.from(new Map(resources.map((resource) => [resource.id, resource])).values());
448
+ }
449
+ function readLatestStageEvidence(contextGraphPath, stageId) {
450
+ const history = readExecutionStageLedgerHistory(contextGraphPath)
451
+ .filter((entry) => entry.stage === stageId && entry.evidence)
452
+ .sort((left, right) => right.updated_at.localeCompare(left.updated_at));
453
+ return history[0]?.evidence ?? null;
454
+ }
455
+ function synthesizeReviewedInputs(options) {
456
+ const evidence = readLatestStageEvidence(options.contextGraphPath, options.stageId);
457
+ const producedBySourceRef = new Map();
458
+ for (const item of evidence?.accepted.items ?? []) {
459
+ for (const sourceRef of item.source_refs) {
460
+ const paths = item.output_refs.map((ref) => ref.path);
461
+ producedBySourceRef.set(sourceRef.path, [...(producedBySourceRef.get(sourceRef.path) ?? []), ...paths]);
462
+ }
463
+ }
464
+ const producedIdsByPath = new Map(options.produced.map((resource) => [resource.path ?? resource.id, resource.id]));
465
+ return options.expected.map((input) => {
466
+ const sourcePath = stringField(input.metadata?.source_path) ?? input.source_refs[0] ?? "";
467
+ const outputPaths = sourcePath ? producedBySourceRef.get(sourcePath) ?? [] : [];
468
+ const outputResourceIds = outputPaths
469
+ .map((path) => producedIdsByPath.get(path))
470
+ .filter((id) => Boolean(id));
471
+ if (outputResourceIds.length > 0) {
472
+ return ReviewedInputSchema.parse({
473
+ resource_id: input.id,
474
+ decision: "used",
475
+ output_resource_ids: outputResourceIds,
476
+ source_refs: input.source_refs,
477
+ });
478
+ }
479
+ return ReviewedInputSchema.parse({
480
+ resource_id: input.id,
481
+ decision: "missing",
482
+ reason: "Stage did not write reviewed-inputs.json for this expected input.",
483
+ source_refs: input.source_refs,
484
+ });
485
+ });
486
+ }
487
+ function missingInputs(reviewed) {
488
+ return reviewed
489
+ .filter((entry) => entry.decision === "missing" || entry.decision === "blocked" || entry.decision === "not-relevant")
490
+ .map((entry) => ({
491
+ resource_id: entry.resource_id,
492
+ status: entry.decision === "blocked" ? "blocked" : entry.decision === "not-relevant" ? "not-relevant" : "missing",
493
+ reason: entry.reason ?? `${entry.resource_id} was not used by this stage.`,
494
+ }));
495
+ }
496
+ /**
497
+ * The Source path an expected ResourceRef points at, if any. Source-file and
498
+ * source-unit resources carry it on `metadata.path` / `metadata.source_path`,
499
+ * and always on `source_refs[0]` / `label`.
500
+ */
501
+ function expectedResourcePath(resource) {
502
+ return stringField(resource.metadata?.path)
503
+ ?? stringField(resource.metadata?.source_path)
504
+ ?? resource.source_refs[0]
505
+ ?? resource.path
506
+ ?? resource.label;
507
+ }
508
+ /**
509
+ * Backstop: drop `missing` entries that point at OS-generated filesystem junk
510
+ * (`.DS_Store`, AppleDouble forks, `Thumbs.db`, …). Inventory already filters
511
+ * these out before they become expected inputs, but a pre-existing on-disk
512
+ * manifest (or any future inventory path) could still surface one. Matched by
513
+ * the expected resource's filename only — provable, never by an agent's
514
+ * `not-relevant` label — so a real skipped content file still counts as missing.
515
+ */
516
+ function dropFilesystemArtifactMissing(missing, expected) {
517
+ if (missing.length === 0)
518
+ return missing;
519
+ const artifactIds = new Set(expected
520
+ .filter((resource) => {
521
+ const path = expectedResourcePath(resource);
522
+ return Boolean(path) && isFilesystemArtifact(path);
523
+ })
524
+ .map((resource) => resource.id));
525
+ if (artifactIds.size === 0)
526
+ return missing;
527
+ return missing.filter((entry) => !artifactIds.has(entry.resource_id));
528
+ }
529
+ function dedupeMissing(missing) {
530
+ const byId = new Map();
531
+ for (const entry of missing) {
532
+ if (!byId.has(entry.resource_id))
533
+ byId.set(entry.resource_id, entry);
534
+ }
535
+ return [...byId.values()];
536
+ }
537
+ function resourceMatchesLink(resource, link) {
538
+ const cleanLink = normalizeRelativePath(link).replace(/\.md$/i, "");
539
+ const path = resource.path ? normalizeRelativePath(resource.path).replace(/\.md$/i, "") : "";
540
+ if (path && (cleanLink === path || cleanLink.endsWith(`/${basename(path)}`)))
541
+ return true;
542
+ return slug(resource.label) === slug(link) || (path ? slug(basename(path)) === slug(link) : false);
543
+ }
544
+ function referencedInputs(expected, produced) {
545
+ const refs = new Map();
546
+ for (const output of produced) {
547
+ for (const link of output.links) {
548
+ const match = expected.find((resource) => resourceMatchesLink(resource, link));
549
+ if (match)
550
+ refs.set(match.id, match);
551
+ }
552
+ for (const sourceRef of output.source_refs) {
553
+ const match = expected.find((resource) => resource.source_refs.some((ref) => sourceRef.includes(ref) || ref.includes(sourceRef)));
554
+ if (match)
555
+ refs.set(match.id, match);
556
+ }
557
+ }
558
+ return [...refs.values()];
559
+ }
560
+ /**
561
+ * Summary-backlink coverage for the knowledge layer, computed in memory from the
562
+ * resources the manifest already holds. A summary is "cited" when a produced note
563
+ * shares a source ref with it; "linked" when a produced note wikilinks the summary
564
+ * note. Orphaned = cited but not linked. Source-agnostic: no task taxonomy, no
565
+ * project-specific input — it reuses the same link/source-ref matching the
566
+ * referenced-inputs rollup uses.
567
+ */
568
+ function summaryBacklinkCoverage(expectedSummaries, producedNotes) {
569
+ const cited = [];
570
+ const linkedCited = [];
571
+ const orphaned = [];
572
+ for (const summary of expectedSummaries) {
573
+ const isCited = producedNotes.some((note) => note.source_refs.some((noteRef) => summary.source_refs.some((summaryRef) => noteRef.includes(summaryRef) || summaryRef.includes(noteRef))));
574
+ if (!isCited)
575
+ continue;
576
+ cited.push(summary);
577
+ const isLinked = producedNotes.some((note) => note.links.some((link) => resourceMatchesLink(summary, link)));
578
+ if (isLinked)
579
+ linkedCited.push(summary);
580
+ else
581
+ orphaned.push(summary);
582
+ }
583
+ return { cited, linkedCited, orphaned };
584
+ }
585
+ /**
586
+ * Does a produced note connect to the summary layer? It connects when it
587
+ * wikilinks at least one of the expected summary notes. This is the note-side
588
+ * complement to the orphaned-summary check: it closes the bypass where a
589
+ * knowledge note cites nothing (no source_refs) — so no summary is "cited" and
590
+ * the orphan rollup is vacuously empty — or uses a single global link that
591
+ * doesn't resolve to any summary. Source-agnostic: it reuses the same
592
+ * link-matching the referenced-inputs rollup uses, with no task taxonomy.
593
+ */
594
+ function noteConnectsToSummaryLayer(note, expectedSummaries) {
595
+ return note.links.some((link) => expectedSummaries.some((summary) => resourceMatchesLink(summary, link)));
596
+ }
597
+ /**
598
+ * Knowledge-layer notes that link no summary at all. A knowledge note's purpose
599
+ * is to connect prepared knowledge back to the source-backed summary layer; a
600
+ * note that links nothing in `summaries/` is a disconnected island that must not
601
+ * pass coverage silently. Scoped to notes that actually live in the knowledge
602
+ * layer (by write path, via `roleForPath`), so home/artifacts entrypoint notes
603
+ * produced by the same stage are not falsely flagged.
604
+ */
605
+ /**
606
+ * Does a produced note land in the knowledge layer? A note is knowledge-layer
607
+ * when it carries the `knowledge` role OR its on-disk write path normalizes into
608
+ * the knowledge layer (via the central `roleForPath` classifier). Single
609
+ * definition of the predicate every knowledge-layer rollup routes through, so the
610
+ * note→layer test can never drift between the disconnected-notes, web, and
611
+ * knowledge-notes paths.
612
+ */
613
+ function isKnowledgeNote(note) {
614
+ return note.role === "knowledge"
615
+ || (note.path ? roleForPath(normalizeRelativePath(note.path)) === "knowledge" : false);
616
+ }
617
+ function disconnectedKnowledgeNotes(expectedSummaries, producedNotes) {
618
+ if (expectedSummaries.length === 0)
619
+ return [];
620
+ return producedNotes.filter((note) => {
621
+ if (!isKnowledgeNote(note))
622
+ return false;
623
+ return !noteConnectsToSummaryLayer(note, expectedSummaries);
624
+ });
625
+ }
626
+ /**
627
+ * Knowledge-web connectivity for the produced notes — the manifest-side mirror of
628
+ * the `knowledge_web_connectivity` Check. Distinct from `disconnectedKnowledgeNotes`
629
+ * (notes → summaries, the DOWN-link): this is notes ↔ notes (the WEB). A knowledge
630
+ * note is web-connected when it links another knowledge note OR another knowledge
631
+ * note links it (UNDIRECTED), reusing the in-memory `ResourceRef.links` graph and
632
+ * `resourceMatchesLink` the referenced-inputs rollup already uses. Connectedness,
633
+ * not a count: degree ≥ 1 passes, degree 0 is a disconnected island. Vacuous pass:
634
+ * ≤1 knowledge note cannot form a web (connected = all, no islands). Both this and
635
+ * the Check derive connectedness from the same link graph, so they agree.
636
+ */
637
+ function knowledgeWebConnectivity(producedNotes) {
638
+ const knowledgeNotes = producedNotes.filter(isKnowledgeNote);
639
+ if (knowledgeNotes.length <= 1) {
640
+ return { knowledgeNotes, connected: knowledgeNotes.length, islands: [] };
641
+ }
642
+ // A directed edge A→B exists when A links a DIFFERENT knowledge note B. Undirected
643
+ // connectivity: a node has degree ≥ 1 when it is the source of any edge (outbound)
644
+ // or the target of any edge from another knowledge note (inbound).
645
+ const hasOutbound = new Set();
646
+ const hasInbound = new Set();
647
+ for (const from of knowledgeNotes) {
648
+ for (const link of from.links) {
649
+ for (const to of knowledgeNotes) {
650
+ if (to.id === from.id)
651
+ continue;
652
+ if (resourceMatchesLink(to, link)) {
653
+ hasOutbound.add(from.id);
654
+ hasInbound.add(to.id);
655
+ }
656
+ }
657
+ }
658
+ }
659
+ const islands = knowledgeNotes.filter((note) => !hasOutbound.has(note.id) && !hasInbound.has(note.id));
660
+ return { knowledgeNotes, connected: knowledgeNotes.length - islands.length, islands };
661
+ }
662
+ function stageRole(contextGraphPath, stage) {
663
+ if (stage.id === "source")
664
+ return "source";
665
+ // Role is decided by where the stage's artifacts are written on disk, resolved
666
+ // through the Build Plan schema — never by a stringly stage id or artifact id.
667
+ // A custom plan that writes artifact id "claims" into path `knowledge/` via
668
+ // stage "extract-claims" is still the knowledge layer and is still policed.
669
+ const writeRoles = stageArtifactRoles(contextGraphPath, stage.writes);
670
+ if (writeRoles.has("summary") || stage.contractType === "build-file-evidence")
671
+ return "summary";
672
+ if (writeRoles.has("entrypoint"))
673
+ return "entrypoint";
674
+ if (writeRoles.has("knowledge"))
675
+ return "knowledge";
676
+ return "other";
677
+ }
678
+ /** Produced notes that land in the knowledge layer, by their on-disk path. */
679
+ function knowledgeNotesIn(produced) {
680
+ return produced.filter(isKnowledgeNote);
681
+ }
682
+ /**
683
+ * Knowledge-layer metrics for the notes a stage produced into `knowledge/`. Emitted
684
+ * whenever a stage produces ANY knowledge note — not gated on the stage's single
685
+ * primary role — so a multi-layer stage (e.g. an entrypoint stage that ALSO writes
686
+ * route/index notes into `knowledge/`) still reports knowledge coverage,
687
+ * backlinks, summary-connectivity, and web connectivity for the notes it added.
688
+ * This is what lets the GraphManifest roll up FINAL knowledge state from whichever
689
+ * stage last mutated the layer.
690
+ *
691
+ * The backlink / summary-connectivity analyses compare produced knowledge notes
692
+ * against the ACTUAL summary layer on disk (`summaries`), NOT against the stage's
693
+ * role-specific `expected` inputs — the entrypoint stage's expected inputs are
694
+ * knowledge notes, not summaries, so reusing `expected` here would compare the
695
+ * wrong layer.
696
+ *
697
+ * `includeCoverageMetric` adds the `knowledge_coverage` traceability count
698
+ * (summaries this stage reviewed/used). It is emitted ONLY for a knowledge-role
699
+ * stage, because only that stage's expected inputs ARE the summaries — emitting
700
+ * it from a multi-layer stage (e.g. the entrypoint stage) would put a confusing
701
+ * "knowledge notes reviewed / summary total" denominator mismatch on the primary
702
+ * metric. The LINK-QUALITY metrics below are emitted for ANY stage that produced
703
+ * knowledge notes, so the GraphManifest rolls up FINAL link state from whichever
704
+ * stage last mutated the layer.
705
+ */
706
+ function knowledgeLayerMetrics(options) {
707
+ const reviewedCount = options.reviewed.filter((entry) => entry.decision === "used" || entry.decision === "reviewed").length;
708
+ const usedCount = options.reviewed.filter((entry) => entry.decision === "used").length || options.referenced.length;
709
+ const backlinks = summaryBacklinkCoverage(options.expectedSummaries, options.produced);
710
+ const knowledgeNotes = knowledgeNotesIn(options.produced);
711
+ const disconnected = disconnectedKnowledgeNotes(options.expectedSummaries, options.produced);
712
+ const connectedNotes = knowledgeNotes.length - disconnected.length;
713
+ const web = knowledgeWebConnectivity(options.produced);
714
+ return [
715
+ // knowledge_coverage counts summaries the knowledge stage reviewed/used — a
716
+ // TRACEABILITY signal (knowledge linked back to the coverage layer), NOT the
717
+ // provable file-level "every Source file was read" COVERAGE guarantee. Tagged
718
+ // traceability so nothing derived/judgment is mislabeled coverage.
719
+ ...(options.includeCoverageMetric
720
+ ? [metric("knowledge_coverage", "Knowledge coverage", reviewedCount, options.reviewedTotal, "summaries", `${reviewedCount} / ${options.reviewedTotal} reviewed, ${usedCount} used`, "traceability")]
721
+ : []),
722
+ metric("knowledge_notes", "Knowledge notes", knowledgeNotes.length, undefined, "notes"),
723
+ metric("knowledge_summary_backlinks", "Summaries wikilinked", backlinks.linkedCited.length, backlinks.cited.length, "summaries", backlinks.orphaned.length === 0
724
+ ? `${backlinks.linkedCited.length} / ${backlinks.cited.length} cited summaries wikilinked`
725
+ : `${backlinks.orphaned.length} cited summar${backlinks.orphaned.length === 1 ? "y" : "ies"} orphaned (cited but never wikilinked)`, "traceability"),
726
+ metric("knowledge_summary_connectivity", "Knowledge notes linked to summaries", connectedNotes, knowledgeNotes.length, "notes", disconnected.length === 0
727
+ ? `${connectedNotes} / ${knowledgeNotes.length} knowledge notes wikilink a summary`
728
+ : formatDisconnectedNotesMessage(disconnected.length, false), "traceability"),
729
+ // notes ↔ notes (the WEB), distinct from knowledge_summary_connectivity
730
+ // (notes → summaries, the DOWN-link). A star (every note links only summaries,
731
+ // none link each other) reads as islands here; a web reads as connected.
732
+ metric("knowledge_web_connectivity", "Knowledge notes linked into the web", web.connected, web.knowledgeNotes.length, "notes", web.islands.length === 0
733
+ ? `${web.connected} / ${web.knowledgeNotes.length} knowledge notes link another knowledge note`
734
+ : `${web.islands.length} knowledge note${web.islands.length === 1 ? "" : "s"} link no other knowledge note (disconnected island${web.islands.length === 1 ? "" : "s"})`, "traceability"),
735
+ ];
736
+ }
737
+ /** Entrypoint-layer metric for the home spine + entrypoint notes a stage produced. */
738
+ function entrypointLayerMetrics(produced) {
739
+ const homeReady = produced.some((resource) => resource.path === "home.md") ? 1 : 0;
740
+ const entrypointCount = produced.filter((resource) => resource.role === "entrypoint").length;
741
+ return [
742
+ metric("entrypoints", "Entrypoints", homeReady, 1, "entrypoints", `${entrypointCount} entrypoint file${entrypointCount === 1 ? "" : "s"} produced`),
743
+ ];
744
+ }
745
+ function stageMetrics(options) {
746
+ if (options.role === "source") {
747
+ return [
748
+ metric("files_processed", "Files processed", options.produced.length, options.manifest.source_total, "files", undefined, "coverage"),
749
+ ];
750
+ }
751
+ if (options.role === "summary") {
752
+ const summarizedCount = options.reviewed.filter((entry) => entry.decision === "used" || entry.decision === "reviewed").length;
753
+ return [
754
+ metric("source_units_summarized", "Source units summarized", summarizedCount, options.expected.length, "source units", undefined, "coverage"),
755
+ ];
756
+ }
757
+ // Knowledge + entrypoint metrics are LAYER-keyed, not role-keyed: a stage emits
758
+ // the metrics for every canonical layer its produced resources actually touch.
759
+ // The default entrypoint stage writes `home`, `artifacts`, AND `knowledge` (route
760
+ // notes), so it emits BOTH entrypoint and knowledge metrics — the knowledge
761
+ // branch no longer hides behind an "entrypoint vs knowledge" role precedence.
762
+ // The GraphManifest then rolls up the FINAL per-layer metric across stages.
763
+ const metrics = [];
764
+ const producesKnowledge = knowledgeNotesIn(options.produced).length > 0;
765
+ const producesEntrypoint = options.role === "entrypoint"
766
+ || options.produced.some((resource) => resource.role === "entrypoint");
767
+ if (producesKnowledge) {
768
+ // Compare produced knowledge notes against the ACTUAL summary layer on disk,
769
+ // not this stage's role-specific expected inputs (which, for an entrypoint
770
+ // stage, are knowledge notes — the wrong layer). For a knowledge-role stage
771
+ // these are the same set. Only the knowledge-role stage emits the
772
+ // knowledge_coverage traceability count (summaries reviewed), with its
773
+ // original expected-input denominator.
774
+ const isKnowledgeRole = options.role === "knowledge";
775
+ const expectedSummaries = isKnowledgeRole
776
+ ? options.expected
777
+ : listSummaryResources(options.contextGraphPath);
778
+ metrics.push(...knowledgeLayerMetrics({
779
+ expectedSummaries,
780
+ reviewedTotal: options.expected.length,
781
+ produced: options.produced,
782
+ referenced: options.referenced,
783
+ reviewed: options.reviewed,
784
+ includeCoverageMetric: isKnowledgeRole,
785
+ }));
786
+ }
787
+ if (producesEntrypoint) {
788
+ metrics.push(...entrypointLayerMetrics(options.produced));
789
+ }
790
+ if (metrics.length > 0)
791
+ return metrics;
792
+ return [
793
+ metric("outputs_produced", "Outputs produced", options.produced.length, undefined, "outputs"),
794
+ ];
795
+ }
796
+ export function writeSourceStageManifest(options) {
797
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
798
+ const expected = options.sourceManifest.files.map(sourceFileResource);
799
+ const reviewed = expected.map((resource) => ReviewedInputSchema.parse({
800
+ resource_id: resource.id,
801
+ decision: "used",
802
+ source_refs: resource.source_refs,
803
+ output_resource_ids: [resource.id],
804
+ }));
805
+ const manifest = StageManifestSchema.parse({
806
+ kind: "interf-stage-manifest",
807
+ version: 1,
808
+ generated_at: generatedAt,
809
+ project: options.sourceManifest.project,
810
+ run_id: options.runId ?? options.sourceManifest.run_id,
811
+ build_plan: options.buildPlanId,
812
+ stage_id: "source",
813
+ stage_label: "Prepare Source",
814
+ role: "source",
815
+ summary: `Source Manifest captured ${options.sourceManifest.source_total} file(s).`,
816
+ input_manifests: [],
817
+ expected,
818
+ reviewed,
819
+ referenced: expected,
820
+ missing: [],
821
+ produced: expected,
822
+ metrics: [
823
+ metric("files_processed", "Files processed", expected.length, options.sourceManifest.source_total, "files", undefined, "coverage"),
824
+ ],
825
+ });
826
+ mkdirSync(contextGraphRuntimeStageRoot(options.contextGraphPath, "source"), { recursive: true });
827
+ writeJsonAtomic(contextGraphRuntimeStageManifestPath(options.contextGraphPath, "source"), manifest);
828
+ return manifest;
829
+ }
830
+ export function loadStageManifest(contextGraphPath, stageId) {
831
+ const path = contextGraphRuntimeStageManifestPath(contextGraphPath, stageId);
832
+ if (!existsSync(path))
833
+ return null;
834
+ return readJsonFileWithSchema(path, `stage manifest ${stageId}`, StageManifestSchema);
835
+ }
836
+ export function listStageManifests(contextGraphPath) {
837
+ const stagesRoot = join(contextGraphPath, ".interf", "runtime", "stages");
838
+ if (!existsSync(stagesRoot))
839
+ return [];
840
+ return readdirSync(stagesRoot, { withFileTypes: true })
841
+ .filter((entry) => entry.isDirectory())
842
+ .map((entry) => loadStageManifest(contextGraphPath, entry.name))
843
+ .filter((entry) => entry !== null)
844
+ .sort((left, right) => {
845
+ const order = ["source", "summarize", "knowledge", "entrypoint"];
846
+ const leftOrder = order.indexOf(left.stage_id);
847
+ const rightOrder = order.indexOf(right.stage_id);
848
+ return (leftOrder === -1 ? 99 : leftOrder) - (rightOrder === -1 ? 99 : rightOrder);
849
+ });
850
+ }
851
+ export function writeStageManifestFromOutputs(options) {
852
+ const sourceManifest = loadContextGraphSourceManifest(options.contextGraphPath);
853
+ if (!sourceManifest)
854
+ return null;
855
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
856
+ const expected = loadExpectedInputs(options.contextGraphPath, options.stage.id);
857
+ const produced = producedResourcesForStage({
858
+ contextGraphPath: options.contextGraphPath,
859
+ stage: options.stage,
860
+ });
861
+ const explicitReviewed = loadReviewedInputs(options.contextGraphPath, options.stage.id);
862
+ const reviewed = explicitReviewed ?? synthesizeReviewedInputs({
863
+ contextGraphPath: options.contextGraphPath,
864
+ expected,
865
+ produced,
866
+ stageId: options.stage.id,
867
+ });
868
+ const referenced = referencedInputs(expected, produced);
869
+ const role = stageRole(options.contextGraphPath, options.stage);
870
+ // Orphaned summaries (cited via a note's source_refs but never wikilinked) are
871
+ // missing coverage for the knowledge layer, even when otherwise reviewed. On a
872
+ // knowledge-role stage the summaries ARE this stage's expected inputs, so the
873
+ // orphan is an expected-input gap and belongs in `missing` (the StageManifest
874
+ // schema requires every `missing.resource_id` to be an expected input, so this
875
+ // is scoped to that case). When a DIFFERENT stage (e.g. the entrypoint stage)
876
+ // also mutates `knowledge/`, its summaries are not its expected inputs, so the
877
+ // orphan cannot live in that stage's `missing`; it instead surfaces as the
878
+ // failing `knowledge_summary_backlinks` metric — which `writeGraphManifest`
879
+ // rolls into graph readiness from FINAL per-layer state, alongside the
880
+ // summary-connectivity and web gaps. So no orphan escapes readiness regardless
881
+ // of which stage last touched the knowledge layer.
882
+ const orphanMissing = role === "knowledge"
883
+ ? summaryBacklinkCoverage(expected, produced).orphaned.map((summary) => ({
884
+ resource_id: summary.id,
885
+ status: "missing",
886
+ reason: `${summary.path ?? summary.label} ${ORPHANED_SUMMARY_REASON}`,
887
+ }))
888
+ : [];
889
+ const missing = dropFilesystemArtifactMissing(dedupeMissing([...missingInputs(reviewed), ...orphanMissing]), expected);
890
+ const manifest = StageManifestSchema.parse({
891
+ kind: "interf-stage-manifest",
892
+ version: 1,
893
+ generated_at: generatedAt,
894
+ project: options.projectId,
895
+ run_id: options.runId ?? null,
896
+ build_plan: options.buildPlanId,
897
+ stage_id: options.stage.id,
898
+ stage_label: options.stage.label,
899
+ role,
900
+ summary: `${options.stage.label} produced ${produced.length} resource${produced.length === 1 ? "" : "s"}.`,
901
+ input_manifests: [
902
+ ResourceRefSchema.parse({
903
+ id: `manifest:${sourceManifest.manifest_id}`,
904
+ role: "diagnostic",
905
+ kind: "source.manifest",
906
+ label: "Source Manifest",
907
+ path: ".interf/runtime/source-manifest.json",
908
+ }),
909
+ ],
910
+ expected,
911
+ reviewed,
912
+ referenced,
913
+ missing,
914
+ produced,
915
+ metrics: stageMetrics({
916
+ contextGraphPath: options.contextGraphPath,
917
+ expected,
918
+ manifest: sourceManifest,
919
+ produced,
920
+ referenced,
921
+ reviewed,
922
+ role,
923
+ }),
924
+ });
925
+ mkdirSync(contextGraphRuntimeStageRoot(options.contextGraphPath, options.stage.id), { recursive: true });
926
+ writeJsonAtomic(contextGraphRuntimeStageManifestPath(options.contextGraphPath, options.stage.id), manifest);
927
+ return manifest;
928
+ }
929
+ /**
930
+ * The FINAL value of a metric across the stage manifests, in execution order.
931
+ * `listStageManifests` returns manifests ordered source → summarize → knowledge →
932
+ * entrypoint, so the LAST manifest that emitted a given metric key reflects the
933
+ * final on-disk state of that layer. Taking the last (not the first) match is
934
+ * what makes the GraphManifest final-state-correct: when the entrypoint stage
935
+ * mutates `knowledge/` after the knowledge stage, its (later) knowledge metrics
936
+ * win over the earlier, now-stale knowledge-stage values.
937
+ */
938
+ function metricByKey(manifests, key) {
939
+ let latest = null;
940
+ for (const manifest of manifests) {
941
+ const found = manifest.metrics.find((entry) => entry.key === key);
942
+ if (found)
943
+ latest = found;
944
+ }
945
+ return latest;
946
+ }
947
+ function countByKind(resources, kind) {
948
+ return resources.filter((resource) => resource.kind === kind || resource.kind.startsWith(`${kind}.`)).length;
949
+ }
950
+ function graphResourceCounts(resources, links, sourceRefs) {
951
+ return {
952
+ nodes: resources.length,
953
+ edges: links + sourceRefs,
954
+ notes: resources.filter((resource) => resource.path?.endsWith(".md")).length,
955
+ links,
956
+ source_refs: sourceRefs,
957
+ claims: countByKind(resources, "knowledge.claim"),
958
+ entities: countByKind(resources, "knowledge.entity"),
959
+ };
960
+ }
961
+ export function loadGraphManifest(contextGraphPath) {
962
+ const path = contextGraphRuntimeGraphManifestPath(contextGraphPath);
963
+ if (!existsSync(path))
964
+ return null;
965
+ return readJsonFileWithSchema(path, "graph manifest", GraphManifestSchema);
966
+ }
967
+ export function writeGraphManifest(options) {
968
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
969
+ const manifests = listStageManifests(options.contextGraphPath);
970
+ // FINAL on-disk state per resource: last-write-wins by id, in execution order.
971
+ // A note in `knowledge/` that the knowledge stage produced and the entrypoint
972
+ // stage then re-read (now carrying the route links the entrypoint stage added)
973
+ // must roll up in its FINAL form, not the earlier knowledge-stage snapshot. The
974
+ // previous first-by-id kept the stale copy and could mask links/refs added by a
975
+ // later stage. `set` already overwrites, so iterating in order yields the latest.
976
+ const resourcesById = new Map();
977
+ for (const manifest of manifests) {
978
+ for (const resource of manifest.produced) {
979
+ resourcesById.set(resource.id, resource);
980
+ }
981
+ }
982
+ const resources = [...resourcesById.values()];
983
+ const entrypoints = resources.filter((resource) => resource.role === "entrypoint");
984
+ // Readiness-feeding missing inputs. The per-summary orphaned-summary entries a
985
+ // knowledge stage records are collapsed here into the single FINAL-state
986
+ // `knowledge:summary-backlinks` gate (added below from the metric), so one
987
+ // orphan gap counts once in readiness — not once per summary AND again via the
988
+ // metric gate. The full, unfiltered stage `missing` is still preserved on each
989
+ // GraphManifest stage summary (`missing_required_total`).
990
+ const missing = manifests
991
+ .flatMap((manifest) => manifest.missing)
992
+ .filter((entry) => !entry.reason.endsWith(ORPHANED_SUMMARY_REASON));
993
+ const links = resources.reduce((total, resource) => total + resource.links.length, 0);
994
+ const sourceRefs = resources.reduce((total, resource) => total + resource.source_refs.length, 0);
995
+ const sourceMetric = metricByKey(manifests, "files_processed");
996
+ const summaryMetric = metricByKey(manifests, "source_units_summarized");
997
+ const knowledgeMetric = metricByKey(manifests, "knowledge_coverage");
998
+ const entrypointMetric = metricByKey(manifests, "entrypoints");
999
+ const graphOutputs = graphResourceCounts(resources, links, sourceRefs);
1000
+ const graphOutputMetric = MetricCountSchema.parse({
1001
+ key: "graph_outputs",
1002
+ label: "Graph outputs",
1003
+ value: graphOutputs.notes,
1004
+ unit: "notes",
1005
+ primary: true,
1006
+ detail: `${graphOutputs.notes} notes, ${graphOutputs.links} links, ${graphOutputs.source_refs} source refs`,
1007
+ metadata: graphOutputs,
1008
+ });
1009
+ const primaryMetrics = [
1010
+ sourceMetric,
1011
+ summaryMetric,
1012
+ knowledgeMetric,
1013
+ graphOutputMetric,
1014
+ entrypointMetric,
1015
+ ].filter((entry) => entry !== null);
1016
+ // The three knowledge-layer link-quality gates (summary-orphan, summary
1017
+ // down-connectivity, and notes↔notes web) are derived HERE from the FINAL
1018
+ // on-disk resources, not read off any single stage manifest. Stage manifests
1019
+ // are per-stage snapshots; whichever stage ran last is not guaranteed to be the
1020
+ // one that holds the final knowledge layer (the entrypoint stage adds route
1021
+ // notes after the knowledge stage). Computing from `resources` — the
1022
+ // last-write-wins union of every stage's produced notes — makes readiness
1023
+ // final-state-correct by construction, independent of stage ordering or a
1024
+ // stale per-stage metric. These reuse the SAME in-memory analyses the stage
1025
+ // metrics use, so the gate and the displayed metric agree.
1026
+ const finalSummaries = resources.filter((resource) => resource.role === "summary");
1027
+ const finalKnowledgeNotes = knowledgeNotesIn(resources);
1028
+ const finalBacklinks = summaryBacklinkCoverage(finalSummaries, finalKnowledgeNotes);
1029
+ const finalDisconnected = disconnectedKnowledgeNotes(finalSummaries, finalKnowledgeNotes);
1030
+ const finalWeb = knowledgeWebConnectivity(finalKnowledgeNotes);
1031
+ // Orphaned summaries: cited via a note's source_refs but never wikilinked. A
1032
+ // disconnected coverage layer is not ready. (Per-summary detail still lives on
1033
+ // each knowledge-role stage's `missing`; here it is one collapsed gate.)
1034
+ const backlinkMissing = finalBacklinks.orphaned.length > 0
1035
+ ? [{
1036
+ resource_id: "knowledge:summary-backlinks",
1037
+ status: "missing",
1038
+ reason: `${finalBacklinks.orphaned.length} cited summar${finalBacklinks.orphaned.length === 1 ? "y is" : "ies are"} orphaned (cited by a knowledge note but never wikilinked).`,
1039
+ }]
1040
+ : [];
1041
+ // Down-connectivity: knowledge notes that wikilink no summary at all — the
1042
+ // no-cite / single-global-link bypass. notes → summaries.
1043
+ const connectivityMissing = finalDisconnected.length > 0
1044
+ ? [{
1045
+ resource_id: "knowledge:summary-connectivity",
1046
+ status: "missing",
1047
+ reason: formatDisconnectedNotesMessage(finalDisconnected.length, true),
1048
+ }]
1049
+ : [];
1050
+ // No-orphan knowledge WEB: notes ↔ notes. A disconnected island (a knowledge
1051
+ // note that links no OTHER knowledge note) is not ready. Honest sparsity (≤1
1052
+ // note, or a genuinely connected web) yields no islands and no gap.
1053
+ const webMissing = finalWeb.islands.length > 0
1054
+ ? [{
1055
+ resource_id: "knowledge:web-connectivity",
1056
+ status: "missing",
1057
+ reason: `${finalWeb.islands.length} knowledge note${finalWeb.islands.length === 1 ? "" : "s"} link no other knowledge note (disconnected island${finalWeb.islands.length === 1 ? "" : "s"}).`,
1058
+ }]
1059
+ : [];
1060
+ const entrypointMissing = entrypoints.length > 0
1061
+ ? []
1062
+ : [{
1063
+ resource_id: "entrypoint:home",
1064
+ status: "missing",
1065
+ reason: "Context Graph has no generated entrypoint.",
1066
+ }];
1067
+ // not-relevant is *not* a free pass for a required input. Required inputs that
1068
+ // are skipped without a substitute still count toward the missing-required
1069
+ // rollup, otherwise readiness can flip ready by tagging real gaps not-relevant.
1070
+ const readinessMissing = [
1071
+ ...missing,
1072
+ ...backlinkMissing,
1073
+ ...connectivityMissing,
1074
+ ...webMissing,
1075
+ ...entrypointMissing,
1076
+ ];
1077
+ const missingRequiredTotal = readinessMissing.length;
1078
+ const readiness = {
1079
+ status: missingRequiredTotal === 0 ? "ready" : "not-ready",
1080
+ ready: missingRequiredTotal === 0,
1081
+ summary: missingRequiredTotal === 0
1082
+ ? "Context Graph has complete required coverage metrics."
1083
+ : `${missingRequiredTotal} required coverage item${missingRequiredTotal === 1 ? "" : "s"} missing.`,
1084
+ context_graph_path: options.contextGraphPath,
1085
+ missing_required_total: missingRequiredTotal,
1086
+ missing: readinessMissing,
1087
+ };
1088
+ const graphManifest = GraphManifestSchema.parse({
1089
+ kind: "interf-graph-manifest",
1090
+ version: 1,
1091
+ generated_at: generatedAt,
1092
+ project: options.projectId,
1093
+ graph_id: options.graphId ?? options.runId ?? `${options.projectId}-latest`,
1094
+ run_id: options.runId ?? null,
1095
+ build_plan: options.buildPlanId,
1096
+ intent: options.intent ?? null,
1097
+ graph_path: options.contextGraphPath,
1098
+ primary_metrics: primaryMetrics,
1099
+ stages: manifests.map((manifest) => ({
1100
+ stage_id: manifest.stage_id,
1101
+ stage_label: manifest.stage_label,
1102
+ role: manifest.role,
1103
+ manifest_path: relativeToGraph(options.contextGraphPath, contextGraphRuntimeStageManifestPath(options.contextGraphPath, manifest.stage_id)),
1104
+ summary: manifest.summary,
1105
+ produced_total: manifest.produced.length,
1106
+ missing_required_total: manifest.missing.length,
1107
+ metrics: manifest.metrics,
1108
+ })),
1109
+ entrypoints,
1110
+ resources,
1111
+ graph_outputs: graphOutputs,
1112
+ readiness,
1113
+ });
1114
+ writeJsonAtomic(contextGraphRuntimeGraphManifestPath(options.contextGraphPath), graphManifest);
1115
+ return graphManifest;
1116
+ }
1117
+ export function reviewedInputsFilePath(contextGraphPath, stageId) {
1118
+ return contextGraphRuntimeStageReviewedInputsPath(contextGraphPath, stageId);
1119
+ }
1120
+ export function expectedInputsFilePath(contextGraphPath, stageId) {
1121
+ return contextGraphRuntimeStageExpectedInputsPath(contextGraphPath, stageId);
1122
+ }
1123
+ export function loadStageInputsForManifest(contextGraphPath, stageId) {
1124
+ return loadBuildStageInputs(contextGraphPath, stageId);
1125
+ }