@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,495 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SMART_DOCS_SCAFFOLD_DIRS = exports.CANONICAL_TARGET = void 0;
4
+ exports.classifyDoc = classifyDoc;
5
+ exports.ensureDocsScaffold = ensureDocsScaffold;
6
+ exports.ingestDocs = ingestDocs;
7
+ exports.printIngestResults = printIngestResults;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const loader_js_1 = require("../config/loader.js");
11
+ const atlas_js_1 = require("../map/atlas.js");
12
+ const monotonic_timestamp_js_1 = require("../utils/monotonic-timestamp.js");
13
+ const smartdoc_ignore_js_1 = require("./smartdoc-ignore.js");
14
+ const doctrine_js_1 = require("./doctrine.js");
15
+ const summary_delta_js_1 = require("../cognition/summary-delta.js");
16
+ exports.CANONICAL_TARGET = "smartdocs";
17
+ const DOCS_INGEST_STATE_FILE = ".taskchain_artifacts/polaris-docs-ingest/current-state.json";
18
+ const DOCS_INGEST_RUNS_DIR = ".taskchain_artifacts/polaris-docs-ingest/runs";
19
+ const DEFAULT_BATCH_LIMIT = 4;
20
+ const TARGET_DIRS = {
21
+ "runtime-summary": `${exports.CANONICAL_TARGET}/runtime/summaries`,
22
+ "run-report": `${exports.CANONICAL_TARGET}/runtime/run-reports`,
23
+ "spec-raw": `${exports.CANONICAL_TARGET}/raw`,
24
+ "spec-active": `${exports.CANONICAL_TARGET}/specs/active`,
25
+ "audit-finding": `${exports.CANONICAL_TARGET}/audits/findings`,
26
+ "doctrine-candidate": `${exports.CANONICAL_TARGET}/doctrine/active`,
27
+ architecture: `${exports.CANONICAL_TARGET}/architecture`,
28
+ decision: `${exports.CANONICAL_TARGET}/decisions`,
29
+ "deprecated-noise": `${exports.CANONICAL_TARGET}/runtime/generated`,
30
+ };
31
+ exports.SMART_DOCS_SCAFFOLD_DIRS = [
32
+ exports.CANONICAL_TARGET,
33
+ ...Object.values(TARGET_DIRS),
34
+ `${exports.CANONICAL_TARGET}/raw`,
35
+ `${exports.CANONICAL_TARGET}/specs/implemented`,
36
+ `${exports.CANONICAL_TARGET}/specs/superseded`,
37
+ `${exports.CANONICAL_TARGET}/audits/resolved`,
38
+ `${exports.CANONICAL_TARGET}/doctrine/active`,
39
+ `${exports.CANONICAL_TARGET}/doctrine/deprecated`,
40
+ ];
41
+ const APPROVAL_REQUIRED = new Set(["spec-active", "architecture", "decision"]);
42
+ /**
43
+ * Conflict detection uses subject-verb-keyword triples rather than bare
44
+ * verb-keyword pairs. A conflict only fires when both the subject AND the
45
+ * keyword match between the ingested doc and an active doctrine file.
46
+ * This prevents false positives such as:
47
+ * doctrine: "SUMMARY.md must contain (standard schema)"
48
+ * ingested: "Workers must not contain stale assumptions"
49
+ * — same keyword (`contain`), completely different subjects.
50
+ */
51
+ const MODAL_REQUIRES_RE = /([A-Za-z][\w/.\-]{0,40})\s+(?:must|always)\s+(\w+)/gi;
52
+ const MODAL_PROHIBITS_RE = /([A-Za-z][\w/.\-]{0,40})\s+(?:never|must\s+not)\s+(\w+)/gi;
53
+ const CONFLICT_STOPWORDS = new Set([
54
+ "this", "that", "with", "from", "have", "been", "will", "when", "then",
55
+ "else", "each", "every", "also", "note", "not", "used", "only", "be",
56
+ "are", "the", "and", "for", "its", "any", "all",
57
+ ]);
58
+ function readJson(filePath, fallback) {
59
+ try {
60
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
61
+ }
62
+ catch (error) {
63
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
64
+ return fallback;
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ function frontMatterValue(content, key) {
70
+ if (!content.startsWith("---\n"))
71
+ return undefined;
72
+ const end = content.indexOf("\n---", 4);
73
+ if (end === -1)
74
+ return undefined;
75
+ const lines = content.slice(4, end).split(/\r?\n/);
76
+ const found = lines.find((line) => line.toLowerCase().startsWith(`${key.toLowerCase()}:`));
77
+ return found?.slice(found.indexOf(":") + 1).trim().replace(/^["']|["']$/g, "");
78
+ }
79
+ function classifyDoc(content, filePath = "") {
80
+ const lower = `${filePath}\n${content}`.toLowerCase();
81
+ const status = frontMatterValue(content, "status")?.toLowerCase();
82
+ const authority = frontMatterValue(content, "authority")?.toLowerCase();
83
+ if ((0, node_path_1.basename)(filePath).toLowerCase() === "summary.md") {
84
+ return "deprecated-noise";
85
+ }
86
+ if (status === "deprecated" || lower.includes("deprecated noise") || lower.includes("obsolete")) {
87
+ return "deprecated-noise";
88
+ }
89
+ if (lower.includes("run report") || lower.includes("run-report"))
90
+ return "run-report";
91
+ if (lower.includes("runtime summary") || lower.includes("session summary"))
92
+ return "runtime-summary";
93
+ if (lower.includes("audit finding") || lower.includes("vulnerability") || lower.includes("security audit")) {
94
+ return "audit-finding";
95
+ }
96
+ if (authority === "doctrine" || lower.includes("doctrine") || lower.includes("must always") || lower.includes("never silently")) {
97
+ return "doctrine-candidate";
98
+ }
99
+ if (lower.includes("architecture decision record") || /^#\s*adr[:\s-]/im.test(content))
100
+ return "decision";
101
+ if (authority === "architecture" || lower.includes("architecture") || lower.includes("structural design"))
102
+ return "architecture";
103
+ if (status === "active" || lower.includes("active spec"))
104
+ return "spec-active";
105
+ if (lower.includes("spec") || lower.includes("acceptance criteria") || lower.includes("implementation plan")) {
106
+ return "spec-raw";
107
+ }
108
+ return "spec-raw";
109
+ }
110
+ function readCurrentState(repoRoot) {
111
+ return readJson((0, node_path_1.resolve)(repoRoot, DOCS_INGEST_STATE_FILE), {});
112
+ }
113
+ function generateRunId(repoRoot) {
114
+ const date = new Date().toISOString().slice(0, 10);
115
+ const runsDir = (0, node_path_1.resolve)(repoRoot, DOCS_INGEST_RUNS_DIR);
116
+ let seq = 1;
117
+ if ((0, node_fs_1.existsSync)(runsDir)) {
118
+ try {
119
+ const existing = (0, node_fs_1.readdirSync)(runsDir).filter((d) => d.includes(date));
120
+ const suffixes = existing
121
+ .map((d) => {
122
+ const match = d.match(/-(\d{3})$/);
123
+ return match ? parseInt(match[1], 10) : 0;
124
+ })
125
+ .filter((n) => !isNaN(n) && n > 0);
126
+ const maxSuffix = suffixes.length > 0 ? Math.max(...suffixes) : 0;
127
+ seq = maxSuffix + 1;
128
+ }
129
+ catch {
130
+ // use seq = 1
131
+ }
132
+ }
133
+ return `polaris-docs-ingest-docs-ingest-${date}-${String(seq).padStart(3, "0")}`;
134
+ }
135
+ function docsIngestTelemetryPath(repoRoot, runId) {
136
+ return (0, node_path_1.resolve)(repoRoot, DOCS_INGEST_RUNS_DIR, runId, "telemetry.jsonl");
137
+ }
138
+ function emitRunStartTelemetry(repoRoot, runId, priorRunId) {
139
+ const telPath = docsIngestTelemetryPath(repoRoot, runId);
140
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telPath), { recursive: true });
141
+ (0, node_fs_1.appendFileSync)(telPath, JSON.stringify({
142
+ event: "run-start",
143
+ run_id: runId,
144
+ prior_run_id: priorRunId,
145
+ timestamp: (0, monotonic_timestamp_js_1.getMonotonicTimestamp)(),
146
+ }) + "\n", "utf-8");
147
+ }
148
+ function emitTelemetry(telPath, runId, event) {
149
+ try {
150
+ (0, node_fs_1.appendFileSync)(telPath, JSON.stringify({ ...event, run_id: runId, timestamp: (0, monotonic_timestamp_js_1.getMonotonicTimestamp)() }) + "\n", "utf-8");
151
+ }
152
+ catch {
153
+ // non-fatal for post-run-start events
154
+ }
155
+ }
156
+ function writeDocsIngestState(repoRoot, state) {
157
+ const statePath = (0, node_path_1.resolve)(repoRoot, DOCS_INGEST_STATE_FILE);
158
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(statePath), { recursive: true });
159
+ (0, node_fs_1.writeFileSync)(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
160
+ }
161
+ function ensureDocsScaffold(repoRoot, options = {}) {
162
+ const created = exports.SMART_DOCS_SCAFFOLD_DIRS.filter((dir) => !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(repoRoot, dir)));
163
+ const existing = exports.SMART_DOCS_SCAFFOLD_DIRS.filter((dir) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(repoRoot, dir)));
164
+ if (!options.dryRun) {
165
+ for (const dir of created) {
166
+ (0, node_fs_1.mkdirSync)((0, node_path_1.resolve)(repoRoot, dir), { recursive: true });
167
+ }
168
+ }
169
+ return {
170
+ created,
171
+ existing,
172
+ dryRun: Boolean(options.dryRun),
173
+ };
174
+ }
175
+ function uniqueDestination(filePath) {
176
+ if (!(0, node_fs_1.existsSync)(filePath))
177
+ return filePath;
178
+ const dir = (0, node_path_1.dirname)(filePath);
179
+ const ext = (0, node_path_1.extname)(filePath);
180
+ const stem = (0, node_path_1.basename)(filePath, ext);
181
+ let index = 2;
182
+ let candidate = (0, node_path_1.join)(dir, `${stem}-${index}${ext}`);
183
+ while ((0, node_fs_1.existsSync)(candidate)) {
184
+ index += 1;
185
+ candidate = (0, node_path_1.join)(dir, `${stem}-${index}${ext}`);
186
+ }
187
+ return candidate;
188
+ }
189
+ function deriveLinkedArea(content, routes) {
190
+ const entries = Object.entries(routes);
191
+ const explicit = entries.find(([filePath]) => content.includes(filePath));
192
+ if (explicit) {
193
+ return { label: explicit[1].route || explicit[0], entry: explicit[1] };
194
+ }
195
+ const area = content.match(/\b(src\/[A-Za-z0-9_/-]+)/)?.[1];
196
+ if (area) {
197
+ const matched = entries.find(([filePath]) => filePath.startsWith(area));
198
+ if (matched)
199
+ return { label: matched[1].route || area, entry: matched[1] };
200
+ return { label: area, entry: null };
201
+ }
202
+ return { label: null, entry: null };
203
+ }
204
+ function updateMapEntry(repoRoot, destinationPath, linkedEntry) {
205
+ const config = (0, loader_js_1.loadConfig)(repoRoot);
206
+ const atlasPath = (0, node_path_1.resolve)(repoRoot, config.repo.sidecarOutputPath ?? ".polaris/map");
207
+ const routes = (0, atlas_js_1.readFileRoutes)(atlasPath);
208
+ const relDestination = (0, node_path_1.relative)(repoRoot, destinationPath).replace(/\\/g, "/");
209
+ const now = new Date().toISOString();
210
+ routes[relDestination] = {
211
+ domain: linkedEntry?.domain ?? "docs",
212
+ route: linkedEntry?.route ?? (0, node_path_1.dirname)(relDestination),
213
+ taskchain: linkedEntry?.taskchain ?? "polaris-docs",
214
+ confidence: linkedEntry ? 0.85 : 0.7,
215
+ classification: "indexed",
216
+ last_updated: now,
217
+ updated_by: "polaris-docs-ingest",
218
+ tags: ["docs", "ingested"],
219
+ instructionFile: linkedEntry?.instructionFile,
220
+ };
221
+ (0, atlas_js_1.writeFileRoutes)(atlasPath, routes);
222
+ const needsReview = (0, atlas_js_1.readNeedsReview)(atlasPath);
223
+ const entries = { ...routes, ...needsReview };
224
+ const instructionCoverage = (0, atlas_js_1.computeInstructionCoverage)(entries);
225
+ (0, atlas_js_1.writeAtlasIndex)(atlasPath, {
226
+ scan_date: now,
227
+ file_count: Object.keys(entries).length,
228
+ coverage_pct: Math.round((Object.values(entries).filter((e) => e.classification === "indexed").length / Object.keys(entries).length) * 100),
229
+ instructionCoverage,
230
+ entries,
231
+ });
232
+ }
233
+ function extractVerbTriples(content, pattern) {
234
+ const result = [];
235
+ const re = new RegExp(pattern.source, pattern.flags);
236
+ for (const match of content.matchAll(re)) {
237
+ const subject = match[1]?.toLowerCase().replace(/[^a-z0-9./\-]/g, "");
238
+ const keyword = match[2]?.toLowerCase();
239
+ if (subject && keyword &&
240
+ keyword.length >= 3 &&
241
+ !CONFLICT_STOPWORDS.has(keyword) &&
242
+ !CONFLICT_STOPWORDS.has(subject)) {
243
+ result.push({ subject, keyword });
244
+ }
245
+ }
246
+ return result;
247
+ }
248
+ /** Returns true if two triple sets share a (subject, keyword) pair. */
249
+ function triplesConflict(a, b) {
250
+ for (const ta of a) {
251
+ for (const tb of b) {
252
+ if (ta.subject === tb.subject && ta.keyword === tb.keyword) {
253
+ return { subject: ta.subject, keyword: ta.keyword };
254
+ }
255
+ }
256
+ }
257
+ return null;
258
+ }
259
+ function detectDoctrineConflict(content, repoRoot) {
260
+ const activeDoctrineDir = (0, node_path_1.resolve)(repoRoot, exports.CANONICAL_TARGET, "doctrine", "active");
261
+ if (!(0, node_fs_1.existsSync)(activeDoctrineDir))
262
+ return null;
263
+ let files;
264
+ try {
265
+ files = (0, node_fs_1.readdirSync)(activeDoctrineDir).filter((f) => f.endsWith(".md"));
266
+ }
267
+ catch {
268
+ return null;
269
+ }
270
+ if (files.length === 0)
271
+ return null;
272
+ const ingestedRequires = extractVerbTriples(content, MODAL_REQUIRES_RE);
273
+ const ingestedProhibits = extractVerbTriples(content, MODAL_PROHIBITS_RE);
274
+ for (const file of files) {
275
+ let docContent;
276
+ try {
277
+ docContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(activeDoctrineDir, file), "utf-8");
278
+ }
279
+ catch {
280
+ continue;
281
+ }
282
+ const docRequires = extractVerbTriples(docContent, MODAL_REQUIRES_RE);
283
+ const docProhibits = extractVerbTriples(docContent, MODAL_PROHIBITS_RE);
284
+ // ingested prohibits X; doctrine requires X — same subject
285
+ const prohibitConflict = triplesConflict(ingestedProhibits, docRequires);
286
+ if (prohibitConflict) {
287
+ return {
288
+ conflictingFile: file,
289
+ detail: `ingested doc prohibits "${prohibitConflict.keyword}" for "${prohibitConflict.subject}" but ${file} requires it`,
290
+ };
291
+ }
292
+ // ingested requires X; doctrine prohibits X — same subject
293
+ const requireConflict = triplesConflict(ingestedRequires, docProhibits);
294
+ if (requireConflict) {
295
+ return {
296
+ conflictingFile: file,
297
+ detail: `ingested doc requires "${requireConflict.keyword}" for "${requireConflict.subject}" but ${file} prohibits it`,
298
+ };
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+ function ingestDocs(files, options) {
304
+ const repoRoot = (0, node_path_1.resolve)(options.repoRoot);
305
+ // Canonical target check (STOP CONDITION)
306
+ const canonicalDir = (0, node_path_1.resolve)(repoRoot, exports.CANONICAL_TARGET);
307
+ if (!(0, node_fs_1.existsSync)(canonicalDir)) {
308
+ throw new Error(`polaris docs ingest: canonical target ${exports.CANONICAL_TARGET}/ not found — halting`);
309
+ }
310
+ const limit = options.maxFiles ?? DEFAULT_BATCH_LIMIT;
311
+ if (files.length === 0)
312
+ throw new Error("polaris docs ingest: provide at least one file");
313
+ if (files.length > limit)
314
+ throw new Error(`polaris docs ingest: batch limit is ${limit} files`);
315
+ const priorState = readCurrentState(repoRoot);
316
+ const clusterId = options.clusterId ?? null;
317
+ const runId = generateRunId(repoRoot);
318
+ // Emit run-start telemetry (STOP CONDITION if this write fails)
319
+ emitRunStartTelemetry(repoRoot, runId, priorState.run_id ?? null);
320
+ const telPath = docsIngestTelemetryPath(repoRoot, runId);
321
+ ensureDocsScaffold(repoRoot);
322
+ const config = (0, loader_js_1.loadConfig)(repoRoot);
323
+ const atlasPath = (0, node_path_1.resolve)(repoRoot, config.repo.sidecarOutputPath ?? ".polaris/map");
324
+ const routes = {
325
+ ...(0, atlas_js_1.readFileRoutes)(atlasPath),
326
+ ...(0, atlas_js_1.readNeedsReview)(atlasPath),
327
+ };
328
+ const results = [];
329
+ for (const source of files) {
330
+ const absSource = (0, node_path_1.resolve)(repoRoot, source);
331
+ const relCheck = (0, node_path_1.relative)(repoRoot, absSource);
332
+ if (relCheck.startsWith("..") || relCheck.startsWith("/")) {
333
+ throw new Error(`polaris docs ingest: path traversal detected, file outside repo: ${source}`);
334
+ }
335
+ const relSource = (0, node_path_1.relative)(repoRoot, absSource).replace(/\\/g, "/");
336
+ if ((0, node_path_1.basename)(relSource).toLowerCase() === "summary.md") {
337
+ throw new Error(`polaris docs ingest: SUMMARY.md is an endpoint artifact and cannot be ingested`);
338
+ }
339
+ const eligibility = (0, smartdoc_ignore_js_1.isIngestIneligible)(relSource, repoRoot);
340
+ if (eligibility.ineligible) {
341
+ emitTelemetry(telPath, runId, {
342
+ event: "docs-ingest-skipped-endpoint-artifact",
343
+ file: relSource,
344
+ reason: eligibility.reason,
345
+ cluster_id: clusterId,
346
+ });
347
+ throw new Error(`polaris docs ingest: ${relSource} is ineligible for docs ingest - ${eligibility.reason}`);
348
+ }
349
+ if (!(0, node_fs_1.existsSync)(absSource))
350
+ throw new Error(`polaris docs ingest: file not found: ${source}`);
351
+ const content = (0, node_fs_1.readFileSync)(absSource, "utf-8");
352
+ const classification = classifyDoc(content, relSource);
353
+ if (APPROVAL_REQUIRED.has(classification) && !options.approveAuthority) {
354
+ throw new Error(`polaris docs ingest: ${classification} requires explicit approval; rerun with --approve-authority`);
355
+ }
356
+ // Conflict detection against active doctrine (STOP CONDITION)
357
+ const conflict = detectDoctrineConflict(content, repoRoot);
358
+ if (conflict) {
359
+ emitTelemetry(telPath, runId, {
360
+ event: "docs-ingest-conflict-detected",
361
+ file: relSource,
362
+ conflicting_doctrine_file: conflict.conflictingFile,
363
+ detail: conflict.detail,
364
+ cluster_id: clusterId,
365
+ });
366
+ throw new Error(`polaris docs ingest: conflict detected — ${conflict.detail}`);
367
+ }
368
+ const { label: linkedMapArea, entry: linkedEntry } = deriveLinkedArea(content, routes);
369
+ const targetDir = (0, node_path_1.resolve)(repoRoot, TARGET_DIRS[classification]);
370
+ (0, node_fs_1.mkdirSync)(targetDir, { recursive: true });
371
+ const rawDestination = (0, node_path_1.join)(targetDir, (0, node_path_1.basename)(absSource));
372
+ const destination = (0, node_path_1.resolve)(rawDestination) === (0, node_path_1.resolve)(absSource)
373
+ ? absSource
374
+ : uniqueDestination(rawDestination);
375
+ const relDestination = (0, node_path_1.relative)(repoRoot, destination).replace(/\\/g, "/");
376
+ const provenancePath = /\.md$/i.test(destination)
377
+ ? destination.replace(/\.md$/i, ".provenance.json")
378
+ : `${destination}.provenance.json`;
379
+ emitTelemetry(telPath, runId, {
380
+ event: "docs-ingest-classified",
381
+ file: relSource,
382
+ classification,
383
+ destination: relDestination,
384
+ linked_map_area: linkedMapArea,
385
+ cluster_id: clusterId,
386
+ });
387
+ if (!options.dryRun) {
388
+ if ((0, node_path_1.resolve)(absSource) !== (0, node_path_1.resolve)(destination)) {
389
+ (0, node_fs_1.renameSync)(absSource, destination);
390
+ }
391
+ const stampedContent = (0, doctrine_js_1.stampIngestFrontMatter)((0, node_fs_1.readFileSync)(destination, "utf-8"), {
392
+ originalPath: relSource,
393
+ classifiedAs: classification,
394
+ ingestRunId: runId,
395
+ ingestClusterId: clusterId,
396
+ linkedMapArea,
397
+ ingestedAt: new Date().toISOString(),
398
+ });
399
+ (0, node_fs_1.writeFileSync)(destination, stampedContent, "utf-8");
400
+ updateMapEntry(repoRoot, destination, linkedEntry);
401
+ }
402
+ if (classification === "doctrine-candidate" && !options.dryRun) {
403
+ emitTelemetry(telPath, runId, {
404
+ event: "doc-auto-promoted",
405
+ file: relDestination,
406
+ classification,
407
+ linked_map_area: linkedMapArea,
408
+ cluster_id: clusterId,
409
+ });
410
+ }
411
+ emitTelemetry(telPath, runId, {
412
+ event: "docs-ingest",
413
+ file: relSource,
414
+ classification,
415
+ destination: relDestination,
416
+ linked_map_area: linkedMapArea,
417
+ cluster_id: clusterId,
418
+ });
419
+ // ── SUMMARY.md delta check ────────────────────────────────────────────
420
+ // Associate the ingested doc with its nearest route SUMMARY.md and
421
+ // determine whether a SUMMARY.md update is warranted. Informational only —
422
+ // never promotes SUMMARY.md into canon or doctrine.
423
+ const summaryDelta = (0, summary_delta_js_1.applySummaryDelta)({
424
+ repoRoot,
425
+ touchedFiles: [relDestination],
426
+ skipRoot: true,
427
+ });
428
+ const nearestSummary = (0, summary_delta_js_1.findNearestSummarymd)(relDestination, repoRoot, true);
429
+ if (summaryDelta.updateWarranted) {
430
+ emitTelemetry(telPath, runId, {
431
+ event: "summary-delta-warranted",
432
+ file: relDestination,
433
+ reasons: summaryDelta.reasons,
434
+ precedence_source: summaryDelta.precedenceSource,
435
+ summary_target: nearestSummary,
436
+ cluster_id: clusterId,
437
+ });
438
+ }
439
+ // ── Promoted-doc route detection ──────────────────────────────────────
440
+ // When a doc lands in an active doctrine or spec path, it becomes the
441
+ // preferred cognition source for nearby routes. Emit a lightweight
442
+ // cognition-maintenance-needed event so operators/workers can act.
443
+ // Does NOT write any file; informational only.
444
+ const precedence = (0, summary_delta_js_1.detectPrecedenceLevel)([relDestination]);
445
+ if (precedence === "promoted-doctrine" || precedence === "spec-or-arch") {
446
+ if (nearestSummary) {
447
+ emitTelemetry(telPath, runId, {
448
+ event: "cognition-maintenance-needed",
449
+ trigger: "doc-promotion",
450
+ promoted_file: relDestination,
451
+ precedence_source: precedence,
452
+ affected_summary: nearestSummary,
453
+ cluster_id: clusterId,
454
+ });
455
+ }
456
+ }
457
+ results.push({
458
+ sourcePath: relSource,
459
+ destinationPath: relDestination,
460
+ classification,
461
+ linkedMapArea,
462
+ runId,
463
+ dryRun: Boolean(options.dryRun),
464
+ nearestSummary,
465
+ summaryDeltaWarranted: summaryDelta.updateWarranted,
466
+ });
467
+ }
468
+ emitTelemetry(telPath, runId, {
469
+ event: "docs-ingest-complete",
470
+ count: results.length,
471
+ cluster_id: clusterId,
472
+ });
473
+ if (!options.dryRun) {
474
+ writeDocsIngestState(repoRoot, {
475
+ run_id: runId,
476
+ prior_run_id: priorState.run_id ?? null,
477
+ cluster_id: clusterId ?? undefined,
478
+ status: "complete",
479
+ files_ingested: results.length,
480
+ last_run_at: new Date().toISOString(),
481
+ });
482
+ }
483
+ return results;
484
+ }
485
+ function printIngestResults(results) {
486
+ for (const result of results) {
487
+ const prefix = result.dryRun ? "[dry-run] " : "";
488
+ console.log(`${prefix}${result.sourcePath} -> ${result.destinationPath}`);
489
+ console.log(`classification: ${result.classification}`);
490
+ console.log(`linked_map_area: ${result.linkedMapArea ?? "none"}`);
491
+ console.log(`run_id: ${result.runId}`);
492
+ if (result.nearestSummary)
493
+ console.log(`nearest_summary: ${result.nearestSummary}${result.summaryDeltaWarranted ? " (delta warranted)" : ""}`);
494
+ }
495
+ }
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_fs_1 = require("node:fs");
4
+ const node_path_1 = require("node:path");
5
+ const node_fs_2 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const vitest_1 = require("vitest");
8
+ const ingest_js_1 = require("./ingest.js");
9
+ function makeRepo() {
10
+ const repoRoot = (0, node_fs_2.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "polaris-docs-ingest-"));
11
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw"), { recursive: true });
12
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, ingest_js_1.CANONICAL_TARGET, "doctrine", "active"), { recursive: true });
13
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, ".polaris", "map"), { recursive: true });
14
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "polaris.config.json"), JSON.stringify({ repo: { sidecarOutputPath: ".polaris/map" } }), "utf-8");
15
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ".polaris", "map", "file-routes.json"), JSON.stringify({
16
+ "src/smartdocs-engine/index.ts": {
17
+ domain: "docs",
18
+ route: "src/smartdocs-engine",
19
+ taskchain: "polaris-docs",
20
+ confidence: 0.95,
21
+ classification: "indexed",
22
+ last_updated: "",
23
+ updated_by: "",
24
+ tags: ["docs"],
25
+ instructionFile: "src/smartdocs-engine/POLARIS.md",
26
+ },
27
+ }), "utf-8");
28
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ".polaris", "map", "needs-review.json"), "{}\n", "utf-8");
29
+ return repoRoot;
30
+ }
31
+ function makeRepoWithoutCanonical() {
32
+ const repoRoot = (0, node_fs_2.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "polaris-docs-ingest-"));
33
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, ".polaris", "map"), { recursive: true });
34
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "polaris.config.json"), JSON.stringify({ repo: { sidecarOutputPath: ".polaris/map" } }), "utf-8");
35
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ".polaris", "map", "file-routes.json"), "{}\n", "utf-8");
36
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ".polaris", "map", "needs-review.json"), "{}\n", "utf-8");
37
+ return repoRoot;
38
+ }
39
+ (0, vitest_1.describe)("classifyDoc", () => {
40
+ (0, vitest_1.it)("classifies docs from explicit content signals", () => {
41
+ (0, vitest_1.expect)((0, ingest_js_1.classifyDoc)("# Runtime Summary\n\nSession summary")).toBe("runtime-summary");
42
+ (0, vitest_1.expect)((0, ingest_js_1.classifyDoc)("# Audit Finding\n\nSecurity audit result")).toBe("audit-finding");
43
+ (0, vitest_1.expect)((0, ingest_js_1.classifyDoc)("# Doctrine\n\nAgents must always preserve state")).toBe("doctrine-candidate");
44
+ (0, vitest_1.expect)((0, ingest_js_1.classifyDoc)("# Feature Spec\n\nAcceptance Criteria")).toBe("spec-raw");
45
+ });
46
+ (0, vitest_1.it)("classifies SUMMARY.md as deprecated-noise regardless of content", () => {
47
+ (0, vitest_1.expect)((0, ingest_js_1.classifyDoc)("# Authoritative Doctrine\nAgents must always X.", "SUMMARY.md")).toBe("deprecated-noise");
48
+ });
49
+ });
50
+ (0, vitest_1.describe)("ingestDocs", () => {
51
+ (0, vitest_1.it)("rejects SUMMARY.md ingest with a hard guard", () => {
52
+ const repoRoot = makeRepo();
53
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "SUMMARY.md"), "# Summary\n", "utf-8");
54
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)(["SUMMARY.md"], { repoRoot })).toThrow("polaris docs ingest: SUMMARY.md is an endpoint artifact and cannot be ingested");
55
+ });
56
+ (0, vitest_1.it)("moves a raw spec, writes provenance, links map area, and emits telemetry", () => {
57
+ const repoRoot = makeRepo();
58
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", "ingest-plan.md"), "# Ingest Spec\n\nAcceptance Criteria\n\nTouches src/smartdocs-engine/index.ts.", "utf-8");
59
+ const [result] = (0, ingest_js_1.ingestDocs)(["smartdocs/docs/raw/ingest-plan.md"], { repoRoot });
60
+ (0, vitest_1.expect)(result.classification).toBe("spec-raw");
61
+ (0, vitest_1.expect)(result.destinationPath).toBe(`${ingest_js_1.CANONICAL_TARGET}/raw/ingest-plan.md`);
62
+ (0, vitest_1.expect)(result.linkedMapArea).toBe("src/smartdocs-engine");
63
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)((0, node_path_1.join)(repoRoot, ingest_js_1.CANONICAL_TARGET, "raw", "ingest-plan.md"))).toBe(true);
64
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)((0, node_path_1.join)(repoRoot, ingest_js_1.CANONICAL_TARGET, "raw", "ingest-plan.provenance.json"))).toBe(true);
65
+ // Telemetry written to polaris-docs-ingest path using the generated run_id
66
+ const runsDir = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-docs-ingest", "runs");
67
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)(runsDir)).toBe(true);
68
+ const runDirs = (0, node_fs_1.readdirSync)(runsDir);
69
+ (0, vitest_1.expect)(runDirs).toHaveLength(1);
70
+ const telemetry = (0, node_fs_1.readFileSync)((0, node_path_1.join)(runsDir, runDirs[0], "telemetry.jsonl"), "utf-8");
71
+ (0, vitest_1.expect)(telemetry).toContain('"event":"run-start"');
72
+ (0, vitest_1.expect)(telemetry).toContain('"event":"docs-ingest"');
73
+ // run_id in result matches the telemetry dir
74
+ (0, vitest_1.expect)(result.runId).toBe(runDirs[0]);
75
+ });
76
+ (0, vitest_1.it)("rejects batches above the bounded file limit", () => {
77
+ const repoRoot = makeRepo();
78
+ for (const name of ["a.md", "b.md", "c.md", "d.md", "e.md"]) {
79
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", name), "# Spec\n\nAcceptance Criteria", "utf-8");
80
+ }
81
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)([
82
+ "smartdocs/docs/raw/a.md",
83
+ "smartdocs/docs/raw/b.md",
84
+ "smartdocs/docs/raw/c.md",
85
+ "smartdocs/docs/raw/d.md",
86
+ "smartdocs/docs/raw/e.md",
87
+ ], { repoRoot })).toThrow("batch limit is 4 files");
88
+ });
89
+ (0, vitest_1.it)("does not promote high-authority architecture docs without approval", () => {
90
+ const repoRoot = makeRepo();
91
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", "architecture.md"), "# Architecture\n\nStructural design", "utf-8");
92
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)(["smartdocs/docs/raw/architecture.md"], { repoRoot })).toThrow("requires explicit approval");
93
+ });
94
+ (0, vitest_1.it)("halts when smartdocs/docs/ canonical target is missing", () => {
95
+ const repoRoot = makeRepoWithoutCanonical();
96
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "test.md"), "# Spec\n\nAcceptance Criteria", "utf-8");
97
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)(["test.md"], { repoRoot })).toThrow("canonical target");
98
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)(["test.md"], { repoRoot })).toThrow("smartdocs/docs");
99
+ });
100
+ (0, vitest_1.it)("dry-run classifies and reports placement without moving files", () => {
101
+ const repoRoot = makeRepo();
102
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", "spec-dry.md"), "# Feature Spec\n\nAcceptance Criteria", "utf-8");
103
+ const [result] = (0, ingest_js_1.ingestDocs)(["smartdocs/docs/raw/spec-dry.md"], { repoRoot, dryRun: true });
104
+ (0, vitest_1.expect)(result.dryRun).toBe(true);
105
+ (0, vitest_1.expect)(result.classification).toBe("spec-raw");
106
+ (0, vitest_1.expect)(result.destinationPath).toBe(`${ingest_js_1.CANONICAL_TARGET}/raw/spec-dry.md`);
107
+ (0, vitest_1.expect)(result.provenancePath).toBeNull();
108
+ // Source file still exists (not moved)
109
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", "spec-dry.md"))).toBe(true);
110
+ // Target file does not exist
111
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)((0, node_path_1.join)(repoRoot, ingest_js_1.CANONICAL_TARGET, "specs", "raw", "spec-dry.md"))).toBe(false);
112
+ // run-start telemetry still emitted even for dry-run
113
+ const runsDir = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-docs-ingest", "runs");
114
+ const runDirs = (0, node_fs_1.readdirSync)(runsDir);
115
+ (0, vitest_1.expect)(runDirs).toHaveLength(1);
116
+ const telemetry = (0, node_fs_1.readFileSync)((0, node_path_1.join)(runsDir, runDirs[0], "telemetry.jsonl"), "utf-8");
117
+ (0, vitest_1.expect)(telemetry).toContain('"event":"run-start"');
118
+ });
119
+ (0, vitest_1.it)("halts and emits conflict telemetry when ingested doc contradicts active doctrine", () => {
120
+ const repoRoot = makeRepo();
121
+ // "must preserve" → docRequires captures "preserve"
122
+ // "never preserve" → ingestedProhibits captures "preserve" → conflict
123
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ingest_js_1.CANONICAL_TARGET, "doctrine", "active", "state-integrity.md"), "# State Integrity Doctrine\n\nAgents must preserve telemetry files.\n", "utf-8");
124
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", "conflicting-doc.md"), "# Conflicting Policy\n\nAgents never preserve telemetry files.", "utf-8");
125
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)(["smartdocs/docs/raw/conflicting-doc.md"], { repoRoot })).toThrow("conflict detected");
126
+ // Conflict telemetry should be present
127
+ const runsDir = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-docs-ingest", "runs");
128
+ const runDirs = (0, node_fs_1.readdirSync)(runsDir);
129
+ (0, vitest_1.expect)(runDirs).toHaveLength(1);
130
+ const telemetry = (0, node_fs_1.readFileSync)((0, node_path_1.join)(runsDir, runDirs[0], "telemetry.jsonl"), "utf-8");
131
+ (0, vitest_1.expect)(telemetry).toContain('"event":"docs-ingest-conflict-detected"');
132
+ });
133
+ (0, vitest_1.it)("writes durable state to polaris-docs-ingest after a successful ingest", () => {
134
+ const repoRoot = makeRepo();
135
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "smartdocs", "docs", "raw", "state-test.md"), "# Feature Spec\n\nAcceptance Criteria", "utf-8");
136
+ const [result] = (0, ingest_js_1.ingestDocs)(["smartdocs/docs/raw/state-test.md"], { repoRoot });
137
+ const stateFile = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-docs-ingest", "current-state.json");
138
+ (0, vitest_1.expect)((0, node_fs_1.existsSync)(stateFile)).toBe(true);
139
+ const state = JSON.parse((0, node_fs_1.readFileSync)(stateFile, "utf-8"));
140
+ (0, vitest_1.expect)(state.run_id).toBe(result.runId);
141
+ (0, vitest_1.expect)(state.status).toBe("complete");
142
+ (0, vitest_1.expect)(state.files_ingested).toBe(1);
143
+ });
144
+ (0, vitest_1.it)("rejects ignored endpoint artifacts before ingesting and emits telemetry", () => {
145
+ const repoRoot = makeRepo();
146
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run"), { recursive: true });
147
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json"), JSON.stringify({ run_id: "test-run" }), "utf-8");
148
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)([".taskchain_artifacts/polaris-run/current-state.json"], { repoRoot })).toThrow("ineligible for docs ingest");
149
+ const runsDir = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-docs-ingest", "runs");
150
+ const runDirs = (0, node_fs_1.readdirSync)(runsDir);
151
+ (0, vitest_1.expect)(runDirs).toHaveLength(1);
152
+ const telemetry = (0, node_fs_1.readFileSync)((0, node_path_1.join)(runsDir, runDirs[0], "telemetry.jsonl"), "utf-8");
153
+ (0, vitest_1.expect)(telemetry).toContain('"event":"docs-ingest-skipped-endpoint-artifact"');
154
+ (0, vitest_1.expect)(telemetry).toContain('"file":".taskchain_artifacts/polaris-run/current-state.json"');
155
+ });
156
+ (0, vitest_1.it)("rejects paths ignored by a custom .smartdocignore pattern", () => {
157
+ const repoRoot = makeRepo();
158
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, "scratch"), { recursive: true });
159
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, ".smartdocignore"), "scratch/*.md\n", "utf-8");
160
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(repoRoot, "scratch", "draft.md"), "# Spec\n\nAcceptance Criteria", "utf-8");
161
+ (0, vitest_1.expect)(() => (0, ingest_js_1.ingestDocs)(["scratch/draft.md"], { repoRoot })).toThrow("ignored by .smartdocignore/defaults: scratch/draft.md");
162
+ });
163
+ });