@lsctech/polaris 0.1.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 (316) hide show
  1. package/README.md +87 -0
  2. package/dist/__mocks__/tool-server-linear.js +34 -0
  3. package/dist/cli/adopt-approve.js +60 -0
  4. package/dist/cli/adopt-cognition.js +199 -0
  5. package/dist/cli/adopt-instructions.js +143 -0
  6. package/dist/cli/adopt-rules.js +169 -0
  7. package/dist/cli/adopt-scan.js +25 -0
  8. package/dist/cli/adopt-smartdocs.js +192 -0
  9. package/dist/cli/adoption-inventory.js +350 -0
  10. package/dist/cli/adoption-plan.js +222 -0
  11. package/dist/cli/args.js +28 -0
  12. package/dist/cli/args.test.js +30 -0
  13. package/dist/cli/branding.js +41 -0
  14. package/dist/cli/graph.js +358 -0
  15. package/dist/cli/index.js +127 -0
  16. package/dist/cli/index.test.js +162 -0
  17. package/dist/cli/init-detect.js +61 -0
  18. package/dist/cli/init.js +492 -0
  19. package/dist/cli/init.test.js +259 -0
  20. package/dist/cli/librarian.js +32 -0
  21. package/dist/cli/medic.js +96 -0
  22. package/dist/cli/startup.test.js +23 -0
  23. package/dist/cli/tracker.js +130 -0
  24. package/dist/cli/tracker.test.js +126 -0
  25. package/dist/cli/version.js +10 -0
  26. package/dist/cli/version.test.js +11 -0
  27. package/dist/cli/worker.js +287 -0
  28. package/dist/cluster-state/fixtures/test-clusters.json +19 -0
  29. package/dist/cluster-state/store.js +302 -0
  30. package/dist/cluster-state/store.test.js +145 -0
  31. package/dist/cluster-state/types.js +2 -0
  32. package/dist/cognition/archive.js +158 -0
  33. package/dist/cognition/closeout-librarian-types.js +161 -0
  34. package/dist/cognition/cognition.test.js +623 -0
  35. package/dist/cognition/index.js +41 -0
  36. package/dist/cognition/librarian-dispatch.js +391 -0
  37. package/dist/cognition/librarian-dispatch.test.js +247 -0
  38. package/dist/cognition/librarian-packet.js +313 -0
  39. package/dist/cognition/librarian-types.js +57 -0
  40. package/dist/cognition/route-cognition-delta.js +350 -0
  41. package/dist/cognition/summary-delta.js +279 -0
  42. package/dist/cognition/validate.js +162 -0
  43. package/dist/config/defaults.js +98 -0
  44. package/dist/config/doctor.js +273 -0
  45. package/dist/config/index.js +14 -0
  46. package/dist/config/loader.js +58 -0
  47. package/dist/config/provider-detect.js +60 -0
  48. package/dist/config/provider-detect.test.js +119 -0
  49. package/dist/config/schema.js +2 -0
  50. package/dist/config/show.js +44 -0
  51. package/dist/config/show.test.js +42 -0
  52. package/dist/config/validator.js +675 -0
  53. package/dist/config/validator.test.js +77 -0
  54. package/dist/docs/canon-check.js +334 -0
  55. package/dist/docs/canon-check.test.js +207 -0
  56. package/dist/docs/doctrine.js +133 -0
  57. package/dist/docs/doctrine.test.js +139 -0
  58. package/dist/docs/index.js +168 -0
  59. package/dist/docs/index.test.js +83 -0
  60. package/dist/docs/ingest.js +295 -0
  61. package/dist/docs/ingest.test.js +71 -0
  62. package/dist/docs/migrate.js +165 -0
  63. package/dist/docs/migrate.test.js +136 -0
  64. package/dist/docs/seed-instructions.js +147 -0
  65. package/dist/docs/seed-instructions.test.js +137 -0
  66. package/dist/docs/validate-instructions.js +267 -0
  67. package/dist/docs/validate-instructions.test.js +238 -0
  68. package/dist/finalize/artifact-policy.js +214 -0
  69. package/dist/finalize/artifact-policy.test.js +84 -0
  70. package/dist/finalize/delivery-integrity.js +141 -0
  71. package/dist/finalize/finalize.test.js +210 -0
  72. package/dist/finalize/github.js +41 -0
  73. package/dist/finalize/index.js +474 -0
  74. package/dist/finalize/linear.js +213 -0
  75. package/dist/finalize/run-report.js +47 -0
  76. package/dist/finalize/steps/01-map-update.js +11 -0
  77. package/dist/finalize/steps/02-map-validate.js +11 -0
  78. package/dist/finalize/steps/03-schema-validate.js +11 -0
  79. package/dist/finalize/steps/04-run-checks.js +118 -0
  80. package/dist/finalize/steps/05-generate-report.js +14 -0
  81. package/dist/finalize/steps/06-commit.js +39 -0
  82. package/dist/finalize/steps/07-push.js +10 -0
  83. package/dist/finalize/steps/08-create-pr.js +9 -0
  84. package/dist/finalize/steps/09-update-state.js +9 -0
  85. package/dist/finalize/steps/10-append-jsonl.js +23 -0
  86. package/dist/finalize/steps/11-update-linear.js +22 -0
  87. package/dist/finalize/steps/12-archive.js +21 -0
  88. package/dist/graph/adapter/c/adapter.js +45 -0
  89. package/dist/graph/adapter/c/extract.js +135 -0
  90. package/dist/graph/adapter/c/index.js +10 -0
  91. package/dist/graph/adapter/c/runtime.js +67 -0
  92. package/dist/graph/adapter/cpp/adapter.js +45 -0
  93. package/dist/graph/adapter/cpp/extract.js +159 -0
  94. package/dist/graph/adapter/cpp/index.js +10 -0
  95. package/dist/graph/adapter/cpp/runtime.js +61 -0
  96. package/dist/graph/adapter/csharp/adapter.js +45 -0
  97. package/dist/graph/adapter/csharp/extract.js +127 -0
  98. package/dist/graph/adapter/csharp/index.js +10 -0
  99. package/dist/graph/adapter/csharp/runtime.js +61 -0
  100. package/dist/graph/adapter/dart/adapter.js +56 -0
  101. package/dist/graph/adapter/dart/extract.js +130 -0
  102. package/dist/graph/adapter/dart/index.js +10 -0
  103. package/dist/graph/adapter/dart/runtime.js +61 -0
  104. package/dist/graph/adapter/go/adapter.js +45 -0
  105. package/dist/graph/adapter/go/extract.js +112 -0
  106. package/dist/graph/adapter/go/index.js +10 -0
  107. package/dist/graph/adapter/go/runtime.js +58 -0
  108. package/dist/graph/adapter/kotlin-java/extract-common.js +83 -0
  109. package/dist/graph/adapter/kotlin-java/index.js +15 -0
  110. package/dist/graph/adapter/kotlin-java/java-adapter.js +45 -0
  111. package/dist/graph/adapter/kotlin-java/java-extract.js +42 -0
  112. package/dist/graph/adapter/kotlin-java/kotlin-adapter.js +48 -0
  113. package/dist/graph/adapter/kotlin-java/kotlin-extract.js +58 -0
  114. package/dist/graph/adapter/kotlin-java/runtime.js +75 -0
  115. package/dist/graph/adapter/python/adapter.js +45 -0
  116. package/dist/graph/adapter/python/extract.js +121 -0
  117. package/dist/graph/adapter/python/index.js +10 -0
  118. package/dist/graph/adapter/python/runtime.js +70 -0
  119. package/dist/graph/adapter/registry.js +82 -0
  120. package/dist/graph/adapter/rust/adapter.js +45 -0
  121. package/dist/graph/adapter/rust/extract.js +110 -0
  122. package/dist/graph/adapter/rust/index.js +10 -0
  123. package/dist/graph/adapter/rust/runtime.js +74 -0
  124. package/dist/graph/adapter/shell/adapter.js +34 -0
  125. package/dist/graph/adapter/shell/extract.js +98 -0
  126. package/dist/graph/adapter/shell/index.js +8 -0
  127. package/dist/graph/adapter/svelte/adapter.js +91 -0
  128. package/dist/graph/adapter/svelte/extract.js +41 -0
  129. package/dist/graph/adapter/svelte/index.js +8 -0
  130. package/dist/graph/adapter/swift/adapter.js +45 -0
  131. package/dist/graph/adapter/swift/extract.js +120 -0
  132. package/dist/graph/adapter/swift/index.js +10 -0
  133. package/dist/graph/adapter/swift/runtime.js +70 -0
  134. package/dist/graph/adapter/types.js +2 -0
  135. package/dist/graph/adapter/typescript-javascript/adapter.js +72 -0
  136. package/dist/graph/adapter/typescript-javascript/extract.js +115 -0
  137. package/dist/graph/adapter/typescript-javascript/index.js +10 -0
  138. package/dist/graph/adapter/typescript-javascript/runtime.js +97 -0
  139. package/dist/graph/capability/index.js +105 -0
  140. package/dist/graph/governance.js +104 -0
  141. package/dist/graph/parser/extract.js +5 -0
  142. package/dist/graph/parser/loader.js +2 -0
  143. package/dist/graph/parser/pipeline.js +194 -0
  144. package/dist/graph/query/index.js +277 -0
  145. package/dist/graph/query/types.js +5 -0
  146. package/dist/graph/resolver/build-edges.js +211 -0
  147. package/dist/graph/resolver/index.js +23 -0
  148. package/dist/graph/resolver/resolve-imports.js +168 -0
  149. package/dist/graph/store/adapter.js +132 -0
  150. package/dist/graph/store/queries.js +124 -0
  151. package/dist/graph/store/types.js +4 -0
  152. package/dist/ignore/defaults.js +41 -0
  153. package/dist/ignore/parser.js +39 -0
  154. package/dist/lint/map-reference-validator.js +72 -0
  155. package/dist/loop/abort.js +188 -0
  156. package/dist/loop/abort.test.js +151 -0
  157. package/dist/loop/adapters/__tests__/terminal-cli.test.js +92 -0
  158. package/dist/loop/adapters/agent-subtask.js +137 -0
  159. package/dist/loop/adapters/agent-subtask.test.js +151 -0
  160. package/dist/loop/adapters/cli-subtask-bridge.js +108 -0
  161. package/dist/loop/adapters/index.js +9 -0
  162. package/dist/loop/adapters/registry.js +17 -0
  163. package/dist/loop/adapters/terminal-cli.js +403 -0
  164. package/dist/loop/adapters/types.js +2 -0
  165. package/dist/loop/adapters/worker-instructions.js +85 -0
  166. package/dist/loop/body-parser.js +226 -0
  167. package/dist/loop/bootstrap-packet.js +97 -0
  168. package/dist/loop/bootstrap-packet.test.js +171 -0
  169. package/dist/loop/branch-governance.js +54 -0
  170. package/dist/loop/budget.js +63 -0
  171. package/dist/loop/budget.test.js +163 -0
  172. package/dist/loop/checkpoint.js +325 -0
  173. package/dist/loop/compact-return.js +59 -0
  174. package/dist/loop/continue.js +645 -0
  175. package/dist/loop/continue.test.js +337 -0
  176. package/dist/loop/dispatch-boundary.js +335 -0
  177. package/dist/loop/dispatch-state.js +223 -0
  178. package/dist/loop/dispatch-state.test.js +186 -0
  179. package/dist/loop/dispatch.js +1236 -0
  180. package/dist/loop/dispatch.test.js +499 -0
  181. package/dist/loop/ephemeral-smoke.test.js +160 -0
  182. package/dist/loop/evidence-backfill.js +216 -0
  183. package/dist/loop/execution-adapter.js +141 -0
  184. package/dist/loop/execution-adapter.test.js +66 -0
  185. package/dist/loop/finalize-evidence.js +169 -0
  186. package/dist/loop/git-custody.js +250 -0
  187. package/dist/loop/impl-packet-input.js +38 -0
  188. package/dist/loop/index.js +270 -0
  189. package/dist/loop/ledger.js +49 -0
  190. package/dist/loop/ledger.test.js +127 -0
  191. package/dist/loop/lifecycle-dispatch.js +348 -0
  192. package/dist/loop/lifecycle-dispatch.test.js +318 -0
  193. package/dist/loop/lifecycle.js +93 -0
  194. package/dist/loop/lifecycle.test.js +150 -0
  195. package/dist/loop/orphan-recovery.js +276 -0
  196. package/dist/loop/orphan-recovery.test.js +236 -0
  197. package/dist/loop/parent.js +1566 -0
  198. package/dist/loop/parent.test.js +738 -0
  199. package/dist/loop/provider-sealed-packet.test.js +164 -0
  200. package/dist/loop/resume.js +295 -0
  201. package/dist/loop/resume.test.js +172 -0
  202. package/dist/loop/run-bootstrap.js +234 -0
  203. package/dist/loop/run-preflight.js +203 -0
  204. package/dist/loop/status.js +665 -0
  205. package/dist/loop/status.test.js +235 -0
  206. package/dist/loop/wait.js +86 -0
  207. package/dist/loop/worker-packet.js +406 -0
  208. package/dist/loop/worker-packet.test.js +243 -0
  209. package/dist/loop/worker-prompt.js +260 -0
  210. package/dist/loop/worker-prompt.test.js +247 -0
  211. package/dist/loop/worker.js +498 -0
  212. package/dist/loop/worker.test.js +372 -0
  213. package/dist/map/atlas.js +77 -0
  214. package/dist/map/backfill.js +171 -0
  215. package/dist/map/backfill.test.js +120 -0
  216. package/dist/map/index.js +255 -0
  217. package/dist/map/inference.js +73 -0
  218. package/dist/map/inference.test.js +35 -0
  219. package/dist/map/query.js +157 -0
  220. package/dist/map/query.test.js +174 -0
  221. package/dist/map/update.js +355 -0
  222. package/dist/map/update.test.js +67 -0
  223. package/dist/map/validate.js +153 -0
  224. package/dist/map/validate.test.js +90 -0
  225. package/dist/map/welfare.js +86 -0
  226. package/dist/mcp/lib/invoke.js +35 -0
  227. package/dist/mcp/lib/redact.js +24 -0
  228. package/dist/mcp/lib/redact.test.js +112 -0
  229. package/dist/mcp/lib/root.js +41 -0
  230. package/dist/mcp/lib/root.test.js +114 -0
  231. package/dist/mcp/server.js +15 -0
  232. package/dist/mcp/tools/claim-child.js +119 -0
  233. package/dist/mcp/tools/claim-child.test.js +69 -0
  234. package/dist/mcp/tools/current-state.js +73 -0
  235. package/dist/mcp/tools/current-state.test.js +114 -0
  236. package/dist/mcp/tools/dispatch-result.js +186 -0
  237. package/dist/mcp/tools/dispatch-result.test.js +109 -0
  238. package/dist/mcp/tools/index.js +222 -0
  239. package/dist/mcp/tools/index.test.js +172 -0
  240. package/dist/mcp/tools/linear.js +74 -0
  241. package/dist/mcp/tools/loop-continue.js +159 -0
  242. package/dist/mcp/tools/loop-continue.test.js +186 -0
  243. package/dist/mcp/tools/loop-dry-run.js +15 -0
  244. package/dist/mcp/tools/status.js +65 -0
  245. package/dist/medic/chart-id.js +75 -0
  246. package/dist/medic/chart-schema.js +139 -0
  247. package/dist/runs/index.js +119 -0
  248. package/dist/runs/index.test.js +184 -0
  249. package/dist/runs/ledger-tail.js +15 -0
  250. package/dist/runs/list.js +29 -0
  251. package/dist/runs/reconcile.js +74 -0
  252. package/dist/runs/show.js +10 -0
  253. package/dist/runtime/audit/logger.js +15 -0
  254. package/dist/runtime/audit/reader.js +42 -0
  255. package/dist/runtime/checkpoint.js +89 -0
  256. package/dist/runtime/continuation/confirmed.js +225 -0
  257. package/dist/runtime/continuation/confirmed.test.js +421 -0
  258. package/dist/runtime/continuation/dry-run.js +74 -0
  259. package/dist/runtime/execution-window.js +106 -0
  260. package/dist/runtime/scheduling/child-selector.js +16 -0
  261. package/dist/runtime/state.js +43 -0
  262. package/dist/runtime/status.js +46 -0
  263. package/dist/runtime/verification/envelope.js +80 -0
  264. package/dist/runtime/verification/fingerprint.js +26 -0
  265. package/dist/skill-packet/cli.test.js +105 -0
  266. package/dist/skill-packet/generator.js +225 -0
  267. package/dist/skill-packet/generator.test.js +159 -0
  268. package/dist/skill-packet/index.js +49 -0
  269. package/dist/skill-packet/types.js +2 -0
  270. package/dist/smartdocs-engine/audit.js +194 -0
  271. package/dist/smartdocs-engine/audit.test.js +122 -0
  272. package/dist/smartdocs-engine/canon-check.js +340 -0
  273. package/dist/smartdocs-engine/canon-check.test.js +234 -0
  274. package/dist/smartdocs-engine/doctrine.js +633 -0
  275. package/dist/smartdocs-engine/doctrine.test.js +374 -0
  276. package/dist/smartdocs-engine/index.js +456 -0
  277. package/dist/smartdocs-engine/index.test.js +82 -0
  278. package/dist/smartdocs-engine/ingest.js +495 -0
  279. package/dist/smartdocs-engine/ingest.test.js +163 -0
  280. package/dist/smartdocs-engine/migrate.js +217 -0
  281. package/dist/smartdocs-engine/migrate.test.js +200 -0
  282. package/dist/smartdocs-engine/seed-instructions.js +335 -0
  283. package/dist/smartdocs-engine/seed-instructions.test.js +275 -0
  284. package/dist/smartdocs-engine/smartdoc-ignore.js +258 -0
  285. package/dist/smartdocs-engine/smartdoc-ignore.test.js +146 -0
  286. package/dist/smartdocs-engine/validate-instructions.js +366 -0
  287. package/dist/smartdocs-engine/validate-instructions.test.js +283 -0
  288. package/dist/tracker/adapters/github/index.js +305 -0
  289. package/dist/tracker/adapters/jira/index.js +293 -0
  290. package/dist/tracker/adapters/linear/http-errors.test.js +51 -0
  291. package/dist/tracker/adapters/linear/index.js +621 -0
  292. package/dist/tracker/adapters/linear/index.test.js +238 -0
  293. package/dist/tracker/adapters/linear.js +50 -0
  294. package/dist/tracker/adapters/local/index.js +10 -0
  295. package/dist/tracker/adapters/local-file/index.js +159 -0
  296. package/dist/tracker/adapters/mcp-bridge.js +158 -0
  297. package/dist/tracker/adapters/mcp-bridge.test.js +12 -0
  298. package/dist/tracker/adapters/spec/index.js +146 -0
  299. package/dist/tracker/capabilities.js +2 -0
  300. package/dist/tracker/index.js +49 -0
  301. package/dist/tracker/lifecycle-policy.js +139 -0
  302. package/dist/tracker/lifecycle-transition.js +164 -0
  303. package/dist/tracker/local-graph.js +98 -0
  304. package/dist/tracker/migration.js +56 -0
  305. package/dist/tracker/schema.js +86 -0
  306. package/dist/tracker/sync/index.js +404 -0
  307. package/dist/tracker/sync/queue-store.js +95 -0
  308. package/dist/tracker/sync/queue-store.test.js +104 -0
  309. package/dist/tracker/types.js +11 -0
  310. package/dist/types/linear.js +3 -0
  311. package/dist/types/result-packet.js +12 -0
  312. package/dist/types/runtime-state.js +2 -0
  313. package/dist/types/work-contract.js +2 -0
  314. package/dist/utils/monotonic-timestamp.js +13 -0
  315. package/dist/utils/monotonic-timestamp.test.js +19 -0
  316. package/package.json +64 -0
@@ -0,0 +1,633 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CANDIDATE_MARKER = void 0;
4
+ exports.parseFrontMatter = parseFrontMatter;
5
+ exports.addCandidateGovernanceMetadata = addCandidateGovernanceMetadata;
6
+ exports.stampIngestFrontMatter = stampIngestFrontMatter;
7
+ exports.doctrineDraft = doctrineDraft;
8
+ exports.doctrinePromote = doctrinePromote;
9
+ exports.doctrineDeprecate = doctrineDeprecate;
10
+ exports.specPromote = specPromote;
11
+ exports.migrateProvenance = migrateProvenance;
12
+ const node_fs_1 = require("node:fs");
13
+ const node_path_1 = require("node:path");
14
+ exports.CANDIDATE_MARKER = "<!-- polaris:doctrine-candidate -->";
15
+ function generateRunId() {
16
+ return `polaris-doctrine-${new Date().toISOString().slice(0, 10)}-001`;
17
+ }
18
+ function lifecycleFilePath(repoRoot, runId) {
19
+ return (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-doctrine", runId, "lifecycle.jsonl");
20
+ }
21
+ /**
22
+ * Builds the filesystem path to the audit JSONL file for a given doctrine run.
23
+ *
24
+ * @param repoRoot - The repository root directory
25
+ * @param runId - The doctrine run identifier
26
+ * @returns The full path to `.taskchain_artifacts/polaris-doctrine/<runId>/audit.jsonl`
27
+ */
28
+ function auditFilePath(repoRoot, runId) {
29
+ return (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-doctrine", runId, "audit.jsonl");
30
+ }
31
+ /**
32
+ * Parse YAML-style frontmatter from a Markdown string into a map of raw key/value pairs.
33
+ *
34
+ * Normalizes CRLF to LF and tolerates a leading candidate marker line. If a frontmatter
35
+ * block delimited by `---` is found, each line inside the block is split at the first
36
+ * `:`; keys are lowercased and trimmed, values are trimmed and stripped of surrounding
37
+ * single/double quotes. If no well-formed frontmatter is present, an empty map is returned.
38
+ *
39
+ * @param content - The raw file content to inspect for a frontmatter block.
40
+ * @returns A Map where each entry is `key -> value` from the frontmatter; keys are lowercased and values are trimmed with surrounding quotes removed.
41
+ */
42
+ function parseFrontMatterRaw(content) {
43
+ const result = new Map();
44
+ const normalized = content.replace(/\r\n/g, "\n");
45
+ const stripped = normalized.startsWith(exports.CANDIDATE_MARKER)
46
+ ? normalized.slice(exports.CANDIDATE_MARKER.length).replace(/^\n/, "")
47
+ : normalized;
48
+ if (!stripped.startsWith("---\n"))
49
+ return result;
50
+ const end = stripped.indexOf("\n---", 4);
51
+ if (end === -1)
52
+ return result;
53
+ const lines = stripped.slice(4, end).split("\n");
54
+ for (const line of lines) {
55
+ const colonIdx = line.indexOf(":");
56
+ if (colonIdx === -1)
57
+ continue;
58
+ const key = line.slice(0, colonIdx).trim().toLowerCase();
59
+ const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
60
+ result.set(key, value);
61
+ }
62
+ return result;
63
+ }
64
+ /**
65
+ * Extracts YAML-style front matter from markdown content into a ParsedFrontMatter object.
66
+ *
67
+ * Strips a leading candidate marker line if present and parses key:value pairs from a YAML-style block delimited by `---`.
68
+ *
69
+ * @param content - Markdown file content to parse
70
+ * @returns The parsed front matter as a ParsedFrontMatter object where keys are front-matter fields and values are their string values
71
+ */
72
+ function parseFrontMatter(content) {
73
+ const raw = parseFrontMatterRaw(content);
74
+ const result = {};
75
+ for (const [key, value] of raw) {
76
+ result[key] = value;
77
+ }
78
+ return result;
79
+ }
80
+ /**
81
+ * Ensure a candidate governance frontmatter block exists and add placeholder governance and relationship fields without overwriting existing keys.
82
+ *
83
+ * Normalizes CRLF to LF. If a YAML-style frontmatter block is present it appends any missing governance keys; if no frontmatter exists it prepends a new block containing the default governance and relationship scaffold. Existing frontmatter keys and their values are preserved.
84
+ *
85
+ * @param content - The markdown document content to update
86
+ * @param docType - The value to use for the `doc-type` field in the frontmatter
87
+ * @returns The markdown content with governance placeholder frontmatter added or updated
88
+ */
89
+ function addCandidateGovernanceMetadata(content, docType) {
90
+ const govDefaults = {
91
+ "doc-type": docType,
92
+ "confidence": "0.0",
93
+ "recommended-action": "hold",
94
+ "overlap-analysis": "pending",
95
+ // Relationship scaffolding — populated by author before promotion
96
+ "implements": "",
97
+ "related": "",
98
+ "supersedes": "",
99
+ "superseded_by": "",
100
+ "depends_on": "",
101
+ "validates": "",
102
+ "source_paths": "",
103
+ };
104
+ // Normalize line endings to Unix style
105
+ const normalized = content.replace(/\r\n/g, "\n");
106
+ if (normalized.startsWith("---\n")) {
107
+ const end = normalized.indexOf("\n---", 4);
108
+ if (end !== -1) {
109
+ const frontMatter = normalized.slice(4, end);
110
+ const afterFrontMatter = normalized.slice(end + 4);
111
+ const lines = frontMatter.split("\n");
112
+ const existingKeys = new Set(lines
113
+ .filter((l) => l.includes(":"))
114
+ .map((l) => l.slice(0, l.indexOf(":")).trim().toLowerCase()));
115
+ const additions = [];
116
+ for (const [key, val] of Object.entries(govDefaults)) {
117
+ if (!existingKeys.has(key))
118
+ additions.push(`${key}: ${val}`);
119
+ }
120
+ if (additions.length === 0)
121
+ return normalized;
122
+ return `---\n${frontMatter}\n${additions.join("\n")}\n---${afterFrontMatter}`;
123
+ }
124
+ }
125
+ // No front matter — create one
126
+ const fields = Object.entries(govDefaults)
127
+ .map(([k, v]) => `${k}: ${v}`)
128
+ .join("\n");
129
+ return `---\n${fields}\n---\n\n${normalized}`;
130
+ }
131
+ /**
132
+ * Stamp ingest provenance directly into a SmartDoc's frontmatter.
133
+ *
134
+ * Instead of writing a separate `.provenance.json` sidecar, this writes
135
+ * the same metadata into the file's own YAML frontmatter using canonical
136
+ * schema fields. Existing frontmatter keys are never overwritten.
137
+ *
138
+ * Fields written (only if not already present):
139
+ * - `source` ← originalPath
140
+ * - `ingest-run-id` ← ingestRunId
141
+ * - `ingest-cluster` ← ingestClusterId
142
+ * - `classified-as` ← classifiedAs
143
+ * - `linked-map-area` ← linkedMapArea
144
+ * - `ingested-at` ← ISO timestamp
145
+ * - `status` ← "raw" (if not already set)
146
+ *
147
+ * @param content - The current file content (will be read from disk by caller)
148
+ * @param stamp - Ingest metadata to embed
149
+ * @returns The updated file content with provenance stamped in frontmatter
150
+ */
151
+ function stampIngestFrontMatter(content, stamp) {
152
+ const fields = {
153
+ source: stamp.originalPath,
154
+ "ingest-run-id": stamp.ingestRunId,
155
+ ...(stamp.ingestClusterId ? { "ingest-cluster": stamp.ingestClusterId } : {}),
156
+ "classified-as": stamp.classifiedAs,
157
+ ...(stamp.linkedMapArea ? { "linked-map-area": stamp.linkedMapArea } : {}),
158
+ "ingested-at": stamp.ingestedAt,
159
+ status: "raw",
160
+ };
161
+ const normalized = content.replace(/\r\n/g, "\n");
162
+ if (normalized.startsWith("---\n")) {
163
+ const end = normalized.indexOf("\n---", 4);
164
+ if (end !== -1) {
165
+ const frontMatter = normalized.slice(4, end);
166
+ const afterFrontMatter = normalized.slice(end + 4);
167
+ const existingKeys = new Set(frontMatter
168
+ .split("\n")
169
+ .filter((l) => l.includes(":"))
170
+ .map((l) => l.slice(0, l.indexOf(":")).trim().toLowerCase()));
171
+ const additions = [];
172
+ for (const [key, val] of Object.entries(fields)) {
173
+ if (!existingKeys.has(key.toLowerCase()))
174
+ additions.push(`${key}: ${val}`);
175
+ }
176
+ if (additions.length === 0)
177
+ return normalized;
178
+ return `---\n${frontMatter}\n${additions.join("\n")}\n---${afterFrontMatter}`;
179
+ }
180
+ }
181
+ // No frontmatter — create one
182
+ const block = Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join("\n");
183
+ return `---\n${block}\n---\n\n${normalized}`;
184
+ }
185
+ function appendLifecycle(lifecyclePath, event) {
186
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(lifecyclePath), { recursive: true });
187
+ (0, node_fs_1.appendFileSync)(lifecyclePath, JSON.stringify(event) + "\n", "utf-8");
188
+ }
189
+ /**
190
+ * Resolve a filesystem path against a repository root and return an absolute path.
191
+ *
192
+ * @param path - The input path to resolve; may be absolute or relative.
193
+ * @param repoRoot - The repository root used to resolve relative paths.
194
+ * @returns An absolute filesystem path. If `path` starts with `/` it is returned unchanged; otherwise the result of resolving `path` against `repoRoot`.
195
+ */
196
+ function resolvePath(path, repoRoot) {
197
+ if (path.startsWith("/"))
198
+ return path;
199
+ return (0, node_path_1.join)((0, node_path_1.resolve)(repoRoot), path);
200
+ }
201
+ /**
202
+ * Move a markdown file from the repository's smartdocs/raw/ directory into smartdocs/doctrine/candidate/,
203
+ * mark it as a doctrine candidate, delete the original, and append a lifecycle event.
204
+ *
205
+ * @param path - Path to the source file; if not absolute, it is resolved relative to `options.repoRoot`
206
+ * @param options - Operation options; `options.repoRoot` specifies the repository root and `options.runId` may override the generated run id
207
+ * @returns An object containing `source` (resolved source path), `destination` (path in the candidate directory), `runId` (the run identifier used), and `lifecyclePath` (path to the lifecycle log file)
208
+ */
209
+ function doctrineDraft(path, options) {
210
+ const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
211
+ const runId = options.runId ?? generateRunId();
212
+ const source = resolvePath(path, repoRoot);
213
+ if (!(0, node_fs_1.existsSync)(source)) {
214
+ throw new Error(`Source file not found: ${source}`);
215
+ }
216
+ const rawDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "raw");
217
+ const relToRaw = (0, node_path_1.relative)(rawDir, source);
218
+ const isInRaw = !relToRaw.startsWith("..") && !relToRaw.startsWith("/");
219
+ if (!isInRaw) {
220
+ throw new Error(`doctrineDraft source must be in smartdocs/raw/ — got: ${source}`);
221
+ }
222
+ const candidateDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "candidate");
223
+ (0, node_fs_1.mkdirSync)(candidateDir, { recursive: true });
224
+ const destination = (0, node_path_1.join)(candidateDir, (0, node_path_1.basename)(source));
225
+ if ((0, node_fs_1.existsSync)(destination)) {
226
+ throw new Error(`Destination already exists: ${destination}`);
227
+ }
228
+ const content = (0, node_fs_1.readFileSync)(source, "utf-8");
229
+ (0, node_fs_1.writeFileSync)(destination, `${exports.CANDIDATE_MARKER}\n${content}`, "utf-8");
230
+ (0, node_fs_1.unlinkSync)(source);
231
+ const lifecyclePath = lifecycleFilePath(repoRoot, runId);
232
+ appendLifecycle(lifecyclePath, {
233
+ event: "doctrine-draft",
234
+ run_id: runId,
235
+ source,
236
+ destination,
237
+ timestamp: new Date().toISOString(),
238
+ });
239
+ return { source, destination, runId, lifecyclePath };
240
+ }
241
+ /**
242
+ * Promote a candidate SmartDoc into the doctrine active pool.
243
+ *
244
+ * Moves a Markdown file from smartdocs/doctrine/candidate/ to smartdocs/doctrine/active/,
245
+ * removes the candidate marker, preserves a provenance sidecar if present, records an audit
246
+ * entry, and appends a lifecycle event.
247
+ *
248
+ * @param path - Path to the candidate Markdown file (absolute or repository-relative)
249
+ * @param options - Doctrine operation options (must include `repoRoot`; may include `runId`)
250
+ * @returns The operation result containing `source`, `destination`, `runId`, and `lifecyclePath`
251
+ * @throws If the source file does not exist
252
+ * @throws If the source is not located under smartdocs/doctrine/candidate/
253
+ * @throws If the file does not contain the candidate marker
254
+ * @throws If required governance frontmatter fields are missing
255
+ * @throws If `recommended-action` in frontmatter is not `"promote"`
256
+ * @throws If the destination file already exists
257
+ */
258
+ function doctrinePromote(path, options) {
259
+ const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
260
+ const runId = options.runId ?? generateRunId();
261
+ const source = resolvePath(path, repoRoot);
262
+ if (!(0, node_fs_1.existsSync)(source)) {
263
+ throw new Error(`Source file not found: ${source}`);
264
+ }
265
+ const candidateDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "doctrine", "candidate");
266
+ const relToCandidate = (0, node_path_1.relative)(candidateDir, source);
267
+ const isInCandidate = !relToCandidate.startsWith("..") && !relToCandidate.startsWith("/");
268
+ if (!isInCandidate) {
269
+ throw new Error(`doctrinePromote source must be in smartdocs/doctrine/candidate/ — got: ${source}`);
270
+ }
271
+ const content = (0, node_fs_1.readFileSync)(source, "utf-8");
272
+ if (!content.includes(exports.CANDIDATE_MARKER)) {
273
+ throw new Error(`File is not in candidate state (missing ${exports.CANDIDATE_MARKER}): ${source}`);
274
+ }
275
+ const lifecyclePath = lifecycleFilePath(repoRoot, runId);
276
+ // Governance check
277
+ const fm = parseFrontMatterRaw(content);
278
+ const requiredFields = ["doc-type", "confidence", "recommended-action", "overlap-analysis"];
279
+ for (const field of requiredFields) {
280
+ if (!fm.has(field)) {
281
+ throw new Error(`doctrinePromote: missing required governance field "${field}" in ${source}`);
282
+ }
283
+ }
284
+ const recommendedAction = fm.get("recommended-action");
285
+ if (recommendedAction !== "promote") {
286
+ throw new Error(`doctrinePromote: recommended-action must be "promote" but got "${recommendedAction}" in ${source}`);
287
+ }
288
+ const activeDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "active");
289
+ (0, node_fs_1.mkdirSync)(activeDir, { recursive: true });
290
+ const destination = (0, node_path_1.join)(activeDir, (0, node_path_1.basename)(source));
291
+ if ((0, node_fs_1.existsSync)(destination)) {
292
+ throw new Error(`Destination already exists: ${destination}`);
293
+ }
294
+ const activeContent = content.replace(`${exports.CANDIDATE_MARKER}\n`, "").replace(exports.CANDIDATE_MARKER, "");
295
+ (0, node_fs_1.writeFileSync)(destination, activeContent, "utf-8");
296
+ (0, node_fs_1.unlinkSync)(source);
297
+ // Move co-located provenance sidecar if present
298
+ const provenanceSrc = source.replace(/\.md$/, ".provenance.json");
299
+ if ((0, node_fs_1.existsSync)(provenanceSrc)) {
300
+ const provenanceDest = destination.replace(/\.md$/, ".provenance.json");
301
+ (0, node_fs_1.renameSync)(provenanceSrc, provenanceDest);
302
+ }
303
+ // Write audit record
304
+ const auditPath = auditFilePath(repoRoot, runId);
305
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(auditPath), { recursive: true });
306
+ (0, node_fs_1.appendFileSync)(auditPath, JSON.stringify({
307
+ event: "doctrine-promoted",
308
+ run_id: runId,
309
+ source,
310
+ destination,
311
+ doc_type: fm.get("doc-type") ?? null,
312
+ confidence: fm.has("confidence") ? parseFloat(fm.get("confidence")) : null,
313
+ recommended_action: fm.get("recommended-action") ?? null,
314
+ overlap_analysis: fm.get("overlap-analysis") ?? null,
315
+ promoted_by: "polaris-cli",
316
+ timestamp: new Date().toISOString(),
317
+ }) + "\n", "utf-8");
318
+ appendLifecycle(lifecyclePath, {
319
+ event: "doctrine-promote",
320
+ run_id: runId,
321
+ source,
322
+ destination,
323
+ timestamp: new Date().toISOString(),
324
+ });
325
+ return { source, destination, runId, lifecyclePath };
326
+ }
327
+ /**
328
+ * Move a doctrine document from smartdocs/doctrine/active/ into smartdocs/doctrine/deprecated/ and record the deprecation event.
329
+ *
330
+ * @param path - Absolute or repository-relative path to the source markdown file; must be located in smartdocs/doctrine/active/.
331
+ * @param options - Options object containing `repoRoot` (repository root used to resolve relative paths) and an optional `runId`.
332
+ * @returns An object with `source` (original path), `destination` (new path under deprecated), `runId` used for this operation, and `lifecyclePath` where the lifecycle event was appended.
333
+ * @throws Error if the source file does not exist, if the source is not inside smartdocs/doctrine/active/, or if the destination file already exists.
334
+ */
335
+ function doctrineDeprecate(path, options) {
336
+ const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
337
+ const runId = options.runId ?? generateRunId();
338
+ const source = resolvePath(path, repoRoot);
339
+ if (!(0, node_fs_1.existsSync)(source)) {
340
+ throw new Error(`Source file not found: ${source}`);
341
+ }
342
+ const activeDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "doctrine", "active");
343
+ const relToActive = (0, node_path_1.relative)(activeDir, source);
344
+ const isInActive = !relToActive.startsWith("..") && !relToActive.startsWith("/");
345
+ if (!isInActive) {
346
+ throw new Error(`doctrineDeprecate source must be in smartdocs/doctrine/active/ — got: ${source}`);
347
+ }
348
+ const deprecatedDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "deprecated");
349
+ (0, node_fs_1.mkdirSync)(deprecatedDir, { recursive: true });
350
+ const destination = (0, node_path_1.join)(deprecatedDir, (0, node_path_1.basename)(source));
351
+ if ((0, node_fs_1.existsSync)(destination)) {
352
+ throw new Error(`Destination already exists: ${destination}`);
353
+ }
354
+ const content = (0, node_fs_1.readFileSync)(source, "utf-8");
355
+ const deprecatedAt = new Date().toISOString();
356
+ const deprecatedContent = `<!-- polaris:doctrine-deprecated deprecatedAt="${deprecatedAt}" runId="${runId}" -->\n${content}`;
357
+ (0, node_fs_1.writeFileSync)(destination, deprecatedContent, "utf-8");
358
+ (0, node_fs_1.unlinkSync)(source);
359
+ // Move co-located provenance sidecar if present
360
+ const provenanceSrc = source.replace(/\.md$/, ".provenance.json");
361
+ if ((0, node_fs_1.existsSync)(provenanceSrc)) {
362
+ const provenanceDest = destination.replace(/\.md$/, ".provenance.json");
363
+ (0, node_fs_1.renameSync)(provenanceSrc, provenanceDest);
364
+ }
365
+ const lifecyclePath = lifecycleFilePath(repoRoot, runId);
366
+ appendLifecycle(lifecyclePath, {
367
+ event: "doctrine-deprecate",
368
+ run_id: runId,
369
+ source,
370
+ destination,
371
+ deprecated_at: deprecatedAt,
372
+ timestamp: deprecatedAt,
373
+ });
374
+ return { source, destination, runId, lifecyclePath };
375
+ }
376
+ // ── Spec verb-keyword conflict detection ──────────────────────────────────────
377
+ const MODAL_REQUIRES = /\b(?:must\s+always|always|must)\s+(\w+)/gi;
378
+ const MODAL_PROHIBITS = /\b(?:must\s+never|must\s+not|never)\s+(\w+)/gi;
379
+ const CONFLICT_STOPWORDS = new Set([
380
+ "this", "that", "with", "from", "have", "been", "will", "when", "then",
381
+ "also", "only", "into", "over", "such", "each", "more", "same", "used",
382
+ ]);
383
+ function extractSpecKeywords(content, pattern) {
384
+ const result = new Set();
385
+ const re = new RegExp(pattern.source, pattern.flags);
386
+ for (const match of content.matchAll(re)) {
387
+ const kw = match[1]?.toLowerCase();
388
+ if (kw && kw.length >= 4 && !CONFLICT_STOPWORDS.has(kw))
389
+ result.add(kw);
390
+ }
391
+ return result;
392
+ }
393
+ /**
394
+ * Promotes a spec file from smartdocs/raw/ into smartdocs/specs/active/.
395
+ *
396
+ * Performs two pre-promotion checks: content conflicts against existing active specs
397
+ * (verb-keyword requirements vs prohibitions) and map-area conflicts based on a
398
+ * provenance `linkedMapArea`. If conflicts are found the operation halts and returns
399
+ * a human-readable report unless approval is explicitly provided.
400
+ *
401
+ * @param path - Filesystem path to the source `.md` spec (must reside under `smartdocs/raw/`)
402
+ * @param options - Promotion options and environment:
403
+ * - `repoRoot`: repository root directory
404
+ * - `approve`: when true, proceed with promotion despite detected conflicts
405
+ * - `runId`: optional run identifier to use for lifecycle/audit paths
406
+ * @returns The promotion result containing:
407
+ * - `source`: original source path
408
+ * - `destination`: destination path under `smartdocs/specs/active/` (empty string if halted)
409
+ * - `runId`: run identifier used
410
+ * - `lifecyclePath`: path to the lifecycle log for this run
411
+ * - `conflicts`: array of detected `SpecConflict` entries
412
+ * - `halted`: `true` if promotion was stopped due to conflicts and `approve` was not set
413
+ * - `report`: multi-line text report summarizing detected conflicts and action taken
414
+ */
415
+ function specPromote(path, options) {
416
+ const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
417
+ const runId = options.runId ?? generateRunId();
418
+ const source = resolvePath(path, repoRoot);
419
+ const lifecyclePath = lifecycleFilePath(repoRoot, runId);
420
+ if (!(0, node_fs_1.existsSync)(source)) {
421
+ throw new Error(`Source file not found: ${source}`);
422
+ }
423
+ const rawDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "raw");
424
+ const relToRaw = (0, node_path_1.relative)(rawDir, source);
425
+ const isInRaw = !relToRaw.startsWith("..") && !relToRaw.startsWith("/");
426
+ if (!isInRaw) {
427
+ throw new Error(`specPromote source must be in smartdocs/raw/ — got: ${source}`);
428
+ }
429
+ const content = (0, node_fs_1.readFileSync)(source, "utf-8");
430
+ const conflicts = [];
431
+ // 1. Content conflict check against specs/active/
432
+ const activeSpecsDir = (0, node_path_1.resolve)(repoRoot, "smartdocs", "specs", "active");
433
+ if ((0, node_fs_1.existsSync)(activeSpecsDir)) {
434
+ const activeFiles = (0, node_fs_1.readdirSync)(activeSpecsDir).filter((f) => f.endsWith(".md"));
435
+ const incomingRequires = extractSpecKeywords(content, MODAL_REQUIRES);
436
+ const incomingProhibits = extractSpecKeywords(content, MODAL_PROHIBITS);
437
+ for (const file of activeFiles) {
438
+ let activeContent;
439
+ try {
440
+ activeContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(activeSpecsDir, file), "utf-8");
441
+ }
442
+ catch {
443
+ continue;
444
+ }
445
+ const activeRequires = extractSpecKeywords(activeContent, MODAL_REQUIRES);
446
+ const activeProhibits = extractSpecKeywords(activeContent, MODAL_PROHIBITS);
447
+ for (const kw of incomingProhibits) {
448
+ if (activeRequires.has(kw)) {
449
+ conflicts.push({ type: "content", conflictingFile: file, detail: `incoming doc prohibits "${kw}" but ${file} requires it` });
450
+ }
451
+ }
452
+ for (const kw of incomingRequires) {
453
+ if (activeProhibits.has(kw)) {
454
+ conflicts.push({ type: "content", conflictingFile: file, detail: `incoming doc requires "${kw}" but ${file} prohibits it` });
455
+ }
456
+ }
457
+ }
458
+ }
459
+ // 2. Map conflict check — linkedMapArea already has an active spec
460
+ const provenanceSrcPath = source.replace(/\.md$/, ".provenance.json");
461
+ let linkedMapArea = null;
462
+ if ((0, node_fs_1.existsSync)(provenanceSrcPath)) {
463
+ try {
464
+ const prov = JSON.parse((0, node_fs_1.readFileSync)(provenanceSrcPath, "utf-8"));
465
+ linkedMapArea = prov.linkedMapArea ?? null;
466
+ }
467
+ catch { /* ignore */ }
468
+ }
469
+ if (linkedMapArea && (0, node_fs_1.existsSync)(activeSpecsDir)) {
470
+ const activeFiles = (0, node_fs_1.readdirSync)(activeSpecsDir).filter((f) => f.endsWith(".md"));
471
+ for (const file of activeFiles) {
472
+ let activeContent;
473
+ try {
474
+ activeContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(activeSpecsDir, file), "utf-8");
475
+ }
476
+ catch {
477
+ continue;
478
+ }
479
+ if (activeContent.includes(linkedMapArea)) {
480
+ conflicts.push({ type: "map", conflictingFile: file, detail: `linked map area "${linkedMapArea}" is already referenced by active spec ${file}` });
481
+ }
482
+ }
483
+ }
484
+ // 3. Build report
485
+ const reportLines = [`spec-promote: ${(0, node_path_1.basename)(source)}`];
486
+ if (conflicts.length === 0) {
487
+ reportLines.push(" no content conflicts detected");
488
+ reportLines.push(" no map area conflicts detected");
489
+ }
490
+ else {
491
+ reportLines.push(` ${conflicts.length} conflict(s) detected:`);
492
+ for (const c of conflicts) {
493
+ reportLines.push(` [${c.type}] ${c.detail}`);
494
+ }
495
+ if (!options.approve) {
496
+ reportLines.push("");
497
+ reportLines.push(" Halted. Resolve conflicts or rerun with --approve to override.");
498
+ }
499
+ }
500
+ const report = reportLines.join("\n");
501
+ // 4. Halt if conflicts and not approved
502
+ if (conflicts.length > 0 && !options.approve) {
503
+ appendLifecycle(lifecyclePath, {
504
+ event: "spec-promote-halted",
505
+ run_id: runId,
506
+ source,
507
+ conflicts,
508
+ timestamp: new Date().toISOString(),
509
+ });
510
+ return { source, destination: "", runId, lifecyclePath, conflicts, halted: true, report };
511
+ }
512
+ // 5. Promote
513
+ const activeDir = (0, node_path_1.join)(repoRoot, "smartdocs", "specs", "active");
514
+ (0, node_fs_1.mkdirSync)(activeDir, { recursive: true });
515
+ const destination = (0, node_path_1.join)(activeDir, (0, node_path_1.basename)(source));
516
+ if ((0, node_fs_1.existsSync)(destination)) {
517
+ throw new Error(`Destination already exists: ${destination}`);
518
+ }
519
+ (0, node_fs_1.renameSync)(source, destination);
520
+ if ((0, node_fs_1.existsSync)(provenanceSrcPath)) {
521
+ (0, node_fs_1.renameSync)(provenanceSrcPath, destination.replace(/\.md$/, ".provenance.json"));
522
+ }
523
+ appendLifecycle(lifecyclePath, {
524
+ event: "spec-promote",
525
+ run_id: runId,
526
+ source,
527
+ destination,
528
+ conflicts_at_promote: conflicts.length,
529
+ approved: options.approve ?? false,
530
+ timestamp: new Date().toISOString(),
531
+ });
532
+ return { source, destination, runId, lifecyclePath, conflicts, halted: false, report };
533
+ }
534
+ /**
535
+ * Migrate .provenance.json sidecars to in-file frontmatter stamps.
536
+ *
537
+ * Walks all of smartdocs/ recursively, finds every .provenance.json file,
538
+ * reads the paired .md file, calls stampIngestFrontMatter() to embed provenance
539
+ * in its frontmatter, writes the updated .md, then deletes the sidecar.
540
+ *
541
+ * In dry-run mode: reports what would be done without writing or deleting anything.
542
+ */
543
+ function migrateProvenance(options) {
544
+ const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
545
+ const runId = options.runId ?? `polaris-provenance-migration-${new Date().toISOString().slice(0, 10)}-001`;
546
+ const lifecyclePath = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-doctrine", runId, "lifecycle.jsonl");
547
+ const smartdocsDir = (0, node_path_1.join)(repoRoot, "smartdocs");
548
+ if (!(0, node_fs_1.existsSync)(smartdocsDir)) {
549
+ throw new Error(`migrateProvenance: smartdocs/ directory not found at ${smartdocsDir}`);
550
+ }
551
+ const records = [];
552
+ function walk(dir) {
553
+ let entries;
554
+ try {
555
+ entries = (0, node_fs_1.readdirSync)(dir);
556
+ }
557
+ catch {
558
+ return;
559
+ }
560
+ for (const entry of entries) {
561
+ const full = (0, node_path_1.join)(dir, entry);
562
+ try {
563
+ const stat = (0, node_fs_1.statSync)(full);
564
+ if (stat.isDirectory()) {
565
+ walk(full);
566
+ }
567
+ else if (entry.endsWith(".provenance.json")) {
568
+ const mdPath = full.replace(/\.provenance\.json$/, ".md");
569
+ const relSidecar = (0, node_path_1.relative)(repoRoot, full).replace(/\\/g, "/");
570
+ const relMd = (0, node_path_1.relative)(repoRoot, mdPath).replace(/\\/g, "/");
571
+ if (!(0, node_fs_1.existsSync)(mdPath)) {
572
+ records.push({ sidecar: relSidecar, mdFile: relMd, status: "skipped-no-md" });
573
+ appendLifecycle(lifecyclePath, {
574
+ event: "provenance-migration-skipped",
575
+ run_id: runId,
576
+ sidecar: relSidecar,
577
+ reason: "no paired .md file",
578
+ timestamp: new Date().toISOString(),
579
+ });
580
+ continue;
581
+ }
582
+ if (options.dryRun) {
583
+ records.push({ sidecar: relSidecar, mdFile: relMd, status: "skipped-dry-run" });
584
+ continue;
585
+ }
586
+ try {
587
+ // Read sidecar fields to build the stamp
588
+ const sidecar = JSON.parse((0, node_fs_1.readFileSync)(full, "utf-8"));
589
+ const mdContent = (0, node_fs_1.readFileSync)(mdPath, "utf-8");
590
+ const stamp = {
591
+ originalPath: sidecar.originalPath ?? relMd,
592
+ classifiedAs: sidecar.classifiedAs ?? "unknown",
593
+ ingestRunId: sidecar.ingestRunId ?? "migrated",
594
+ ingestClusterId: sidecar.ingestClusterId ?? null,
595
+ linkedMapArea: sidecar.linkedMapArea ?? null,
596
+ ingestedAt: sidecar.ingestedAt ?? new Date().toISOString(),
597
+ };
598
+ const stamped = stampIngestFrontMatter(mdContent, stamp);
599
+ (0, node_fs_1.writeFileSync)(mdPath, stamped, "utf-8");
600
+ (0, node_fs_1.unlinkSync)(full);
601
+ records.push({ sidecar: relSidecar, mdFile: relMd, status: "stamped" });
602
+ appendLifecycle(lifecyclePath, {
603
+ event: "provenance-migration-stamped",
604
+ run_id: runId,
605
+ sidecar: relSidecar,
606
+ md_file: relMd,
607
+ timestamp: new Date().toISOString(),
608
+ });
609
+ }
610
+ catch (err) {
611
+ const msg = err instanceof Error ? err.message : String(err);
612
+ records.push({ sidecar: relSidecar, mdFile: relMd, status: "error", error: msg });
613
+ appendLifecycle(lifecyclePath, {
614
+ event: "provenance-migration-error",
615
+ run_id: runId,
616
+ sidecar: relSidecar,
617
+ error: msg,
618
+ timestamp: new Date().toISOString(),
619
+ });
620
+ }
621
+ }
622
+ }
623
+ catch {
624
+ continue;
625
+ }
626
+ }
627
+ }
628
+ walk(smartdocsDir);
629
+ const stamped = records.filter((r) => r.status === "stamped").length;
630
+ const skipped = records.filter((r) => r.status === "skipped-no-md" || r.status === "skipped-dry-run").length;
631
+ const errors = records.filter((r) => r.status === "error").length;
632
+ return { runId, lifecyclePath: (0, node_path_1.relative)(repoRoot, lifecyclePath).replace(/\\/g, "/"), records, stamped, skipped, errors };
633
+ }