@harness-engineering/cli 1.25.7 → 1.26.1

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 (189) hide show
  1. package/dist/agents/skills/README.md +1 -0
  2. package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +33 -5
  3. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +6 -0
  4. package/dist/agents/skills/claude-code/harness-knowledge-pipeline/SKILL.md +277 -0
  5. package/dist/agents/skills/claude-code/harness-knowledge-pipeline/skill.yaml +60 -0
  6. package/dist/agents/skills/claude-code/harness-planning/SKILL.md +25 -1
  7. package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +22 -6
  8. package/dist/agents/skills/claude-code/initialize-harness-project/skill.yaml +2 -1
  9. package/dist/agents/skills/claude-code/initialize-test-suite-project/SKILL.md +415 -0
  10. package/dist/agents/skills/claude-code/initialize-test-suite-project/skill.yaml +35 -0
  11. package/dist/agents/skills/codex/harness-brainstorming/SKILL.md +33 -5
  12. package/dist/agents/skills/codex/harness-execution/SKILL.md +6 -0
  13. package/dist/agents/skills/codex/harness-knowledge-pipeline/SKILL.md +277 -0
  14. package/dist/agents/skills/codex/harness-knowledge-pipeline/skill.yaml +60 -0
  15. package/dist/agents/skills/codex/harness-planning/SKILL.md +25 -1
  16. package/dist/agents/skills/codex/initialize-harness-project/SKILL.md +22 -6
  17. package/dist/agents/skills/codex/initialize-harness-project/skill.yaml +2 -1
  18. package/dist/agents/skills/codex/initialize-test-suite-project/SKILL.md +415 -0
  19. package/dist/agents/skills/codex/initialize-test-suite-project/skill.yaml +35 -0
  20. package/dist/agents/skills/cursor/harness-brainstorming/SKILL.md +33 -5
  21. package/dist/agents/skills/cursor/harness-execution/SKILL.md +6 -0
  22. package/dist/agents/skills/cursor/harness-knowledge-pipeline/SKILL.md +277 -0
  23. package/dist/agents/skills/cursor/harness-knowledge-pipeline/skill.yaml +60 -0
  24. package/dist/agents/skills/cursor/harness-planning/SKILL.md +25 -1
  25. package/dist/agents/skills/cursor/initialize-harness-project/SKILL.md +22 -6
  26. package/dist/agents/skills/cursor/initialize-harness-project/skill.yaml +2 -1
  27. package/dist/agents/skills/cursor/initialize-test-suite-project/SKILL.md +415 -0
  28. package/dist/agents/skills/cursor/initialize-test-suite-project/skill.yaml +35 -0
  29. package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +33 -5
  30. package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +6 -0
  31. package/dist/agents/skills/gemini-cli/harness-knowledge-pipeline/SKILL.md +277 -0
  32. package/dist/agents/skills/gemini-cli/harness-knowledge-pipeline/skill.yaml +60 -0
  33. package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +25 -1
  34. package/dist/agents/skills/gemini-cli/initialize-harness-project/SKILL.md +22 -6
  35. package/dist/agents/skills/gemini-cli/initialize-harness-project/skill.yaml +2 -1
  36. package/dist/agents/skills/gemini-cli/initialize-test-suite-project/SKILL.md +415 -0
  37. package/dist/agents/skills/gemini-cli/initialize-test-suite-project/skill.yaml +35 -0
  38. package/dist/agents/skills/package.json +2 -2
  39. package/dist/agents/skills/tests/initialize-test-suite-project.test.ts +152 -0
  40. package/dist/{agents-md-QPO3H5HX.js → agents-md-VI3HLR7E.js} +3 -3
  41. package/dist/{architecture-IJLXRHZA.js → architecture-ZDDKEVID.js} +4 -4
  42. package/dist/{assess-project-QU6XIVJK.js → assess-project-Y6W7YDUA.js} +1 -1
  43. package/dist/bin/harness-mcp.js +16 -15
  44. package/dist/bin/harness.js +27 -27
  45. package/dist/{check-phase-gate-TNY64AOS.js → check-phase-gate-CXR3VM22.js} +4 -4
  46. package/dist/{chunk-JJQ3TW2E.js → chunk-2BPPS6YQ.js} +804 -93
  47. package/dist/{chunk-NKQEMWGT.js → chunk-2FPUPRCY.js} +3419 -126
  48. package/dist/{chunk-MLOGYWLY.js → chunk-3AMQYWGT.js} +2 -2
  49. package/dist/{chunk-AS5DQT4S.js → chunk-56A6OXVA.js} +9 -9
  50. package/dist/{chunk-G5SNXUIK.js → chunk-7GY6WNEI.js} +1 -1
  51. package/dist/{chunk-SZ3Q4XEO.js → chunk-AMRC4FU2.js} +441 -288
  52. package/dist/{chunk-K533WNZG.js → chunk-B66LRB5M.js} +1 -1
  53. package/dist/{chunk-YRBNAHH4.js → chunk-BK4DJIIY.js} +4 -4
  54. package/dist/{chunk-2DF6OZLG.js → chunk-DBIX3X4H.js} +2 -2
  55. package/dist/{chunk-LXPRO2FH.js → chunk-FUND5WJR.js} +2 -2
  56. package/dist/{chunk-FRDS4XGD.js → chunk-JV4Y2U5F.js} +1 -1
  57. package/dist/{chunk-XVJRFP7W.js → chunk-K56DTX7G.js} +6 -6
  58. package/dist/{chunk-K35D7HF3.js → chunk-N6B6RKQQ.js} +1 -1
  59. package/dist/{chunk-LTCEV7VF.js → chunk-NY7DTZ6K.js} +3 -3
  60. package/dist/{chunk-JPZY7ZQX.js → chunk-OLW7BJ5N.js} +1 -1
  61. package/dist/{chunk-KVJZQ2MV.js → chunk-ORNEK65Y.js} +1 -1
  62. package/dist/{chunk-D3XL2FLX.js → chunk-PNNFCVW4.js} +5 -5
  63. package/dist/{chunk-E5Z6NTTQ.js → chunk-Q3ZLVMQM.js} +1 -1
  64. package/dist/{chunk-PCOU2UXU.js → chunk-RAVD7QLY.js} +8 -8
  65. package/dist/{chunk-IQO7IINR.js → chunk-TSVYBDDE.js} +1 -1
  66. package/dist/{chunk-MCDCZMKN.js → chunk-TY2ESLVL.js} +1 -1
  67. package/dist/{chunk-TQRCODRJ.js → chunk-VKS3F46M.js} +1 -1
  68. package/dist/{chunk-GVNT2YC7.js → chunk-WZ7UHPT3.js} +9 -9
  69. package/dist/{chunk-ZIYLIYN4.js → chunk-YC4EYIER.js} +8 -2
  70. package/dist/{chunk-IT2KORSK.js → chunk-ZN6ROTYP.js} +1 -1
  71. package/dist/{ci-workflow-FJYRFZYQ.js → ci-workflow-EW37KVPS.js} +3 -3
  72. package/dist/{create-skill-6QWJHQYS.js → create-skill-4T5CBSJ2.js} +2 -2
  73. package/dist/{dist-ZV6GO6FA.js → dist-CJDGASPP.js} +2 -2
  74. package/dist/{dist-HPWNWCT6.js → dist-NCNAXUFD.js} +41 -1
  75. package/dist/{docs-7DW3DI4Z.js → docs-4XI2FQW2.js} +4 -4
  76. package/dist/{engine-P7EE2IWO.js → engine-4L4CDTHU.js} +3 -3
  77. package/dist/{entropy-HCXRTDFU.js → entropy-PBYSIQYS.js} +3 -3
  78. package/dist/{feedback-Y4FYN3NR.js → feedback-OTIFW5MK.js} +1 -1
  79. package/dist/{generate-agent-definitions-3EHPIWBO.js → generate-agent-definitions-CZGUXIIN.js} +4 -4
  80. package/dist/{graph-loader-KXVS3YK3.js → graph-loader-RDXSEBD7.js} +1 -1
  81. package/dist/index.d.ts +8 -8
  82. package/dist/index.js +27 -27
  83. package/dist/{loader-SKHF3JVV.js → loader-UZOXCWFC.js} +3 -3
  84. package/dist/{mcp-MWYIPDQU.js → mcp-GWVVV6LH.js} +16 -15
  85. package/dist/{performance-ADWH5NJY.js → performance-5UQPP3XX.js} +4 -4
  86. package/dist/{review-pipeline-TLKE5OQD.js → review-pipeline-65V4EO6O.js} +3 -3
  87. package/dist/{runtime-7LYAW7GU.js → runtime-PXX7HNB3.js} +3 -3
  88. package/dist/{scan-BGJ7TLLV.js → scan-WM7XDUF7.js} +1 -1
  89. package/dist/{security-5XWINYVB.js → security-CAYDRZIZ.js} +1 -1
  90. package/dist/{validate-BFFHYHY7.js → validate-CWGXR7QM.js} +4 -4
  91. package/dist/{validate-cross-check-GQX7OVCR.js → validate-cross-check-BKCNLZYG.js} +3 -3
  92. package/package.json +10 -10
  93. package/dist/agents/commands/claude-code/harness/harness.md +0 -36
  94. package/dist/agents/commands/codex/AGENTS.md +0 -39
  95. package/dist/agents/commands/codex/harness/add-harness-component/SKILL.md +0 -204
  96. package/dist/agents/commands/codex/harness/add-harness-component/agents/openai.yaml +0 -3
  97. package/dist/agents/commands/codex/harness/cleanup-dead-code/SKILL.md +0 -257
  98. package/dist/agents/commands/codex/harness/cleanup-dead-code/agents/openai.yaml +0 -3
  99. package/dist/agents/commands/codex/harness/detect-doc-drift/SKILL.md +0 -191
  100. package/dist/agents/commands/codex/harness/detect-doc-drift/agents/openai.yaml +0 -3
  101. package/dist/agents/commands/codex/harness/enforce-architecture/SKILL.md +0 -289
  102. package/dist/agents/commands/codex/harness/enforce-architecture/agents/openai.yaml +0 -3
  103. package/dist/agents/commands/codex/harness/harness-architecture-advisor/SKILL.md +0 -442
  104. package/dist/agents/commands/codex/harness/harness-architecture-advisor/agents/openai.yaml +0 -3
  105. package/dist/agents/commands/codex/harness/harness-autopilot/SKILL.md +0 -929
  106. package/dist/agents/commands/codex/harness/harness-autopilot/agents/openai.yaml +0 -3
  107. package/dist/agents/commands/codex/harness/harness-brainstorming/SKILL.md +0 -418
  108. package/dist/agents/commands/codex/harness/harness-brainstorming/agents/openai.yaml +0 -3
  109. package/dist/agents/commands/codex/harness/harness-code-review/SKILL.md +0 -850
  110. package/dist/agents/commands/codex/harness/harness-code-review/agents/openai.yaml +0 -3
  111. package/dist/agents/commands/codex/harness/harness-codebase-cleanup/SKILL.md +0 -236
  112. package/dist/agents/commands/codex/harness/harness-codebase-cleanup/agents/openai.yaml +0 -3
  113. package/dist/agents/commands/codex/harness/harness-debugging/SKILL.md +0 -378
  114. package/dist/agents/commands/codex/harness/harness-debugging/agents/openai.yaml +0 -3
  115. package/dist/agents/commands/codex/harness/harness-dependency-health/SKILL.md +0 -190
  116. package/dist/agents/commands/codex/harness/harness-dependency-health/agents/openai.yaml +0 -3
  117. package/dist/agents/commands/codex/harness/harness-docs-pipeline/SKILL.md +0 -472
  118. package/dist/agents/commands/codex/harness/harness-docs-pipeline/agents/openai.yaml +0 -3
  119. package/dist/agents/commands/codex/harness/harness-execution/SKILL.md +0 -522
  120. package/dist/agents/commands/codex/harness/harness-execution/agents/openai.yaml +0 -3
  121. package/dist/agents/commands/codex/harness/harness-hotspot-detector/SKILL.md +0 -172
  122. package/dist/agents/commands/codex/harness/harness-hotspot-detector/agents/openai.yaml +0 -3
  123. package/dist/agents/commands/codex/harness/harness-impact-analysis/SKILL.md +0 -195
  124. package/dist/agents/commands/codex/harness/harness-impact-analysis/agents/openai.yaml +0 -3
  125. package/dist/agents/commands/codex/harness/harness-integrity/SKILL.md +0 -178
  126. package/dist/agents/commands/codex/harness/harness-integrity/agents/openai.yaml +0 -3
  127. package/dist/agents/commands/codex/harness/harness-onboarding/SKILL.md +0 -299
  128. package/dist/agents/commands/codex/harness/harness-onboarding/agents/openai.yaml +0 -3
  129. package/dist/agents/commands/codex/harness/harness-perf/SKILL.md +0 -272
  130. package/dist/agents/commands/codex/harness/harness-perf/agents/openai.yaml +0 -3
  131. package/dist/agents/commands/codex/harness/harness-planning/SKILL.md +0 -592
  132. package/dist/agents/commands/codex/harness/harness-planning/agents/openai.yaml +0 -3
  133. package/dist/agents/commands/codex/harness/harness-refactoring/SKILL.md +0 -181
  134. package/dist/agents/commands/codex/harness/harness-refactoring/agents/openai.yaml +0 -3
  135. package/dist/agents/commands/codex/harness/harness-release-readiness/SKILL.md +0 -701
  136. package/dist/agents/commands/codex/harness/harness-release-readiness/agents/openai.yaml +0 -3
  137. package/dist/agents/commands/codex/harness/harness-roadmap/SKILL.md +0 -607
  138. package/dist/agents/commands/codex/harness/harness-roadmap/agents/openai.yaml +0 -3
  139. package/dist/agents/commands/codex/harness/harness-security-scan/SKILL.md +0 -147
  140. package/dist/agents/commands/codex/harness/harness-security-scan/agents/openai.yaml +0 -3
  141. package/dist/agents/commands/codex/harness/harness-skill-authoring/SKILL.md +0 -314
  142. package/dist/agents/commands/codex/harness/harness-skill-authoring/agents/openai.yaml +0 -3
  143. package/dist/agents/commands/codex/harness/harness-soundness-review/SKILL.md +0 -1280
  144. package/dist/agents/commands/codex/harness/harness-soundness-review/agents/openai.yaml +0 -3
  145. package/dist/agents/commands/codex/harness/harness-supply-chain-audit/SKILL.md +0 -255
  146. package/dist/agents/commands/codex/harness/harness-supply-chain-audit/agents/openai.yaml +0 -3
  147. package/dist/agents/commands/codex/harness/harness-tdd/SKILL.md +0 -189
  148. package/dist/agents/commands/codex/harness/harness-tdd/agents/openai.yaml +0 -3
  149. package/dist/agents/commands/codex/harness/harness-test-advisor/SKILL.md +0 -171
  150. package/dist/agents/commands/codex/harness/harness-test-advisor/agents/openai.yaml +0 -3
  151. package/dist/agents/commands/codex/harness/harness-verification/SKILL.md +0 -433
  152. package/dist/agents/commands/codex/harness/harness-verification/agents/openai.yaml +0 -3
  153. package/dist/agents/commands/codex/harness/harness-verify/SKILL.md +0 -170
  154. package/dist/agents/commands/codex/harness/harness-verify/agents/openai.yaml +0 -3
  155. package/dist/agents/commands/codex/harness/initialize-harness-project/SKILL.md +0 -244
  156. package/dist/agents/commands/codex/harness/initialize-harness-project/agents/openai.yaml +0 -3
  157. package/dist/agents/commands/cursor/harness/add-harness-component.mdc +0 -200
  158. package/dist/agents/commands/cursor/harness/cleanup-dead-code.mdc +0 -253
  159. package/dist/agents/commands/cursor/harness/detect-doc-drift.mdc +0 -187
  160. package/dist/agents/commands/cursor/harness/enforce-architecture.mdc +0 -304
  161. package/dist/agents/commands/cursor/harness/harness-architecture-advisor.mdc +0 -457
  162. package/dist/agents/commands/cursor/harness/harness-autopilot.mdc +0 -924
  163. package/dist/agents/commands/cursor/harness/harness-brainstorming.mdc +0 -414
  164. package/dist/agents/commands/cursor/harness/harness-code-review.mdc +0 -865
  165. package/dist/agents/commands/cursor/harness/harness-codebase-cleanup.mdc +0 -232
  166. package/dist/agents/commands/cursor/harness/harness-debugging.mdc +0 -374
  167. package/dist/agents/commands/cursor/harness/harness-dependency-health.mdc +0 -187
  168. package/dist/agents/commands/cursor/harness/harness-docs-pipeline.mdc +0 -468
  169. package/dist/agents/commands/cursor/harness/harness-execution.mdc +0 -518
  170. package/dist/agents/commands/cursor/harness/harness-hotspot-detector.mdc +0 -169
  171. package/dist/agents/commands/cursor/harness/harness-impact-analysis.mdc +0 -192
  172. package/dist/agents/commands/cursor/harness/harness-integrity.mdc +0 -175
  173. package/dist/agents/commands/cursor/harness/harness-onboarding.mdc +0 -296
  174. package/dist/agents/commands/cursor/harness/harness-perf.mdc +0 -268
  175. package/dist/agents/commands/cursor/harness/harness-planning.mdc +0 -587
  176. package/dist/agents/commands/cursor/harness/harness-refactoring.mdc +0 -177
  177. package/dist/agents/commands/cursor/harness/harness-release-readiness.mdc +0 -697
  178. package/dist/agents/commands/cursor/harness/harness-roadmap.mdc +0 -603
  179. package/dist/agents/commands/cursor/harness/harness-security-scan.mdc +0 -162
  180. package/dist/agents/commands/cursor/harness/harness-skill-authoring.mdc +0 -300
  181. package/dist/agents/commands/cursor/harness/harness-soundness-review.mdc +0 -1275
  182. package/dist/agents/commands/cursor/harness/harness-supply-chain-audit.mdc +0 -252
  183. package/dist/agents/commands/cursor/harness/harness-tdd.mdc +0 -185
  184. package/dist/agents/commands/cursor/harness/harness-test-advisor.mdc +0 -168
  185. package/dist/agents/commands/cursor/harness/harness-verification.mdc +0 -429
  186. package/dist/agents/commands/cursor/harness/harness-verify.mdc +0 -167
  187. package/dist/agents/commands/cursor/harness/initialize-harness-project.mdc +0 -240
  188. package/dist/agents/commands/gemini-cli/harness/harness.toml +0 -277
  189. package/dist/{chunk-RRHUCDRD.js → chunk-ZP5E7YET.js} +3 -3
@@ -19,12 +19,22 @@ import * as fs3 from "fs/promises";
19
19
  import * as path4 from "path";
20
20
  import * as fs4 from "fs/promises";
21
21
  import * as path5 from "path";
22
+ import * as fs9 from "fs/promises";
23
+ import * as path10 from "path";
22
24
  import * as fs5 from "fs/promises";
23
25
  import * as path6 from "path";
24
- import { minimatch } from "minimatch";
25
- import { relative as relative3 } from "path";
26
26
  import * as fs6 from "fs/promises";
27
27
  import * as path7 from "path";
28
+ import * as fs7 from "fs/promises";
29
+ import * as path8 from "path";
30
+ import * as fs8 from "fs/promises";
31
+ import * as path9 from "path";
32
+ import * as fs10 from "fs/promises";
33
+ import * as path11 from "path";
34
+ import { minimatch } from "minimatch";
35
+ import { relative as relative5 } from "path";
36
+ import * as fs11 from "fs/promises";
37
+ import * as path12 from "path";
28
38
  function streamGraphJson(filePath, nodes, edges) {
29
39
  return new Promise((resolve, reject) => {
30
40
  const stream = createWriteStream(filePath, { encoding: "utf-8" });
@@ -353,6 +363,496 @@ function detectEarsPattern(text) {
353
363
  if (/^the\s+\w+\s+shall\b/.test(lower)) return "ubiquitous";
354
364
  return void 0;
355
365
  }
366
+ function emptyResult2(diagramType = "unknown") {
367
+ return {
368
+ entities: [],
369
+ relationships: [],
370
+ metadata: { format: "mermaid", diagramType }
371
+ };
372
+ }
373
+ function detectDiagramType(content) {
374
+ for (const line of content.split("\n")) {
375
+ const trimmed = line.trim();
376
+ if (!trimmed) continue;
377
+ if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
378
+ if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
379
+ if (/^classDiagram\b/.test(trimmed)) return "class";
380
+ if (/^erDiagram\b/.test(trimmed)) return "er";
381
+ break;
382
+ }
383
+ return "unknown";
384
+ }
385
+ function isDecisionNode(content, nodeId) {
386
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
387
+ return decisionRegex.test(content);
388
+ }
389
+ function buildFlowchartEntity(content, match) {
390
+ const id = match[1] ?? "";
391
+ const label = (match[2] ?? "").trim();
392
+ if (!id) return null;
393
+ const decision = isDecisionNode(content, id);
394
+ const entity = {
395
+ id,
396
+ label,
397
+ ...decision ? { type: "decision" } : {}
398
+ };
399
+ return { id, entity };
400
+ }
401
+ function extractFlowchartNodes(content) {
402
+ const entities = /* @__PURE__ */ new Map();
403
+ const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
404
+ let match = nodeRegex.exec(content);
405
+ while (match !== null) {
406
+ const parsed = buildFlowchartEntity(content, match);
407
+ if (parsed && !entities.has(parsed.id)) {
408
+ entities.set(parsed.id, parsed.entity);
409
+ }
410
+ match = nodeRegex.exec(content);
411
+ }
412
+ return entities;
413
+ }
414
+ function parseLabeledEdgeMatch(match) {
415
+ const from = match[1] ?? "";
416
+ const label = (match[2] ?? "").trim();
417
+ const to = match[3] ?? "";
418
+ if (!from || !to) return null;
419
+ return { key: `${from}->${to}:${label}`, rel: { from, to, label } };
420
+ }
421
+ function extractLabeledEdges(stripped, edgeKeys) {
422
+ const relationships = [];
423
+ const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
424
+ let match = labeledEdgeRegex.exec(stripped);
425
+ while (match !== null) {
426
+ const parsed = parseLabeledEdgeMatch(match);
427
+ if (parsed && !edgeKeys.has(parsed.key)) {
428
+ edgeKeys.add(parsed.key);
429
+ relationships.push(parsed.rel);
430
+ }
431
+ match = labeledEdgeRegex.exec(stripped);
432
+ }
433
+ return relationships;
434
+ }
435
+ function hasLabeledEdgeBetween(labeledEdges, from, to) {
436
+ return labeledEdges.some((r) => r.from === from && r.to === to);
437
+ }
438
+ function extractUnlabeledEdges(stripped, edgeKeys, labeledEdges) {
439
+ const relationships = [];
440
+ const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
441
+ let match = unlabeledEdgeRegex.exec(stripped);
442
+ while (match !== null) {
443
+ const from = match[1] ?? "";
444
+ const to = match[2] ?? "";
445
+ const key = `${from}->${to}`;
446
+ if (from && to && !hasLabeledEdgeBetween(labeledEdges, from, to) && !edgeKeys.has(key)) {
447
+ edgeKeys.add(key);
448
+ relationships.push({ from, to });
449
+ }
450
+ match = unlabeledEdgeRegex.exec(stripped);
451
+ }
452
+ return relationships;
453
+ }
454
+ function parseFlowchart(content, diagramType) {
455
+ const entities = extractFlowchartNodes(content);
456
+ const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
457
+ const edgeKeys = /* @__PURE__ */ new Set();
458
+ const labeledEdges = extractLabeledEdges(stripped, edgeKeys);
459
+ const unlabeledEdges = extractUnlabeledEdges(stripped, edgeKeys, labeledEdges);
460
+ return {
461
+ entities: Array.from(entities.values()),
462
+ relationships: [...labeledEdges, ...unlabeledEdges],
463
+ metadata: { format: "mermaid", diagramType }
464
+ };
465
+ }
466
+ function extractParticipants(content) {
467
+ const entities = [];
468
+ const seenParticipants = /* @__PURE__ */ new Set();
469
+ const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
470
+ let match = participantRegex.exec(content);
471
+ while (match !== null) {
472
+ const id = match[1] ?? "";
473
+ const label = match[2]?.trim() || id;
474
+ if (id && !seenParticipants.has(id)) {
475
+ seenParticipants.add(id);
476
+ entities.push({ id, label });
477
+ }
478
+ match = participantRegex.exec(content);
479
+ }
480
+ return entities;
481
+ }
482
+ function relationshipFromMatch(match) {
483
+ const from = match[1] ?? "";
484
+ const to = match[2] ?? "";
485
+ const label = (match[3] ?? "").trim();
486
+ return from && to ? { from, to, label } : null;
487
+ }
488
+ function collectMessageMatches(content, regex) {
489
+ const results = [];
490
+ let match = regex.exec(content);
491
+ while (match !== null) {
492
+ const rel = relationshipFromMatch(match);
493
+ if (rel) results.push(rel);
494
+ match = regex.exec(content);
495
+ }
496
+ return results;
497
+ }
498
+ function extractMessages(content) {
499
+ const forward = collectMessageMatches(content, /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g);
500
+ const returns = collectMessageMatches(content, /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g);
501
+ return [...forward, ...returns];
502
+ }
503
+ function parseSequence(content, diagramType) {
504
+ return {
505
+ entities: extractParticipants(content),
506
+ relationships: extractMessages(content),
507
+ metadata: { format: "mermaid", diagramType }
508
+ };
509
+ }
510
+ function emptyResult3() {
511
+ return {
512
+ entities: [],
513
+ relationships: [],
514
+ metadata: { format: "d2", diagramType: "architecture" }
515
+ };
516
+ }
517
+ function parseBlockShape(stripped) {
518
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
519
+ if (match) {
520
+ const id = match[1];
521
+ const label = match[2];
522
+ if (id && label) return { id, label };
523
+ }
524
+ return null;
525
+ }
526
+ function parseConnection(stripped) {
527
+ const match = stripped.match(/^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/);
528
+ if (match) {
529
+ const from = match[1] ?? "";
530
+ const to = match[2] ?? "";
531
+ const label = match[3]?.trim();
532
+ if (from && to) return { from, to, ...label ? { label } : {} };
533
+ }
534
+ return null;
535
+ }
536
+ function parseSimpleShape(stripped) {
537
+ const match = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
538
+ if (match) {
539
+ const id = match[1];
540
+ const label = (match[2] ?? "").trim();
541
+ if (id && label) return { id, label };
542
+ }
543
+ return null;
544
+ }
545
+ function handleBlockOpen(stripped, state) {
546
+ if (state.braceDepth === 0) {
547
+ const shape = parseBlockShape(stripped);
548
+ if (shape) state.entities.set(shape.id, { id: shape.id, label: shape.label });
549
+ }
550
+ state.braceDepth++;
551
+ }
552
+ function processTopLevelLine(stripped, state) {
553
+ const conn = parseConnection(stripped);
554
+ if (conn) {
555
+ state.relationships.push(conn);
556
+ return;
557
+ }
558
+ const shape = parseSimpleShape(stripped);
559
+ if (shape && !state.entities.has(shape.id)) {
560
+ state.entities.set(shape.id, { id: shape.id, label: shape.label });
561
+ }
562
+ }
563
+ function processD2Line(stripped, state) {
564
+ if (!stripped || stripped.startsWith("#")) return;
565
+ if (stripped.endsWith("{")) {
566
+ handleBlockOpen(stripped, state);
567
+ return;
568
+ }
569
+ if (stripped === "}") {
570
+ state.braceDepth = Math.max(0, state.braceDepth - 1);
571
+ return;
572
+ }
573
+ if (state.braceDepth > 0) return;
574
+ processTopLevelLine(stripped, state);
575
+ }
576
+ function emptyResult4() {
577
+ return {
578
+ entities: [],
579
+ relationships: [],
580
+ metadata: { format: "plantuml", diagramType: "unknown" }
581
+ };
582
+ }
583
+ function detectDiagramType2(content) {
584
+ if (/class\s+\w+/.test(content)) return "class";
585
+ if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
586
+ if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
587
+ return "unknown";
588
+ }
589
+ function stripWrappers(content) {
590
+ return content.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
591
+ }
592
+ function extractClasses(body, entities) {
593
+ const classRegex = /class\s+(\w+)/g;
594
+ let match = classRegex.exec(body);
595
+ while (match !== null) {
596
+ const id = match[1] ?? "";
597
+ if (id && !entities.has(id)) {
598
+ entities.set(id, { id, label: id });
599
+ }
600
+ match = classRegex.exec(body);
601
+ }
602
+ }
603
+ function extractComponents(body, entities) {
604
+ const componentRegex = /\[([^\]]+)\]/g;
605
+ let match = componentRegex.exec(body);
606
+ while (match !== null) {
607
+ const label = (match[1] ?? "").trim();
608
+ if (label) {
609
+ const id = label.replace(/\s+/g, "_");
610
+ if (!entities.has(id)) {
611
+ entities.set(id, { id, label });
612
+ }
613
+ }
614
+ match = componentRegex.exec(body);
615
+ }
616
+ }
617
+ function parseRelationshipMatch(match) {
618
+ const from = match[1] ?? "";
619
+ const to = match[2] ?? "";
620
+ if (!from || !to) return null;
621
+ const label = match[3]?.trim();
622
+ return { from, to, ...label ? { label } : {} };
623
+ }
624
+ function collectRelationshipMatches(body, regex) {
625
+ const results = [];
626
+ let match = regex.exec(body);
627
+ while (match !== null) {
628
+ const rel = parseRelationshipMatch(match);
629
+ if (rel) results.push(rel);
630
+ match = regex.exec(body);
631
+ }
632
+ return results;
633
+ }
634
+ function extractRelationships(body) {
635
+ return collectRelationshipMatches(
636
+ body,
637
+ /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g
638
+ );
639
+ }
640
+ function applyReactionBoost(candidate, nodeType, metadata) {
641
+ if (nodeType !== "conversation" || !metadata.reactions) return;
642
+ const reactions = metadata.reactions;
643
+ const totalReactions = Object.values(reactions).reduce((sum, count) => sum + count, 0);
644
+ if (totalReactions > 0) {
645
+ candidate.confidence = Math.min(1, Math.round((candidate.confidence + 0.1) * 100) / 100);
646
+ }
647
+ }
648
+ function detectLanguage(filePath) {
649
+ const name = path9.basename(filePath);
650
+ for (const skip of SKIP_EXTENSIONS2) {
651
+ if (name.endsWith(skip)) return void 0;
652
+ }
653
+ const ext = path9.extname(filePath);
654
+ return EXT_TO_LANGUAGE[ext];
655
+ }
656
+ function createExtractionRunner() {
657
+ return new ExtractionRunner(ALL_EXTRACTORS);
658
+ }
659
+ function levenshteinDistance(a, b) {
660
+ const m = a.length;
661
+ const n = b.length;
662
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
663
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
664
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
665
+ for (let i = 1; i <= m; i++) {
666
+ for (let j = 1; j <= n; j++) {
667
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
668
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
669
+ }
670
+ }
671
+ return dp[m][n];
672
+ }
673
+ function levenshteinRatio(a, b) {
674
+ const maxLen = Math.max(a.length, b.length);
675
+ if (maxLen === 0) return 1;
676
+ return 1 - levenshteinDistance(a, b) / maxLen;
677
+ }
678
+ function classifyConflict(a, b) {
679
+ const factTypes = /* @__PURE__ */ new Set([
680
+ "business_fact",
681
+ "business_metric",
682
+ "design_token"
683
+ ]);
684
+ const termTypes = /* @__PURE__ */ new Set(["business_term", "business_concept"]);
685
+ if (factTypes.has(a.type) || factTypes.has(b.type)) return "value_mismatch";
686
+ if (termTypes.has(a.type) || termTypes.has(b.type)) return "definition_conflict";
687
+ if (a.lastModified && b.lastModified && a.lastModified !== b.lastModified)
688
+ return "temporal_conflict";
689
+ return "status_divergence";
690
+ }
691
+ function buildEntry(node, source) {
692
+ return {
693
+ nodeId: node.id,
694
+ source,
695
+ name: node.name,
696
+ content: node.content ?? "",
697
+ lastModified: node.lastModified
698
+ };
699
+ }
700
+ function groupByName(nodes) {
701
+ const byName = /* @__PURE__ */ new Map();
702
+ for (const node of nodes) {
703
+ const key = node.name.toLowerCase().trim();
704
+ const group = byName.get(key) ?? [];
705
+ group.push(node);
706
+ byName.set(key, group);
707
+ }
708
+ return byName;
709
+ }
710
+ function tryAddContradiction(acc, a, b, similarity) {
711
+ const sourceA = a.metadata.source ?? "unknown";
712
+ const sourceB = b.metadata.source ?? "unknown";
713
+ if (sourceA === sourceB) return;
714
+ if ((a.hash ?? a.id) === (b.hash ?? b.id)) return;
715
+ const pairId = [a.id, b.id].sort().join(":");
716
+ if (acc.seen.has(pairId)) return;
717
+ acc.seen.add(pairId);
718
+ const conflictType = classifyConflict(a, b);
719
+ const pairKey = [sourceA, sourceB].sort().join("\u2194");
720
+ acc.sourcePairCounts[pairKey] = (acc.sourcePairCounts[pairKey] ?? 0) + 1;
721
+ acc.contradictions.push({
722
+ id: `contradiction:${a.id}:${b.id}`,
723
+ entityA: buildEntry(a, sourceA),
724
+ entityB: buildEntry(b, sourceB),
725
+ similarity,
726
+ conflictType,
727
+ severity: SEVERITY_MAP[conflictType],
728
+ description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
729
+ });
730
+ }
731
+ function collectExactMatches(byName, acc) {
732
+ for (const [, group] of byName) {
733
+ if (group.length < 2) continue;
734
+ for (let i = 0; i < group.length; i++) {
735
+ for (let j = i + 1; j < group.length; j++) {
736
+ tryAddContradiction(acc, group[i], group[j], 1);
737
+ }
738
+ }
739
+ }
740
+ }
741
+ function collectFuzzyMatches(byName, acc) {
742
+ const keys = [...byName.keys()];
743
+ for (let i = 0; i < keys.length; i++) {
744
+ for (let j = i + 1; j < keys.length; j++) {
745
+ const keyA = keys[i];
746
+ const keyB = keys[j];
747
+ const maxLen = Math.max(keyA.length, keyB.length);
748
+ const minLen = Math.min(keyA.length, keyB.length);
749
+ if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
750
+ const ratio = levenshteinRatio(keyA, keyB);
751
+ if (ratio < SIMILARITY_THRESHOLD) continue;
752
+ const groupA = byName.get(keyA);
753
+ const groupB = byName.get(keyB);
754
+ for (const a of groupA) {
755
+ for (const b of groupB) {
756
+ tryAddContradiction(acc, a, b, ratio);
757
+ }
758
+ }
759
+ }
760
+ }
761
+ }
762
+ function toGrade(score) {
763
+ if (score >= 80) return "A";
764
+ if (score >= 60) return "B";
765
+ if (score >= 40) return "C";
766
+ if (score >= 20) return "D";
767
+ return "F";
768
+ }
769
+ function groupByDomain(nodes, fallback) {
770
+ const map = /* @__PURE__ */ new Map();
771
+ for (const node of nodes) {
772
+ const domain = node.metadata.domain ?? fallback(node);
773
+ const group = map.get(domain) ?? [];
774
+ group.push(node);
775
+ map.set(domain, group);
776
+ }
777
+ return map;
778
+ }
779
+ function countLinkedEntities(codeEntries, store) {
780
+ const linkedIds = /* @__PURE__ */ new Set();
781
+ for (const codeNode of codeEntries) {
782
+ if (hasKnowledgeEdge(codeNode.id, store)) {
783
+ linkedIds.add(codeNode.id);
784
+ }
785
+ }
786
+ return linkedIds;
787
+ }
788
+ function hasKnowledgeEdge(nodeId, store) {
789
+ for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
790
+ if (store.getEdges({ to: nodeId, type: edgeType }).length > 0) return true;
791
+ }
792
+ return false;
793
+ }
794
+ function computeSourceBreakdown(knEntries) {
795
+ const breakdown = {};
796
+ for (const kn of knEntries) {
797
+ const src = kn.metadata.source ?? "unknown";
798
+ breakdown[src] = (breakdown[src] ?? 0) + 1;
799
+ }
800
+ return breakdown;
801
+ }
802
+ function computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources) {
803
+ const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
804
+ const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
805
+ const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
806
+ return Math.round(codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent);
807
+ }
808
+ function scoreDomain(domain, knEntries, codeEntries, store) {
809
+ const linkedIds = countLinkedEntities(codeEntries, store);
810
+ const sourceBreakdown = computeSourceBreakdown(knEntries);
811
+ const codeEntities = codeEntries.length;
812
+ const linkedEntities = linkedIds.size;
813
+ const knowledgeEntries = knEntries.length;
814
+ const uniqueSources = Object.keys(sourceBreakdown).length;
815
+ const score = computeDomainScore(knowledgeEntries, codeEntities, linkedEntities, uniqueSources);
816
+ return {
817
+ domain,
818
+ score,
819
+ knowledgeEntries,
820
+ codeEntities,
821
+ linkedEntities,
822
+ unlinkedEntities: codeEntities - linkedEntities,
823
+ sourceBreakdown,
824
+ grade: toGrade(score)
825
+ };
826
+ }
827
+ function withRetry(client, options) {
828
+ const maxRetries = options?.maxRetries ?? 3;
829
+ const baseDelayMs = options?.baseDelayMs ?? 1e3;
830
+ const maxDelayMs = options?.maxDelayMs ?? 3e4;
831
+ return async (url, requestOptions) => {
832
+ let lastError;
833
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
834
+ try {
835
+ const response = await client(url, requestOptions);
836
+ if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
837
+ return response;
838
+ }
839
+ if (attempt === maxRetries) {
840
+ return response;
841
+ }
842
+ } catch (err) {
843
+ lastError = err instanceof Error ? err : new Error(String(err));
844
+ if (attempt === maxRetries) {
845
+ throw lastError;
846
+ }
847
+ }
848
+ const exponentialDelay = baseDelayMs * 2 ** attempt;
849
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
850
+ const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
851
+ await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
852
+ }
853
+ throw lastError ?? new Error("Request failed after retries");
854
+ };
855
+ }
356
856
  function sanitizeExternalText(text, maxLength = 2e3) {
357
857
  let sanitized = text;
358
858
  for (const rule of SANITIZE_RULES) {
@@ -383,6 +883,44 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
383
883
  }
384
884
  return edgesCreated;
385
885
  }
886
+ async function condenseContent(raw, options, summarizeFn) {
887
+ const originalLength = raw.length;
888
+ const { maxLength } = options;
889
+ const summarizationThreshold = options.summarizationThreshold ?? maxLength * 2;
890
+ if (raw.length <= maxLength) {
891
+ return { content: raw, method: "passthrough", originalLength };
892
+ }
893
+ if (raw.length < summarizationThreshold) {
894
+ return {
895
+ content: sanitizeExternalText(raw, maxLength),
896
+ method: "truncated",
897
+ originalLength
898
+ };
899
+ }
900
+ if (options.modelEndpoint && summarizeFn) {
901
+ try {
902
+ const summarized = await summarizeFn(SUMMARIZE_PROMPT + raw, {
903
+ endpoint: options.modelEndpoint,
904
+ model: options.modelName ?? "default",
905
+ maxTokens: Math.ceil(maxLength / 4)
906
+ // rough token estimate
907
+ });
908
+ const finalContent = summarized.length > maxLength ? sanitizeExternalText(summarized, maxLength) : summarized;
909
+ return { content: finalContent, method: "summarized", originalLength };
910
+ } catch {
911
+ return {
912
+ content: sanitizeExternalText(raw, maxLength),
913
+ method: "truncated",
914
+ originalLength
915
+ };
916
+ }
917
+ }
918
+ return {
919
+ content: sanitizeExternalText(raw, maxLength),
920
+ method: "truncated",
921
+ originalLength
922
+ };
923
+ }
386
924
  function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
387
925
  return {
388
926
  nodesAdded,
@@ -408,6 +946,28 @@ function buildJql(config) {
408
946
  }
409
947
  return jql;
410
948
  }
949
+ function parseAcceptanceCriteria(description) {
950
+ const criteria = [];
951
+ const checkboxRegex = /[-*]\s*\[[ x]\]\s*(.+)/gi;
952
+ let match;
953
+ while ((match = checkboxRegex.exec(description)) !== null) {
954
+ criteria.push(match[1].trim());
955
+ }
956
+ const gwtRegex = /Given\b.+?When\b.+?Then\b.+/gi;
957
+ while ((match = gwtRegex.exec(description)) !== null) {
958
+ criteria.push(match[0].trim());
959
+ }
960
+ return criteria;
961
+ }
962
+ function extractCustomFields(fields) {
963
+ const result = {};
964
+ for (const [key, value] of Object.entries(fields)) {
965
+ if (key.startsWith("customfield_") && value != null && typeof value === "string") {
966
+ result[key] = value;
967
+ }
968
+ }
969
+ return result;
970
+ }
411
971
  function missingApiKeyResult(envVar, start) {
412
972
  return {
413
973
  nodesAdded: 0,
@@ -418,7 +978,7 @@ function missingApiKeyResult(envVar, start) {
418
978
  durationMs: Date.now() - start
419
979
  };
420
980
  }
421
- function emptyResult2(errors, start) {
981
+ function emptyResult5(errors, start) {
422
982
  return {
423
983
  nodesAdded: 0,
424
984
  nodesUpdated: 0,
@@ -473,6 +1033,99 @@ function ingestRun(store, run) {
473
1033
  }
474
1034
  return { nodesAdded, edgesAdded };
475
1035
  }
1036
+ function hasConstraintKeyword(text) {
1037
+ const lower = text.toLowerCase();
1038
+ return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
1039
+ }
1040
+ function buildNodeMetadata(condensed, base) {
1041
+ const metadata = { ...base };
1042
+ if (condensed.method !== "passthrough") {
1043
+ metadata.condensed = condensed.method;
1044
+ metadata.originalLength = condensed.originalLength;
1045
+ }
1046
+ return metadata;
1047
+ }
1048
+ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
1049
+ return {
1050
+ nodesAdded,
1051
+ nodesUpdated: 0,
1052
+ edgesAdded,
1053
+ edgesUpdated: 0,
1054
+ errors,
1055
+ durationMs: Date.now() - start
1056
+ };
1057
+ }
1058
+ function validateFigmaConfig(config) {
1059
+ const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
1060
+ const apiKey = process.env[apiKeyEnv];
1061
+ if (!apiKey) return { error: `Missing API key: environment variable "${apiKeyEnv}" is not set` };
1062
+ const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
1063
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
1064
+ try {
1065
+ const parsed = new URL(baseUrl);
1066
+ if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
1067
+ return { error: `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com` };
1068
+ }
1069
+ } catch {
1070
+ return { error: `Invalid ${baseUrlEnv}: not a valid URL` };
1071
+ }
1072
+ const fileIds = config.fileIds;
1073
+ if (!fileIds || fileIds.length === 0) return { error: "No fileIds provided in connector config" };
1074
+ return { apiKey, baseUrl, fileIds };
1075
+ }
1076
+ function stripHtml(html) {
1077
+ return html.replace(/<[^>]*>/g, "");
1078
+ }
1079
+ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
1080
+ return {
1081
+ nodesAdded,
1082
+ nodesUpdated: 0,
1083
+ edgesAdded,
1084
+ edgesUpdated: 0,
1085
+ errors,
1086
+ durationMs: Date.now() - start
1087
+ };
1088
+ }
1089
+ function validateBaseUrl(baseUrl, baseUrlEnv) {
1090
+ try {
1091
+ const parsed = new URL(baseUrl);
1092
+ if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
1093
+ return `Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`;
1094
+ }
1095
+ } catch {
1096
+ return `Invalid ${baseUrlEnv}: not a valid URL`;
1097
+ }
1098
+ return null;
1099
+ }
1100
+ function resolveConfig(config, start) {
1101
+ const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
1102
+ const apiKey = process.env[apiKeyEnv];
1103
+ if (!apiKey) {
1104
+ return {
1105
+ ok: false,
1106
+ result: buildIngestResult3(
1107
+ 0,
1108
+ 0,
1109
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
1110
+ start
1111
+ )
1112
+ };
1113
+ }
1114
+ const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
1115
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
1116
+ const urlError = validateBaseUrl(baseUrl, baseUrlEnv);
1117
+ if (urlError) {
1118
+ return { ok: false, result: buildIngestResult3(0, 0, [urlError], start) };
1119
+ }
1120
+ const boardIds = config.boardIds;
1121
+ if (!boardIds || boardIds.length === 0) {
1122
+ return {
1123
+ ok: false,
1124
+ result: buildIngestResult3(0, 0, ["No boardIds provided in config"], start)
1125
+ };
1126
+ }
1127
+ return { ok: true, apiKey, baseUrl, boardIds, headers: { Authorization: `Bearer ${apiKey}` } };
1128
+ }
476
1129
  function isSkippableWord(cleaned, allConsumed) {
477
1130
  if (allConsumed.has(cleaned)) return true;
478
1131
  const lower = cleaned.toLowerCase();
@@ -700,7 +1353,7 @@ function isDTCGToken(obj) {
700
1353
  }
701
1354
  async function readFileOrNull(filePath) {
702
1355
  try {
703
- return await fs6.readFile(filePath, "utf-8");
1356
+ return await fs11.readFile(filePath, "utf-8");
704
1357
  } catch {
705
1358
  return null;
706
1359
  }
@@ -782,7 +1435,7 @@ function parseAntiPatterns(content) {
782
1435
  }
783
1436
  return patterns;
784
1437
  }
785
- var NODE_TYPES, EDGE_TYPES, OBSERVABILITY_TYPES, CURRENT_SCHEMA_VERSION, NODE_STABILITY, GraphNodeSchema, GraphEdgeSchema, POISONED_KEYS, GraphStore, VectorStore, DEFAULT_TTL_MS, PackedSummaryCache, ContextQL, TEST_TYPES, DOC_TYPES, CODE_TYPES, SKIP_METHOD_NAMES, FUNCTION_PATTERNS, CLASS_PATTERNS, INTERFACE_PATTERNS, METHOD_PATTERNS, GO_METHOD_PATTERN, VARIABLE_PATTERNS, SUPPORTED_EXTENSIONS, SKIP_EXTENSIONS, CodeIngestor, execFileAsync, GitIngestor, TopologicalLinker, CODE_NODE_TYPES, KnowledgeIngestor, BUSINESS_KNOWLEDGE_TYPES, GOVERNS_SOURCE_TYPES, CODE_NODE_TYPES2, MEASURABLE_TYPES, BusinessKnowledgeIngestor, REQUIREMENT_SECTIONS, SECTION_HEADING_RE, NUMBERED_ITEM_RE, CODE_NODE_TYPES3, RequirementIngestor, CODE_NODE_TYPES4, SANITIZE_RULES, SyncManager, JiraConnector, SlackConnector, ConfluenceConnector, CIConnector, STOP_WORDS, FusionLayer, CODE_NODE_TYPES5, GraphEntropyAdapter, GraphComplexityAdapter, GraphCouplingAdapter, DEFAULT_THRESHOLD, DEFAULT_METRICS, RECOGNIZED_METRICS, GraphAnomalyAdapter, INTENTS, SIGNAL_WEIGHTS, INTENT_SIGNALS, IntentClassifier, INTENT_KEYWORDS, STOP_WORDS2, PASCAL_OR_CAMEL_RE, FILE_PATH_RE, QUOTED_RE, EntityExtractor, EntityResolver, ResponseFormatter, CompositeProbabilityStrategy, DEFAULT_PROBABILITY_FLOOR, DEFAULT_MAX_DEPTH, CascadeSimulator, ENTITY_REQUIRED_INTENTS, classifier, extractor, formatter, PHASE_NODE_TYPES, CODE_NODE_TYPES6, Assembler, GraphConstraintAdapter, DesignIngestor, DesignConstraintAdapter, GraphFeedbackAdapter, DEFAULT_EDGE_TYPES, TaskIndependenceAnalyzer, ConflictPredictor, VERSION;
1438
+ var NODE_TYPES, EDGE_TYPES, OBSERVABILITY_TYPES, CURRENT_SCHEMA_VERSION, NODE_STABILITY, GraphNodeSchema, GraphEdgeSchema, POISONED_KEYS, GraphStore, VectorStore, DEFAULT_TTL_MS, PackedSummaryCache, ContextQL, TEST_TYPES, DOC_TYPES, CODE_TYPES, SKIP_METHOD_NAMES, FUNCTION_PATTERNS, CLASS_PATTERNS, INTERFACE_PATTERNS, METHOD_PATTERNS, GO_METHOD_PATTERN, VARIABLE_PATTERNS, SUPPORTED_EXTENSIONS, SKIP_EXTENSIONS, CodeIngestor, execFileAsync, GitIngestor, TopologicalLinker, CODE_NODE_TYPES, KnowledgeIngestor, BUSINESS_KNOWLEDGE_TYPES, GOVERNS_SOURCE_TYPES, CODE_NODE_TYPES2, MEASURABLE_TYPES, BusinessKnowledgeIngestor, REQUIREMENT_SECTIONS, SECTION_HEADING_RE, NUMBERED_ITEM_RE, CODE_NODE_TYPES3, RequirementIngestor, MermaidParser, D2Parser, PlantUmlParser, DIAGRAM_EXTENSIONS, SKIP_DIRS, DiagramParser, HEURISTIC_PATTERNS, SCANNABLE_TYPES, KnowledgeLinker, StructuralDriftDetector, KnowledgeStagingAggregator, SKIP_DIRS2, EXT_TO_LANGUAGE, SKIP_EXTENSIONS2, EXTRACTOR_EDGE_TYPE, ExtractionRunner, PATTERNS, TestDescriptionExtractor, EnumConstantExtractor, ValidationRuleExtractor, ApiPathExtractor, ALL_EXTRACTORS, ImageAnalysisExtractor, KNOWLEDGE_NODE_TYPES, SIMILARITY_THRESHOLD, SEVERITY_MAP, ContradictionDetector, KNOWLEDGE_TYPES, CODE_TYPES2, KNOWLEDGE_EDGE_TYPES, CoverageScorer, BUSINESS_NODE_TYPES, SNAPSHOT_NODE_TYPES, KnowledgePipelineRunner, CODE_NODE_TYPES4, RETRYABLE_STATUSES, SANITIZE_RULES, SyncManager, SUMMARIZE_PROMPT, JiraConnector, SlackConnector, ConfluenceConnector, CIConnector, CONSTRAINT_KEYWORDS, FigmaConnector, MIN_CONTENT_LENGTH, CONCEPT_ITEM_TYPES, MiroConnector, STOP_WORDS, FusionLayer, CODE_NODE_TYPES5, GraphEntropyAdapter, GraphComplexityAdapter, GraphCouplingAdapter, DEFAULT_THRESHOLD, DEFAULT_METRICS, RECOGNIZED_METRICS, GraphAnomalyAdapter, INTENTS, SIGNAL_WEIGHTS, INTENT_SIGNALS, IntentClassifier, INTENT_KEYWORDS, STOP_WORDS2, PASCAL_OR_CAMEL_RE, FILE_PATH_RE, QUOTED_RE, EntityExtractor, EntityResolver, ResponseFormatter, CompositeProbabilityStrategy, DEFAULT_PROBABILITY_FLOOR, DEFAULT_MAX_DEPTH, CascadeSimulator, ENTITY_REQUIRED_INTENTS, classifier, extractor, formatter, PHASE_NODE_TYPES, CODE_NODE_TYPES6, Assembler, GraphConstraintAdapter, DesignIngestor, DesignConstraintAdapter, GraphFeedbackAdapter, DEFAULT_EDGE_TYPES, TaskIndependenceAnalyzer, ConflictPredictor, VERSION;
786
1439
  var init_dist = __esm({
787
1440
  "../graph/dist/index.mjs"() {
788
1441
  NODE_TYPES = [
@@ -822,6 +1475,7 @@ var init_dist = __esm({
822
1475
  "design_token",
823
1476
  "aesthetic_intent",
824
1477
  "design_constraint",
1478
+ "image_annotation",
825
1479
  // Traceability
826
1480
  "requirement",
827
1481
  // Business Knowledge
@@ -830,6 +1484,7 @@ var init_dist = __esm({
830
1484
  "business_concept",
831
1485
  "business_term",
832
1486
  "business_metric",
1487
+ "business_fact",
833
1488
  // Cache
834
1489
  "packed_summary"
835
1490
  ];
@@ -869,6 +1524,7 @@ var init_dist = __esm({
869
1524
  // Business Knowledge relationships
870
1525
  "governs",
871
1526
  "measures",
1527
+ "annotates",
872
1528
  // Cache relationships
873
1529
  "caches"
874
1530
  ];
@@ -2671,80 +3327,2328 @@ var init_dist = __esm({
2671
3327
  return count;
2672
3328
  }
2673
3329
  };
2674
- CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2675
- SANITIZE_RULES = [
2676
- // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
2677
- {
2678
- pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
2679
- replacement: ""
2680
- },
2681
- // Strip markdown-style system prompt markers (including trailing space)
2682
- {
2683
- pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
2684
- replacement: ""
2685
- },
2686
- // Strip common injection prefixes
2687
- {
2688
- pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
2689
- replacement: "[filtered]"
2690
- },
2691
- // Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
2692
- {
2693
- pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
2694
- replacement: "[filtered]"
3330
+ MermaidParser = class {
3331
+ canParse(_content, ext) {
3332
+ return ext === ".mmd" || ext === ".mermaid";
2695
3333
  }
2696
- ];
2697
- SyncManager = class {
2698
- constructor(store, graphDir) {
2699
- this.store = store;
2700
- this.metadataPath = path6.join(graphDir, "sync-metadata.json");
3334
+ parse(content, _filePath) {
3335
+ const trimmed = content.trim();
3336
+ if (!trimmed) {
3337
+ return emptyResult2();
3338
+ }
3339
+ const diagramType = detectDiagramType(trimmed);
3340
+ switch (diagramType) {
3341
+ case "flowchart":
3342
+ return parseFlowchart(trimmed, diagramType);
3343
+ case "sequence":
3344
+ return parseSequence(trimmed, diagramType);
3345
+ default:
3346
+ return emptyResult2(diagramType);
3347
+ }
2701
3348
  }
2702
- store;
2703
- registrations = /* @__PURE__ */ new Map();
2704
- metadataPath;
2705
- registerConnector(connector, config) {
2706
- this.registrations.set(connector.name, { connector, config });
3349
+ };
3350
+ D2Parser = class {
3351
+ canParse(_content, ext) {
3352
+ return ext === ".d2";
2707
3353
  }
2708
- async sync(connectorName) {
2709
- const registration = this.registrations.get(connectorName);
2710
- if (!registration) {
2711
- return {
2712
- nodesAdded: 0,
2713
- nodesUpdated: 0,
2714
- edgesAdded: 0,
2715
- edgesUpdated: 0,
2716
- errors: [`Connector "${connectorName}" not registered`],
2717
- durationMs: 0
2718
- };
3354
+ parse(content, _filePath) {
3355
+ const trimmed = content.trim();
3356
+ if (!trimmed) return emptyResult3();
3357
+ const state = {
3358
+ entities: /* @__PURE__ */ new Map(),
3359
+ relationships: [],
3360
+ braceDepth: 0
3361
+ };
3362
+ for (const line of trimmed.split("\n")) {
3363
+ processD2Line(line.trim(), state);
2719
3364
  }
2720
- const { connector, config } = registration;
2721
- const result = await connector.ingest(this.store, config);
2722
- const metadata = await this.loadMetadata();
2723
- metadata.connectors[connectorName] = {
2724
- lastSyncTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
2725
- lastResult: result
3365
+ return {
3366
+ entities: Array.from(state.entities.values()),
3367
+ relationships: state.relationships,
3368
+ metadata: { format: "d2", diagramType: "architecture" }
2726
3369
  };
2727
- await this.saveMetadata(metadata);
2728
- return result;
2729
3370
  }
2730
- async syncAll() {
2731
- const combined = {
2732
- nodesAdded: 0,
2733
- nodesUpdated: 0,
2734
- edgesAdded: 0,
2735
- edgesUpdated: 0,
2736
- errors: [],
2737
- durationMs: 0
3371
+ };
3372
+ PlantUmlParser = class {
3373
+ canParse(_content, ext) {
3374
+ return ext === ".puml" || ext === ".plantuml";
3375
+ }
3376
+ parse(content, _filePath) {
3377
+ const trimmed = content.trim();
3378
+ if (!trimmed) return emptyResult4();
3379
+ const diagramType = detectDiagramType2(trimmed);
3380
+ const entities = /* @__PURE__ */ new Map();
3381
+ const body = stripWrappers(trimmed);
3382
+ extractClasses(body, entities);
3383
+ extractComponents(body, entities);
3384
+ const relationships = extractRelationships(body);
3385
+ return {
3386
+ entities: Array.from(entities.values()),
3387
+ relationships,
3388
+ metadata: { format: "plantuml", diagramType }
2738
3389
  };
2739
- for (const [name] of this.registrations) {
2740
- const result = await this.sync(name);
2741
- combined.nodesAdded += result.nodesAdded;
2742
- combined.nodesUpdated += result.nodesUpdated;
2743
- combined.edgesAdded += result.edgesAdded;
3390
+ }
3391
+ };
3392
+ DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
3393
+ SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
3394
+ DiagramParser = class {
3395
+ constructor(store) {
3396
+ this.store = store;
3397
+ }
3398
+ store;
3399
+ parsers = [
3400
+ new MermaidParser(),
3401
+ new D2Parser(),
3402
+ new PlantUmlParser()
3403
+ ];
3404
+ parse(content, filePath) {
3405
+ const ext = path6.extname(filePath).toLowerCase();
3406
+ for (const parser of this.parsers) {
3407
+ if (parser.canParse(content, ext)) {
3408
+ return parser.parse(content, filePath);
3409
+ }
3410
+ }
3411
+ return {
3412
+ entities: [],
3413
+ relationships: [],
3414
+ metadata: { format: "mermaid", diagramType: "unknown" }
3415
+ };
3416
+ }
3417
+ async ingest(projectDir) {
3418
+ const start = Date.now();
3419
+ let nodesAdded = 0;
3420
+ let edgesAdded = 0;
3421
+ const errors = [];
3422
+ const diagramFiles = await this.findDiagramFiles(projectDir);
3423
+ for (const filePath of diagramFiles) {
3424
+ try {
3425
+ const content = await fs5.readFile(filePath, "utf-8");
3426
+ const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
3427
+ const result = this.parse(content, filePath);
3428
+ if (result.entities.length === 0) continue;
3429
+ const pathHash = hash(relPath);
3430
+ nodesAdded += this.addEntityNodes(result, relPath, pathHash);
3431
+ edgesAdded += this.addRelationshipEdges(result, pathHash);
3432
+ } catch (err) {
3433
+ errors.push(
3434
+ `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3435
+ );
3436
+ }
3437
+ }
3438
+ return {
3439
+ nodesAdded,
3440
+ nodesUpdated: 0,
3441
+ edgesAdded,
3442
+ edgesUpdated: 0,
3443
+ errors,
3444
+ durationMs: Date.now() - start
3445
+ };
3446
+ }
3447
+ /** Map diagram entities to business_concept graph nodes. */
3448
+ addEntityNodes(result, relPath, pathHash) {
3449
+ let count = 0;
3450
+ for (const entity of result.entities) {
3451
+ const nodeId = `diagram:${pathHash}:${entity.id}`;
3452
+ this.store.addNode({
3453
+ id: nodeId,
3454
+ type: "business_concept",
3455
+ name: entity.label,
3456
+ path: relPath,
3457
+ metadata: {
3458
+ source: "diagram",
3459
+ format: result.metadata.format,
3460
+ diagramType: result.metadata.diagramType,
3461
+ confidence: 0.85,
3462
+ ...entity.type ? { entityType: entity.type } : {}
3463
+ }
3464
+ });
3465
+ count++;
3466
+ }
3467
+ return count;
3468
+ }
3469
+ /** Map diagram relationships to references graph edges. */
3470
+ addRelationshipEdges(result, pathHash) {
3471
+ let count = 0;
3472
+ for (const rel of result.relationships) {
3473
+ const fromId = `diagram:${pathHash}:${rel.from}`;
3474
+ const toId = `diagram:${pathHash}:${rel.to}`;
3475
+ this.store.addEdge({
3476
+ from: fromId,
3477
+ to: toId,
3478
+ type: "references",
3479
+ metadata: {
3480
+ ...rel.label ? { label: rel.label } : {}
3481
+ }
3482
+ });
3483
+ count++;
3484
+ }
3485
+ return count;
3486
+ }
3487
+ async findDiagramFiles(dir) {
3488
+ const files = [];
3489
+ const walk = async (currentDir) => {
3490
+ let entries;
3491
+ try {
3492
+ entries = await fs5.readdir(currentDir, { withFileTypes: true });
3493
+ } catch {
3494
+ return;
3495
+ }
3496
+ for (const entry of entries) {
3497
+ if (entry.isDirectory()) {
3498
+ if (!SKIP_DIRS.has(entry.name)) {
3499
+ await walk(path6.join(currentDir, entry.name));
3500
+ }
3501
+ } else if (entry.isFile()) {
3502
+ const ext = path6.extname(entry.name).toLowerCase();
3503
+ if (DIAGRAM_EXTENSIONS.has(ext)) {
3504
+ files.push(path6.join(currentDir, entry.name));
3505
+ }
3506
+ }
3507
+ }
3508
+ };
3509
+ await walk(dir);
3510
+ return files;
3511
+ }
3512
+ };
3513
+ HEURISTIC_PATTERNS = [
3514
+ {
3515
+ name: "business-rule-imperative",
3516
+ pattern: /\b(?:must|shall|required)\b.*\b(?:system|service|api|user|data|app|application|client|server|process|handler|module)\b/i,
3517
+ signal: "Business rule",
3518
+ confidence: 0.7,
3519
+ nodeType: "business_fact"
3520
+ },
3521
+ {
3522
+ name: "sla-slo-pattern",
3523
+ pattern: /(?:\b\d+(?:\.\d+)?%\s*(?:availability|uptime|success\s*rate)|\bunder\s+\d+\s*(?:ms|seconds?|minutes?|hours?)\b|\b(?:SLA|SLO)\b|\b99(?:\.\d+)?%\b)/i,
3524
+ signal: "Business constraint (SLA/SLO)",
3525
+ confidence: 0.8,
3526
+ nodeType: "business_fact"
3527
+ },
3528
+ {
3529
+ name: "monetary-amount",
3530
+ pattern: /\$[\d,]+(?:\.\d{2})?\s*(?:\b(?:cost|revenue|budget|price|fee|license|subscription|annual|monthly)\b)?/i,
3531
+ signal: "Business fact (monetary)",
3532
+ confidence: 0.6,
3533
+ nodeType: "business_fact"
3534
+ },
3535
+ {
3536
+ name: "acceptance-criteria",
3537
+ pattern: /\b(?:Given\b.*\bWhen\b.*\bThen\b|\[[ x]\])/i,
3538
+ signal: "Business rule (acceptance criteria)",
3539
+ confidence: 0.8,
3540
+ nodeType: "business_fact"
3541
+ },
3542
+ {
3543
+ name: "regulatory-reference",
3544
+ pattern: /\b(?:GDPR|SOC\s*2|PCI(?:\s*-?\s*DSS)?|HIPAA|CCPA|FERPA|SOX)\b/i,
3545
+ signal: "Business rule (regulatory)",
3546
+ confidence: 0.9,
3547
+ nodeType: "business_fact"
3548
+ }
3549
+ ];
3550
+ SCANNABLE_TYPES = ["issue", "conversation", "document"];
3551
+ KnowledgeLinker = class {
3552
+ constructor(store, outputDir) {
3553
+ this.store = store;
3554
+ this.outputDir = outputDir;
3555
+ }
3556
+ store;
3557
+ outputDir;
3558
+ async link() {
3559
+ const errors = [];
3560
+ const candidates = this.scanAllNodes(errors);
3561
+ await this.writeJsonl(candidates);
3562
+ const conceptsClustered = this.clusterBySource(candidates);
3563
+ const promoted = this.promoteAndDeduplicate(candidates);
3564
+ await this.writeStagedJsonl(promoted.staged);
3565
+ return {
3566
+ factsCreated: promoted.factsCreated,
3567
+ conceptsClustered,
3568
+ duplicatesMerged: promoted.duplicatesMerged,
3569
+ stagedForReview: promoted.staged.length,
3570
+ errors
3571
+ };
3572
+ }
3573
+ /** Stage 1: Scan all scannable node types for heuristic pattern matches. */
3574
+ scanAllNodes(errors) {
3575
+ const candidates = [];
3576
+ for (const type of SCANNABLE_TYPES) {
3577
+ const nodes = this.store.findNodes({ type });
3578
+ for (const node of nodes) {
3579
+ if (!node.content) continue;
3580
+ try {
3581
+ const matches = this.scanPatterns(node.content, node.id, type);
3582
+ for (const match of matches) {
3583
+ applyReactionBoost(match, type, node.metadata);
3584
+ candidates.push(match);
3585
+ }
3586
+ } catch (err) {
3587
+ errors.push(
3588
+ `Scan failed for ${node.id}: ${err instanceof Error ? err.message : String(err)}`
3589
+ );
3590
+ }
3591
+ }
3592
+ }
3593
+ return candidates;
3594
+ }
3595
+ /** Stage 2: Group candidates by source node and create business_concept for 3+. */
3596
+ clusterBySource(candidates) {
3597
+ const sourceGroups = /* @__PURE__ */ new Map();
3598
+ for (const candidate of candidates) {
3599
+ const group = sourceGroups.get(candidate.sourceNodeId) ?? [];
3600
+ group.push(candidate);
3601
+ sourceGroups.set(candidate.sourceNodeId, group);
3602
+ }
3603
+ let clustered = 0;
3604
+ for (const [sourceNodeId, group] of sourceGroups) {
3605
+ if (group.length >= 3) {
3606
+ this.store.addNode({
3607
+ id: `concept:linker:${hash(sourceNodeId)}`,
3608
+ type: "business_concept",
3609
+ name: `Business concept cluster from ${sourceNodeId}`,
3610
+ metadata: {
3611
+ source: "knowledge-linker",
3612
+ sources: [sourceNodeId],
3613
+ factCount: group.length
3614
+ }
3615
+ });
3616
+ clustered++;
3617
+ }
3618
+ }
3619
+ return clustered;
3620
+ }
3621
+ /** Stage 3: Promote high-confidence candidates, stage medium, discard low. */
3622
+ promoteAndDeduplicate(candidates) {
3623
+ let factsCreated = 0;
3624
+ let duplicatesMerged = 0;
3625
+ const staged = [];
3626
+ for (const candidate of candidates) {
3627
+ if (candidate.confidence >= 0.8) {
3628
+ const merged = this.tryMergeDuplicate(candidate);
3629
+ if (merged) {
3630
+ duplicatesMerged++;
3631
+ continue;
3632
+ }
3633
+ this.createFactNode(candidate);
3634
+ factsCreated++;
3635
+ } else if (candidate.confidence >= 0.5) {
3636
+ staged.push(candidate);
3637
+ }
3638
+ }
3639
+ return { factsCreated, duplicatesMerged, staged };
3640
+ }
3641
+ /** Check for existing duplicate fact and merge sources if found. Returns true if merged. */
3642
+ tryMergeDuplicate(candidate) {
3643
+ if (this.store.getNode(candidate.id)) return true;
3644
+ const existingFacts = this.store.findNodes({ type: "business_fact" });
3645
+ const duplicate = existingFacts.find(
3646
+ (f) => f.content === candidate.content && f.id !== candidate.id
3647
+ );
3648
+ if (!duplicate) return false;
3649
+ const existingSources = duplicate.metadata.sources ?? [];
3650
+ if (!existingSources.includes(candidate.sourceNodeId)) {
3651
+ existingSources.push(candidate.sourceNodeId);
3652
+ duplicate.metadata.sources = existingSources;
3653
+ }
3654
+ return true;
3655
+ }
3656
+ /** Create a business_fact node and governs edge for a promoted candidate. */
3657
+ createFactNode(candidate) {
3658
+ this.store.addNode({
3659
+ id: candidate.id,
3660
+ type: candidate.nodeType,
3661
+ name: candidate.name,
3662
+ content: candidate.content,
3663
+ metadata: {
3664
+ source: "knowledge-linker",
3665
+ pattern: candidate.pattern,
3666
+ signal: candidate.signal,
3667
+ confidence: candidate.confidence,
3668
+ sourceNodeId: candidate.sourceNodeId,
3669
+ sourceNodeType: candidate.sourceNodeType,
3670
+ sources: [candidate.sourceNodeId]
3671
+ }
3672
+ });
3673
+ this.store.addEdge({
3674
+ from: candidate.id,
3675
+ to: candidate.sourceNodeId,
3676
+ type: "governs",
3677
+ confidence: candidate.confidence,
3678
+ metadata: { source: "knowledge-linker" }
3679
+ });
3680
+ }
3681
+ /**
3682
+ * Write candidates to JSONL file for audit trail.
3683
+ */
3684
+ async writeJsonl(candidates) {
3685
+ if (!this.outputDir) return;
3686
+ await fs6.mkdir(this.outputDir, { recursive: true });
3687
+ const filePath = path7.join(this.outputDir, "linker.jsonl");
3688
+ const lines = candidates.map(
3689
+ (c) => JSON.stringify({
3690
+ id: c.id,
3691
+ sourceNodeId: c.sourceNodeId,
3692
+ sourceNodeType: c.sourceNodeType,
3693
+ name: c.name,
3694
+ confidence: c.confidence,
3695
+ pattern: c.pattern,
3696
+ signal: c.signal,
3697
+ nodeType: c.nodeType
3698
+ })
3699
+ );
3700
+ await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3701
+ }
3702
+ /**
3703
+ * Write medium-confidence candidates to staged JSONL for human review.
3704
+ */
3705
+ async writeStagedJsonl(candidates) {
3706
+ if (!this.outputDir || candidates.length === 0) return;
3707
+ const stagedDir = path7.join(this.outputDir, "staged");
3708
+ await fs6.mkdir(stagedDir, { recursive: true });
3709
+ const filePath = path7.join(stagedDir, "linker-staged.jsonl");
3710
+ const lines = candidates.map(
3711
+ (c) => JSON.stringify({
3712
+ id: c.id,
3713
+ sourceNodeId: c.sourceNodeId,
3714
+ sourceNodeType: c.sourceNodeType,
3715
+ name: c.name,
3716
+ confidence: c.confidence,
3717
+ pattern: c.pattern,
3718
+ signal: c.signal,
3719
+ nodeType: c.nodeType
3720
+ })
3721
+ );
3722
+ await fs6.writeFile(filePath, lines.join("\n") + "\n");
3723
+ }
3724
+ /**
3725
+ * Apply heuristic patterns to content and return candidate extractions.
3726
+ */
3727
+ scanPatterns(content, nodeId, nodeType) {
3728
+ const candidates = [];
3729
+ for (const heuristic of HEURISTIC_PATTERNS) {
3730
+ if (heuristic.pattern.test(content)) {
3731
+ const candidateId = `extracted:linker:${hash(nodeId + ":" + heuristic.name)}`;
3732
+ candidates.push({
3733
+ id: candidateId,
3734
+ sourceNodeId: nodeId,
3735
+ sourceNodeType: nodeType,
3736
+ name: `${heuristic.signal} from ${nodeId}`,
3737
+ content,
3738
+ confidence: heuristic.confidence,
3739
+ pattern: heuristic.name,
3740
+ signal: heuristic.signal,
3741
+ nodeType: heuristic.nodeType
3742
+ });
3743
+ }
3744
+ }
3745
+ return candidates;
3746
+ }
3747
+ };
3748
+ StructuralDriftDetector = class {
3749
+ detect(current, fresh) {
3750
+ const findings = [];
3751
+ const currentById = new Map(current.entries.map((e) => [e.id, e]));
3752
+ const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
3753
+ for (const [id, entry] of freshById) {
3754
+ if (!currentById.has(id)) {
3755
+ findings.push({
3756
+ entryId: id,
3757
+ classification: "new",
3758
+ fresh: entry,
3759
+ severity: "low"
3760
+ });
3761
+ }
3762
+ }
3763
+ for (const [id, entry] of currentById) {
3764
+ if (!freshById.has(id)) {
3765
+ findings.push({
3766
+ entryId: id,
3767
+ classification: "stale",
3768
+ current: entry,
3769
+ severity: "high"
3770
+ });
3771
+ }
3772
+ }
3773
+ for (const [id, freshEntry] of freshById) {
3774
+ const currentEntry = currentById.get(id);
3775
+ if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
3776
+ findings.push({
3777
+ entryId: id,
3778
+ classification: "drifted",
3779
+ current: currentEntry,
3780
+ fresh: freshEntry,
3781
+ severity: "medium"
3782
+ });
3783
+ }
3784
+ }
3785
+ const byName = /* @__PURE__ */ new Map();
3786
+ for (const entry of fresh.entries) {
3787
+ const group = byName.get(entry.name) ?? [];
3788
+ group.push(entry);
3789
+ byName.set(entry.name, group);
3790
+ }
3791
+ for (const [, group] of byName) {
3792
+ if (group.length > 1) {
3793
+ const sources = new Set(group.map((e) => e.source));
3794
+ const hashes = new Set(group.map((e) => e.contentHash));
3795
+ if (sources.size > 1 && hashes.size > 1) {
3796
+ const alreadyContradicting = new Set(
3797
+ findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
3798
+ );
3799
+ for (const entry of group) {
3800
+ if (!alreadyContradicting.has(entry.id)) {
3801
+ findings.push({
3802
+ entryId: entry.id,
3803
+ classification: "contradicting",
3804
+ fresh: entry,
3805
+ severity: "critical"
3806
+ });
3807
+ break;
3808
+ }
3809
+ }
3810
+ }
3811
+ }
3812
+ }
3813
+ const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
3814
+ const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
3815
+ const summary = {
3816
+ new: findings.filter((f) => f.classification === "new").length,
3817
+ drifted: findings.filter((f) => f.classification === "drifted").length,
3818
+ stale: findings.filter((f) => f.classification === "stale").length,
3819
+ contradicting: findings.filter((f) => f.classification === "contradicting").length
3820
+ };
3821
+ return { findings, driftScore, summary };
3822
+ }
3823
+ };
3824
+ KnowledgeStagingAggregator = class {
3825
+ constructor(projectDir) {
3826
+ this.projectDir = projectDir;
3827
+ }
3828
+ projectDir;
3829
+ async aggregate(extractorResults, linkerResults, diagramResults) {
3830
+ const all = [...extractorResults, ...linkerResults, ...diagramResults];
3831
+ const byHash = /* @__PURE__ */ new Map();
3832
+ for (const entry of all) {
3833
+ const existing = byHash.get(entry.contentHash);
3834
+ if (!existing || entry.confidence > existing.confidence) {
3835
+ byHash.set(entry.contentHash, entry);
3836
+ }
3837
+ }
3838
+ const deduplicated = Array.from(byHash.values());
3839
+ if (deduplicated.length === 0) {
3840
+ return { staged: 0 };
3841
+ }
3842
+ const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
3843
+ await fs7.mkdir(stagedDir, { recursive: true });
3844
+ const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
3845
+ await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3846
+ return { staged: deduplicated.length };
3847
+ }
3848
+ async generateGapReport(knowledgeDir) {
3849
+ const domains = [];
3850
+ let totalEntries = 0;
3851
+ try {
3852
+ const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
3853
+ const domainDirs = entries.filter((e) => e.isDirectory());
3854
+ for (const dir of domainDirs) {
3855
+ const domainPath = path8.join(knowledgeDir, dir.name);
3856
+ const files = await fs7.readdir(domainPath);
3857
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
3858
+ const entryCount = mdFiles.length;
3859
+ totalEntries += entryCount;
3860
+ domains.push({ domain: dir.name, entryCount });
3861
+ }
3862
+ } catch {
3863
+ }
3864
+ return {
3865
+ domains,
3866
+ totalEntries,
3867
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3868
+ };
3869
+ }
3870
+ async writeGapReport(report) {
3871
+ const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
3872
+ await fs7.mkdir(gapsDir, { recursive: true });
3873
+ const lines = [
3874
+ "# Knowledge Gaps Report",
3875
+ "",
3876
+ `Generated: ${report.generatedAt}`,
3877
+ "",
3878
+ "## Coverage by Domain",
3879
+ "",
3880
+ "| Domain | Entries |",
3881
+ "| ------ | ------- |"
3882
+ ];
3883
+ for (const domain of report.domains) {
3884
+ lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3885
+ }
3886
+ lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3887
+ await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3888
+ }
3889
+ };
3890
+ SKIP_DIRS2 = /* @__PURE__ */ new Set([
3891
+ "node_modules",
3892
+ "dist",
3893
+ "target",
3894
+ "build",
3895
+ ".git",
3896
+ "__pycache__",
3897
+ ".venv",
3898
+ ".turbo",
3899
+ ".harness",
3900
+ ".next",
3901
+ ".vscode",
3902
+ ".idea",
3903
+ "vendor",
3904
+ "out",
3905
+ ".gradle",
3906
+ ".gradle-home",
3907
+ "bin",
3908
+ "obj",
3909
+ "venv",
3910
+ "_build",
3911
+ "deps",
3912
+ "coverage",
3913
+ ".nyc_output"
3914
+ ]);
3915
+ EXT_TO_LANGUAGE = {
3916
+ ".ts": "typescript",
3917
+ ".tsx": "typescript",
3918
+ ".js": "javascript",
3919
+ ".jsx": "javascript",
3920
+ ".py": "python",
3921
+ ".go": "go",
3922
+ ".rs": "rust",
3923
+ ".java": "java"
3924
+ };
3925
+ SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
3926
+ EXTRACTOR_EDGE_TYPE = {
3927
+ "test-descriptions": "governs",
3928
+ "enum-constants": "documents",
3929
+ "validation-rules": "governs",
3930
+ "api-paths": "documents"
3931
+ };
3932
+ ExtractionRunner = class {
3933
+ constructor(extractors) {
3934
+ this.extractors = extractors;
3935
+ }
3936
+ extractors;
3937
+ /**
3938
+ * Run all extractors against a project directory.
3939
+ * @param projectDir - Project root directory
3940
+ * @param store - GraphStore for node/edge persistence
3941
+ * @param outputDir - Directory for JSONL output (e.g. .harness/knowledge/extracted/)
3942
+ */
3943
+ async run(projectDir, store, outputDir) {
3944
+ const start = Date.now();
3945
+ const errors = [];
3946
+ let nodesAdded = 0;
3947
+ let nodesUpdated = 0;
3948
+ let edgesAdded = 0;
3949
+ await fs8.mkdir(outputDir, { recursive: true });
3950
+ const files = await this.findSourceFiles(projectDir);
3951
+ const recordsByExtractor = /* @__PURE__ */ new Map();
3952
+ for (const ext of this.extractors) {
3953
+ recordsByExtractor.set(ext.name, []);
3954
+ }
3955
+ for (const filePath of files) {
3956
+ const language = detectLanguage(filePath);
3957
+ if (!language) continue;
3958
+ const ext = path9.extname(filePath);
3959
+ let content;
3960
+ try {
3961
+ content = await fs8.readFile(filePath, "utf-8");
3962
+ } catch (err) {
3963
+ errors.push(
3964
+ `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3965
+ );
3966
+ continue;
3967
+ }
3968
+ const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
3969
+ for (const extractor2 of this.extractors) {
3970
+ if (!extractor2.supportedExtensions.includes(ext)) continue;
3971
+ try {
3972
+ const records = extractor2.extract(content, relativePath, language);
3973
+ recordsByExtractor.get(extractor2.name).push(...records);
3974
+ } catch (err) {
3975
+ errors.push(
3976
+ `Extractor ${extractor2.name} failed on ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
3977
+ );
3978
+ }
3979
+ }
3980
+ }
3981
+ const allCurrentIds = /* @__PURE__ */ new Set();
3982
+ for (const [extractorName, records] of recordsByExtractor) {
3983
+ await this.writeJsonl(records, outputDir, extractorName);
3984
+ for (const record of records) {
3985
+ allCurrentIds.add(record.id);
3986
+ const result = this.persistRecord(record, store);
3987
+ nodesAdded += result.nodesAdded;
3988
+ nodesUpdated += result.nodesUpdated;
3989
+ edgesAdded += result.edgesAdded;
3990
+ }
3991
+ }
3992
+ const staleCount = this.markStale(store, allCurrentIds);
3993
+ nodesUpdated += staleCount;
3994
+ return {
3995
+ nodesAdded,
3996
+ nodesUpdated,
3997
+ edgesAdded,
3998
+ edgesUpdated: 0,
3999
+ errors,
4000
+ durationMs: Date.now() - start
4001
+ };
4002
+ }
4003
+ /** Write extraction records to JSONL file. */
4004
+ async writeJsonl(records, outputDir, extractorName) {
4005
+ const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
4006
+ const lines = records.map((r) => JSON.stringify(r));
4007
+ await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
4008
+ }
4009
+ /** Create or update a graph node from an extraction record. */
4010
+ persistRecord(record, store) {
4011
+ const existing = store.getNode(record.id);
4012
+ const node = {
4013
+ id: record.id,
4014
+ type: record.nodeType,
4015
+ name: record.name,
4016
+ path: record.filePath,
4017
+ location: {
4018
+ fileId: `file:${hash(record.filePath)}`,
4019
+ startLine: record.line,
4020
+ endLine: record.line
4021
+ },
4022
+ content: record.content,
4023
+ metadata: {
4024
+ ...record.metadata,
4025
+ source: "code-extractor",
4026
+ extractor: record.extractor,
4027
+ confidence: record.confidence,
4028
+ language: record.language,
4029
+ stale: false
4030
+ }
4031
+ };
4032
+ store.addNode(node);
4033
+ const fileNodeId = `file:${hash(record.filePath)}`;
4034
+ const edgeType = EXTRACTOR_EDGE_TYPE[record.extractor] ?? "documents";
4035
+ const edge = {
4036
+ from: record.id,
4037
+ to: fileNodeId,
4038
+ type: edgeType,
4039
+ confidence: record.confidence,
4040
+ metadata: { source: "code-extractor" }
4041
+ };
4042
+ store.addEdge(edge);
4043
+ return {
4044
+ nodesAdded: existing ? 0 : 1,
4045
+ nodesUpdated: existing ? 1 : 0,
4046
+ edgesAdded: existing ? 0 : 1
4047
+ };
4048
+ }
4049
+ /**
4050
+ * Mark nodes from previous extractions that are no longer present as stale.
4051
+ * Returns the number of nodes marked stale.
4052
+ */
4053
+ markStale(store, currentIds) {
4054
+ let count = 0;
4055
+ const businessTypes = ["business_rule", "business_process", "business_term"];
4056
+ for (const type of businessTypes) {
4057
+ const nodes = store.findNodes({ type });
4058
+ for (const node of nodes) {
4059
+ if (node.metadata.source === "code-extractor" && !node.metadata.stale && !currentIds.has(node.id)) {
4060
+ store.addNode({
4061
+ ...node,
4062
+ metadata: {
4063
+ ...node.metadata,
4064
+ stale: true,
4065
+ staleAt: (/* @__PURE__ */ new Date()).toISOString()
4066
+ }
4067
+ });
4068
+ count++;
4069
+ }
4070
+ }
4071
+ }
4072
+ return count;
4073
+ }
4074
+ /** Recursively find source files, skipping common non-source directories. */
4075
+ async findSourceFiles(dir) {
4076
+ const results = [];
4077
+ let entries;
4078
+ try {
4079
+ entries = await fs8.readdir(dir, { withFileTypes: true });
4080
+ } catch {
4081
+ return results;
4082
+ }
4083
+ for (const entry of entries) {
4084
+ const fullPath = path9.join(dir, entry.name);
4085
+ if (entry.isDirectory() && !SKIP_DIRS2.has(entry.name)) {
4086
+ results.push(...await this.findSourceFiles(fullPath));
4087
+ } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
4088
+ results.push(fullPath);
4089
+ }
4090
+ }
4091
+ return results;
4092
+ }
4093
+ };
4094
+ PATTERNS = {
4095
+ typescript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
4096
+ javascript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
4097
+ python: [/def\s+(test_\w+)\s*\(/g, /"""((?:(?!""").)*?)"""/gs, /class\s+(Test\w+)/g],
4098
+ go: [/func\s+(Test\w+)\s*\(/g, /t\.Run\(\s*"([^"]+)"/g],
4099
+ rust: [/#\[test\]\s*\n\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g, /\/\/\/\s*(.+)/g],
4100
+ java: [
4101
+ /@DisplayName\s*\(\s*"([^"]+)"\s*\)/g,
4102
+ /@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
4103
+ ]
4104
+ };
4105
+ TestDescriptionExtractor = class {
4106
+ name = "test-descriptions";
4107
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4108
+ extract(content, filePath, language) {
4109
+ const records = [];
4110
+ const patterns = PATTERNS[language];
4111
+ if (!patterns) return records;
4112
+ const lines = content.split("\n");
4113
+ if (language === "typescript" || language === "javascript") {
4114
+ return this.extractJsTs(content, filePath, language, lines);
4115
+ }
4116
+ if (language === "python") {
4117
+ return this.extractPython(content, filePath, language, lines);
4118
+ }
4119
+ if (language === "go") {
4120
+ return this.extractGo(content, filePath, language, lines);
4121
+ }
4122
+ if (language === "rust") {
4123
+ return this.extractRust(content, filePath, language, lines);
4124
+ }
4125
+ if (language === "java") {
4126
+ return this.extractJava(content, filePath, language, lines);
4127
+ }
4128
+ return records;
4129
+ }
4130
+ extractJsTs(_content, filePath, language, lines) {
4131
+ const records = [];
4132
+ const describeStack = [];
4133
+ for (let i = 0; i < lines.length; i++) {
4134
+ const line = lines[i];
4135
+ const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
4136
+ if (describeMatch) {
4137
+ describeStack.push(describeMatch[2]);
4138
+ }
4139
+ const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
4140
+ if (itMatch) {
4141
+ const testName = itMatch[2];
4142
+ const fullPath = [...describeStack, testName].join(" > ");
4143
+ const patternKey = fullPath;
4144
+ records.push({
4145
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
4146
+ extractor: "test-descriptions",
4147
+ language,
4148
+ filePath,
4149
+ line: i + 1,
4150
+ nodeType: "business_rule",
4151
+ name: testName,
4152
+ content: fullPath,
4153
+ confidence: 0.7,
4154
+ metadata: {
4155
+ suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
4156
+ framework: "vitest"
4157
+ }
4158
+ });
4159
+ }
4160
+ if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
4161
+ describeStack.pop();
4162
+ }
4163
+ }
4164
+ return records;
4165
+ }
4166
+ extractPython(_content, filePath, language, lines) {
4167
+ const records = [];
4168
+ let currentClass;
4169
+ for (let i = 0; i < lines.length; i++) {
4170
+ const line = lines[i];
4171
+ const classMatch = line.match(/^class\s+(Test\w+)/);
4172
+ if (classMatch) {
4173
+ currentClass = classMatch[1];
4174
+ }
4175
+ const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
4176
+ if (funcMatch) {
4177
+ const testName = funcMatch[1];
4178
+ const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
4179
+ let docstring;
4180
+ if (i + 1 < lines.length) {
4181
+ const nextLine = lines[i + 1].trim();
4182
+ const docMatch = nextLine.match(/^"""(.+?)"""/);
4183
+ if (docMatch) {
4184
+ docstring = docMatch[1];
4185
+ }
4186
+ }
4187
+ const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
4188
+ records.push({
4189
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
4190
+ extractor: "test-descriptions",
4191
+ language,
4192
+ filePath,
4193
+ line: i + 1,
4194
+ nodeType: "business_rule",
4195
+ name: docstring ?? humanName,
4196
+ content: patternKey,
4197
+ confidence: docstring ? 0.7 : 0.5,
4198
+ metadata: {
4199
+ suite: currentClass,
4200
+ framework: "pytest",
4201
+ functionName: testName
4202
+ }
4203
+ });
4204
+ }
4205
+ if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
4206
+ currentClass = void 0;
4207
+ }
4208
+ }
4209
+ return records;
4210
+ }
4211
+ extractGo(_content, filePath, language, lines) {
4212
+ const records = [];
4213
+ let currentTest;
4214
+ for (let i = 0; i < lines.length; i++) {
4215
+ const line = lines[i];
4216
+ const funcMatch = line.match(/^func\s+(Test\w+)\s*\(/);
4217
+ if (funcMatch) {
4218
+ currentTest = funcMatch[1];
4219
+ const humanName = currentTest.replace(/^Test/, "").replace(/([A-Z])/g, " $1").trim();
4220
+ records.push({
4221
+ id: `extracted:test-descriptions:${hash(filePath + ":" + currentTest)}`,
4222
+ extractor: "test-descriptions",
4223
+ language,
4224
+ filePath,
4225
+ line: i + 1,
4226
+ nodeType: "business_rule",
4227
+ name: humanName,
4228
+ content: currentTest,
4229
+ confidence: 0.5,
4230
+ metadata: { framework: "testing" }
4231
+ });
4232
+ }
4233
+ const runMatch = line.match(/t\.Run\(\s*"([^"]+)"/);
4234
+ if (runMatch && currentTest) {
4235
+ const subtestName = runMatch[1];
4236
+ const patternKey = `${currentTest} > ${subtestName}`;
4237
+ records.push({
4238
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
4239
+ extractor: "test-descriptions",
4240
+ language,
4241
+ filePath,
4242
+ line: i + 1,
4243
+ nodeType: "business_rule",
4244
+ name: subtestName,
4245
+ content: patternKey,
4246
+ confidence: 0.7,
4247
+ metadata: {
4248
+ suite: currentTest,
4249
+ framework: "testing"
4250
+ }
4251
+ });
4252
+ }
4253
+ }
4254
+ return records;
4255
+ }
4256
+ extractRust(_content, filePath, language, lines) {
4257
+ const records = [];
4258
+ for (let i = 0; i < lines.length; i++) {
4259
+ const line = lines[i];
4260
+ if (line.trim() === "#[test]") {
4261
+ for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
4262
+ const fnLine = lines[j];
4263
+ const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
4264
+ if (fnMatch) {
4265
+ const testName = fnMatch[1];
4266
+ const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
4267
+ let docComment;
4268
+ if (i > 0) {
4269
+ const prevLine = lines[i - 1].trim();
4270
+ const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
4271
+ if (docMatch) {
4272
+ docComment = docMatch[1];
4273
+ }
4274
+ }
4275
+ records.push({
4276
+ id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
4277
+ extractor: "test-descriptions",
4278
+ language,
4279
+ filePath,
4280
+ line: j + 1,
4281
+ nodeType: "business_rule",
4282
+ name: docComment ?? humanName,
4283
+ content: testName,
4284
+ confidence: docComment ? 0.7 : 0.5,
4285
+ metadata: { framework: "rust-test" }
4286
+ });
4287
+ break;
4288
+ }
4289
+ }
4290
+ }
4291
+ }
4292
+ return records;
4293
+ }
4294
+ extractJava(_content, filePath, language, lines) {
4295
+ const records = [];
4296
+ for (let i = 0; i < lines.length; i++) {
4297
+ const line = lines[i];
4298
+ const testMatch = line.match(/@Test\s*$/);
4299
+ if (testMatch) {
4300
+ let displayName;
4301
+ for (let k = Math.max(0, i - 3); k < i; k++) {
4302
+ const prevLine = lines[k];
4303
+ const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
4304
+ if (dm) displayName = dm[1];
4305
+ }
4306
+ for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
4307
+ const scanLine = lines[j];
4308
+ const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
4309
+ if (adjacentDisplay) {
4310
+ displayName = adjacentDisplay[1];
4311
+ continue;
4312
+ }
4313
+ const methodMatch = scanLine.match(
4314
+ /^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
4315
+ );
4316
+ if (methodMatch) {
4317
+ const methodName = methodMatch[1];
4318
+ const name = displayName ?? methodName;
4319
+ const patternKey = displayName ?? methodName;
4320
+ records.push({
4321
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
4322
+ extractor: "test-descriptions",
4323
+ language,
4324
+ filePath,
4325
+ line: j + 1,
4326
+ nodeType: "business_rule",
4327
+ name,
4328
+ content: patternKey,
4329
+ confidence: displayName ? 0.7 : 0.5,
4330
+ metadata: { framework: "junit5" }
4331
+ });
4332
+ break;
4333
+ }
4334
+ }
4335
+ }
4336
+ }
4337
+ return records;
4338
+ }
4339
+ };
4340
+ EnumConstantExtractor = class {
4341
+ name = "enum-constants";
4342
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4343
+ extract(content, filePath, language) {
4344
+ switch (language) {
4345
+ case "typescript":
4346
+ return this.extractTypeScript(content, filePath, language);
4347
+ case "javascript":
4348
+ return this.extractJavaScript(content, filePath, language);
4349
+ case "python":
4350
+ return this.extractPython(content, filePath, language);
4351
+ case "go":
4352
+ return this.extractGo(content, filePath, language);
4353
+ case "rust":
4354
+ return this.extractRust(content, filePath, language);
4355
+ case "java":
4356
+ return this.extractJava(content, filePath, language);
4357
+ }
4358
+ }
4359
+ extractTypeScript(content, filePath, language) {
4360
+ const records = [];
4361
+ const lines = content.split("\n");
4362
+ for (let i = 0; i < lines.length; i++) {
4363
+ const line = lines[i];
4364
+ const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
4365
+ if (enumMatch) {
4366
+ const enumName = enumMatch[1];
4367
+ const members = this.collectEnumMembers(lines, i + 1);
4368
+ records.push({
4369
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4370
+ extractor: "enum-constants",
4371
+ language,
4372
+ filePath,
4373
+ line: i + 1,
4374
+ nodeType: "business_term",
4375
+ name: enumName,
4376
+ content: `enum ${enumName} { ${members.join(", ")} }`,
4377
+ confidence: 0.8,
4378
+ metadata: { kind: "enum", members }
4379
+ });
4380
+ }
4381
+ const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
4382
+ if (constMatch && content.includes("as const")) {
4383
+ const blockEnd = this.findClosingBrace(lines, i);
4384
+ const block = lines.slice(i, blockEnd + 1).join("\n");
4385
+ if (block.includes("as const")) {
4386
+ const constName = constMatch[1];
4387
+ const members = this.collectObjectKeys(lines, i + 1);
4388
+ records.push({
4389
+ id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
4390
+ extractor: "enum-constants",
4391
+ language,
4392
+ filePath,
4393
+ line: i + 1,
4394
+ nodeType: "business_term",
4395
+ name: constName,
4396
+ content: `const ${constName} = { ${members.join(", ")} } as const`,
4397
+ confidence: 0.8,
4398
+ metadata: { kind: "as-const", members }
4399
+ });
4400
+ }
4401
+ }
4402
+ const unionMatch = line.match(
4403
+ /(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
4404
+ );
4405
+ if (unionMatch) {
4406
+ const typeName = unionMatch[1];
4407
+ const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
4408
+ records.push({
4409
+ id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
4410
+ extractor: "enum-constants",
4411
+ language,
4412
+ filePath,
4413
+ line: i + 1,
4414
+ nodeType: "business_term",
4415
+ name: typeName,
4416
+ content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
4417
+ confidence: 0.7,
4418
+ metadata: { kind: "union-type", members: values }
4419
+ });
4420
+ }
4421
+ }
4422
+ return records;
4423
+ }
4424
+ extractJavaScript(content, filePath, language) {
4425
+ const records = [];
4426
+ const lines = content.split("\n");
4427
+ for (let i = 0; i < lines.length; i++) {
4428
+ const line = lines[i];
4429
+ const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
4430
+ if (freezeMatch) {
4431
+ const name = freezeMatch[1];
4432
+ const members = this.collectObjectKeys(lines, i + 1);
4433
+ records.push({
4434
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
4435
+ extractor: "enum-constants",
4436
+ language,
4437
+ filePath,
4438
+ line: i + 1,
4439
+ nodeType: "business_term",
4440
+ name,
4441
+ content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
4442
+ confidence: 0.8,
4443
+ metadata: { kind: "frozen-object", members }
4444
+ });
4445
+ }
4446
+ const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
4447
+ if (constMatch && !freezeMatch) {
4448
+ const name = constMatch[1];
4449
+ const members = this.collectObjectKeys(lines, i + 1);
4450
+ if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
4451
+ records.push({
4452
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
4453
+ extractor: "enum-constants",
4454
+ language,
4455
+ filePath,
4456
+ line: i + 1,
4457
+ nodeType: "business_term",
4458
+ name,
4459
+ content: `const ${name} = { ${members.join(", ")} }`,
4460
+ confidence: 0.6,
4461
+ metadata: { kind: "const-object", members }
4462
+ });
4463
+ }
4464
+ }
4465
+ }
4466
+ return records;
4467
+ }
4468
+ extractPython(content, filePath, language) {
4469
+ const records = [];
4470
+ const lines = content.split("\n");
4471
+ for (let i = 0; i < lines.length; i++) {
4472
+ const line = lines[i];
4473
+ const enumMatch = line.match(
4474
+ /^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
4475
+ );
4476
+ if (enumMatch) {
4477
+ const enumName = enumMatch[1];
4478
+ const members = this.collectPythonEnumMembers(lines, i + 1);
4479
+ records.push({
4480
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4481
+ extractor: "enum-constants",
4482
+ language,
4483
+ filePath,
4484
+ line: i + 1,
4485
+ nodeType: "business_term",
4486
+ name: enumName,
4487
+ content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
4488
+ confidence: 0.8,
4489
+ metadata: { kind: "enum", members }
4490
+ });
4491
+ }
4492
+ const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
4493
+ if (literalMatch) {
4494
+ const typeName = literalMatch[1];
4495
+ const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
4496
+ records.push({
4497
+ id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
4498
+ extractor: "enum-constants",
4499
+ language,
4500
+ filePath,
4501
+ line: i + 1,
4502
+ nodeType: "business_term",
4503
+ name: typeName,
4504
+ content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
4505
+ confidence: 0.7,
4506
+ metadata: { kind: "literal-type", members: values }
4507
+ });
4508
+ }
4509
+ }
4510
+ return records;
4511
+ }
4512
+ extractGo(content, filePath, language) {
4513
+ const records = [];
4514
+ const lines = content.split("\n");
4515
+ for (let i = 0; i < lines.length; i++) {
4516
+ const line = lines[i];
4517
+ const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
4518
+ if (typeMatch) {
4519
+ }
4520
+ const constBlockMatch = line.match(/^const\s*\(/);
4521
+ if (constBlockMatch) {
4522
+ const consts = [];
4523
+ let typeName;
4524
+ for (let j = i + 1; j < lines.length; j++) {
4525
+ const constLine = lines[j].trim();
4526
+ if (constLine === ")") break;
4527
+ if (constLine === "" || constLine.startsWith("//")) continue;
4528
+ const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
4529
+ if (iotaMatch) {
4530
+ typeName = iotaMatch[2];
4531
+ consts.push(iotaMatch[1]);
4532
+ continue;
4533
+ }
4534
+ const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
4535
+ if (typedMatch) {
4536
+ typeName = typeName ?? typedMatch[2];
4537
+ consts.push(typedMatch[1]);
4538
+ continue;
4539
+ }
4540
+ const bareMatch = constLine.match(/^(\w+)\s*$/);
4541
+ if (bareMatch) {
4542
+ consts.push(bareMatch[1]);
4543
+ }
4544
+ }
4545
+ if (consts.length > 0) {
4546
+ const name = typeName ?? consts[0];
4547
+ records.push({
4548
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
4549
+ extractor: "enum-constants",
4550
+ language,
4551
+ filePath,
4552
+ line: i + 1,
4553
+ nodeType: "business_term",
4554
+ name,
4555
+ content: `const ( ${consts.join(", ")} )`,
4556
+ confidence: typeName ? 0.8 : 0.6,
4557
+ metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
4558
+ });
4559
+ }
4560
+ }
4561
+ }
4562
+ return records;
4563
+ }
4564
+ extractRust(content, filePath, language) {
4565
+ const records = [];
4566
+ const lines = content.split("\n");
4567
+ for (let i = 0; i < lines.length; i++) {
4568
+ const line = lines[i];
4569
+ const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
4570
+ if (enumMatch) {
4571
+ const enumName = enumMatch[1];
4572
+ const members = this.collectRustEnumVariants(lines, i + 1);
4573
+ records.push({
4574
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4575
+ extractor: "enum-constants",
4576
+ language,
4577
+ filePath,
4578
+ line: i + 1,
4579
+ nodeType: "business_term",
4580
+ name: enumName,
4581
+ content: `enum ${enumName} { ${members.join(", ")} }`,
4582
+ confidence: 0.8,
4583
+ metadata: { kind: "enum", members }
4584
+ });
4585
+ }
4586
+ }
4587
+ return records;
4588
+ }
4589
+ extractJava(content, filePath, language) {
4590
+ const records = [];
4591
+ const lines = content.split("\n");
4592
+ for (let i = 0; i < lines.length; i++) {
4593
+ const line = lines[i];
4594
+ const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
4595
+ if (enumMatch) {
4596
+ const enumName = enumMatch[1];
4597
+ const members = this.collectJavaEnumConstants(lines, i + 1);
4598
+ records.push({
4599
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4600
+ extractor: "enum-constants",
4601
+ language,
4602
+ filePath,
4603
+ line: i + 1,
4604
+ nodeType: "business_term",
4605
+ name: enumName,
4606
+ content: `enum ${enumName} { ${members.join(", ")} }`,
4607
+ confidence: 0.8,
4608
+ metadata: { kind: "enum", members }
4609
+ });
4610
+ }
4611
+ }
4612
+ return records;
4613
+ }
4614
+ // --- Helper methods ---
4615
+ collectEnumMembers(lines, startLine) {
4616
+ const members = [];
4617
+ for (let i = startLine; i < lines.length; i++) {
4618
+ const line = lines[i].trim();
4619
+ if (line === "}") break;
4620
+ if (line === "" || line.startsWith("//")) continue;
4621
+ const match = line.match(/^(\w+)/);
4622
+ if (match) members.push(match[1]);
4623
+ }
4624
+ return members;
4625
+ }
4626
+ collectObjectKeys(lines, startLine) {
4627
+ const keys = [];
4628
+ for (let i = startLine; i < lines.length; i++) {
4629
+ const line = lines[i].trim();
4630
+ if (line.startsWith("}")) break;
4631
+ if (line === "" || line.startsWith("//")) continue;
4632
+ const match = line.match(/^(\w+)\s*:/);
4633
+ if (match) keys.push(match[1]);
4634
+ }
4635
+ return keys;
4636
+ }
4637
+ findClosingBrace(lines, startLine) {
4638
+ let depth = 0;
4639
+ for (let i = startLine; i < lines.length; i++) {
4640
+ for (const ch of lines[i]) {
4641
+ if (ch === "{") depth++;
4642
+ if (ch === "}") {
4643
+ depth--;
4644
+ if (depth === 0) return i;
4645
+ }
4646
+ }
4647
+ }
4648
+ return lines.length - 1;
4649
+ }
4650
+ collectPythonEnumMembers(lines, startLine) {
4651
+ const members = [];
4652
+ for (let i = startLine; i < lines.length; i++) {
4653
+ const line = lines[i];
4654
+ if (/^\S/.test(line) && line.trim() !== "") break;
4655
+ const match = line.match(/^\s+(\w+)\s*=/);
4656
+ if (match) members.push(match[1]);
4657
+ }
4658
+ return members;
4659
+ }
4660
+ collectRustEnumVariants(lines, startLine) {
4661
+ const variants = [];
4662
+ for (let i = startLine; i < lines.length; i++) {
4663
+ const line = lines[i].trim();
4664
+ if (line === "}") break;
4665
+ if (line === "" || line.startsWith("//")) continue;
4666
+ const match = line.match(/^(\w+)/);
4667
+ if (match) variants.push(match[1]);
4668
+ }
4669
+ return variants;
4670
+ }
4671
+ collectJavaEnumConstants(lines, startLine) {
4672
+ const constants = [];
4673
+ for (let i = startLine; i < lines.length; i++) {
4674
+ const line = lines[i].trim();
4675
+ if (line === "}") break;
4676
+ if (line === "" || line.startsWith("//") || line.startsWith("/*")) continue;
4677
+ if (line.match(/^\s*(?:private|public|protected|static)/)) break;
4678
+ const match = line.match(/^(\w+)[\s(,;]/);
4679
+ if (match) constants.push(match[1]);
4680
+ const singleMatch = line.match(/^(\w+)$/);
4681
+ if (singleMatch) constants.push(singleMatch[1]);
4682
+ }
4683
+ return constants;
4684
+ }
4685
+ };
4686
+ ValidationRuleExtractor = class {
4687
+ name = "validation-rules";
4688
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4689
+ extract(content, filePath, language) {
4690
+ switch (language) {
4691
+ case "typescript":
4692
+ case "javascript":
4693
+ return this.extractZod(content, filePath, language);
4694
+ case "python":
4695
+ return this.extractPydantic(content, filePath, language);
4696
+ case "go":
4697
+ return this.extractGoValidate(content, filePath, language);
4698
+ case "rust":
4699
+ return this.extractRustValidate(content, filePath, language);
4700
+ case "java":
4701
+ return this.extractBeanValidation(content, filePath, language);
4702
+ }
4703
+ }
4704
+ extractZod(content, filePath, language) {
4705
+ const records = [];
4706
+ const lines = content.split("\n");
4707
+ for (let i = 0; i < lines.length; i++) {
4708
+ const line = lines[i];
4709
+ const schemaMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*z\.object\s*\(\s*\{/);
4710
+ if (schemaMatch) {
4711
+ const schemaName = schemaMatch[1];
4712
+ const constraints = this.collectZodConstraints(lines, i + 1);
4713
+ records.push({
4714
+ id: `extracted:validation-rules:${hash(filePath + ":" + schemaName)}`,
4715
+ extractor: "validation-rules",
4716
+ language,
4717
+ filePath,
4718
+ line: i + 1,
4719
+ nodeType: "business_rule",
4720
+ name: schemaName,
4721
+ content: `${schemaName}: ${constraints.join("; ")}`,
4722
+ confidence: 0.8,
4723
+ metadata: { kind: "zod-schema", constraints, framework: "zod" }
4724
+ });
4725
+ }
4726
+ }
4727
+ return records;
4728
+ }
4729
+ collectZodConstraints(lines, startLine) {
4730
+ const constraints = [];
4731
+ for (let i = startLine; i < lines.length; i++) {
4732
+ const line = lines[i].trim();
4733
+ if (line.startsWith("}") || line.startsWith(")")) break;
4734
+ if (line === "" || line.startsWith("//")) continue;
4735
+ const fieldMatch = line.match(/(\w+)\s*:\s*z\.(.+?)(?:,?\s*$)/);
4736
+ if (fieldMatch) {
4737
+ const fieldName = fieldMatch[1];
4738
+ const chain = fieldMatch[2];
4739
+ constraints.push(`${fieldName}: ${chain}`);
4740
+ }
4741
+ }
4742
+ return constraints;
4743
+ }
4744
+ extractPydantic(content, filePath, language) {
4745
+ const records = [];
4746
+ const lines = content.split("\n");
4747
+ for (let i = 0; i < lines.length; i++) {
4748
+ const line = lines[i];
4749
+ const modelMatch = line.match(/^class\s+(\w+)\s*\(\s*BaseModel\s*\)/);
4750
+ if (modelMatch) {
4751
+ const modelName = modelMatch[1];
4752
+ const constraints = this.collectPydanticConstraints(lines, i + 1);
4753
+ records.push({
4754
+ id: `extracted:validation-rules:${hash(filePath + ":" + modelName)}`,
4755
+ extractor: "validation-rules",
4756
+ language,
4757
+ filePath,
4758
+ line: i + 1,
4759
+ nodeType: "business_rule",
4760
+ name: modelName,
4761
+ content: `${modelName}: ${constraints.join("; ")}`,
4762
+ confidence: 0.8,
4763
+ metadata: { kind: "pydantic-model", constraints, framework: "pydantic" }
4764
+ });
4765
+ }
4766
+ }
4767
+ return records;
4768
+ }
4769
+ collectPydanticConstraints(lines, startLine) {
4770
+ const constraints = [];
4771
+ for (let i = startLine; i < lines.length; i++) {
4772
+ const line = lines[i];
4773
+ if (/^\S/.test(line) && line.trim() !== "") break;
4774
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
4775
+ const fieldMatch = line.match(/^\s+(\w+)\s*:\s*(.+?)(?:\s*$)/);
4776
+ if (fieldMatch) {
4777
+ constraints.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
4778
+ }
4779
+ }
4780
+ return constraints;
4781
+ }
4782
+ extractGoValidate(content, filePath, language) {
4783
+ const records = [];
4784
+ const lines = content.split("\n");
4785
+ for (let i = 0; i < lines.length; i++) {
4786
+ const line = lines[i];
4787
+ const structMatch = line.match(/^type\s+(\w+)\s+struct\s*\{/);
4788
+ if (structMatch) {
4789
+ const structName = structMatch[1];
4790
+ const constraints = this.collectGoValidateTags(lines, i + 1);
4791
+ if (constraints.length > 0) {
4792
+ records.push({
4793
+ id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
4794
+ extractor: "validation-rules",
4795
+ language,
4796
+ filePath,
4797
+ line: i + 1,
4798
+ nodeType: "business_rule",
4799
+ name: structName,
4800
+ content: `${structName}: ${constraints.join("; ")}`,
4801
+ confidence: 0.8,
4802
+ metadata: { kind: "struct-tags", constraints, framework: "go-playground/validator" }
4803
+ });
4804
+ }
4805
+ }
4806
+ }
4807
+ return records;
4808
+ }
4809
+ collectGoValidateTags(lines, startLine) {
4810
+ const constraints = [];
4811
+ for (let i = startLine; i < lines.length; i++) {
4812
+ const line = lines[i].trim();
4813
+ if (line === "}") break;
4814
+ if (line === "" || line.startsWith("//")) continue;
4815
+ const tagMatch = line.match(/(\w+)\s+\S+\s+`[^`]*validate:"([^"]+)"[^`]*`/);
4816
+ if (tagMatch) {
4817
+ constraints.push(`${tagMatch[1]}: ${tagMatch[2]}`);
4818
+ }
4819
+ }
4820
+ return constraints;
4821
+ }
4822
+ extractRustValidate(content, filePath, language) {
4823
+ const records = [];
4824
+ const lines = content.split("\n");
4825
+ for (let i = 0; i < lines.length; i++) {
4826
+ const line = lines[i];
4827
+ const deriveMatch = line.match(/#\[derive\([^)]*Validate[^)]*\)\]/);
4828
+ if (deriveMatch) {
4829
+ for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
4830
+ const structLine = lines[j];
4831
+ const structMatch = structLine.match(/^(?:pub\s+)?struct\s+(\w+)/);
4832
+ if (structMatch) {
4833
+ const structName = structMatch[1];
4834
+ const constraints = this.collectRustValidateAttrs(lines, j + 1);
4835
+ if (constraints.length > 0) {
4836
+ records.push({
4837
+ id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
4838
+ extractor: "validation-rules",
4839
+ language,
4840
+ filePath,
4841
+ line: j + 1,
4842
+ nodeType: "business_rule",
4843
+ name: structName,
4844
+ content: `${structName}: ${constraints.join("; ")}`,
4845
+ confidence: 0.8,
4846
+ metadata: { kind: "validate-derive", constraints, framework: "validator" }
4847
+ });
4848
+ }
4849
+ break;
4850
+ }
4851
+ }
4852
+ }
4853
+ }
4854
+ return records;
4855
+ }
4856
+ collectRustValidateAttrs(lines, startLine) {
4857
+ const constraints = [];
4858
+ let pendingValidate;
4859
+ for (let i = startLine; i < lines.length; i++) {
4860
+ const line = lines[i].trim();
4861
+ if (line === "}") break;
4862
+ const attrMatch = line.match(/#\[validate\((.+?)\)\]/);
4863
+ if (attrMatch) {
4864
+ pendingValidate = attrMatch[1];
4865
+ continue;
4866
+ }
4867
+ if (pendingValidate) {
4868
+ const fieldMatch = line.match(/(?:pub\s+)?(\w+)\s*:/);
4869
+ if (fieldMatch) {
4870
+ constraints.push(`${fieldMatch[1]}: ${pendingValidate}`);
4871
+ pendingValidate = void 0;
4872
+ }
4873
+ }
4874
+ }
4875
+ return constraints;
4876
+ }
4877
+ extractBeanValidation(content, filePath, language) {
4878
+ const records = [];
4879
+ const lines = content.split("\n");
4880
+ const classRanges = this.findJavaClasses(lines);
4881
+ for (const { name: className, startLine, endLine } of classRanges) {
4882
+ const constraints = this.collectBeanConstraints(lines, startLine, endLine);
4883
+ if (constraints.length > 0) {
4884
+ records.push({
4885
+ id: `extracted:validation-rules:${hash(filePath + ":" + className)}`,
4886
+ extractor: "validation-rules",
4887
+ language,
4888
+ filePath,
4889
+ line: startLine + 1,
4890
+ nodeType: "business_rule",
4891
+ name: className,
4892
+ content: `${className}: ${constraints.join("; ")}`,
4893
+ confidence: 0.8,
4894
+ metadata: { kind: "bean-validation", constraints, framework: "javax.validation" }
4895
+ });
4896
+ }
4897
+ }
4898
+ return records;
4899
+ }
4900
+ findJavaClasses(lines) {
4901
+ const classes = [];
4902
+ for (let i = 0; i < lines.length; i++) {
4903
+ const line = lines[i];
4904
+ const match = line.match(/(?:public\s+|private\s+|protected\s+)?class\s+(\w+)/);
4905
+ if (match) {
4906
+ const endLine = this.findClosingBrace(lines, i);
4907
+ classes.push({ name: match[1], startLine: i, endLine });
4908
+ }
4909
+ }
4910
+ return classes;
4911
+ }
4912
+ findClosingBrace(lines, startLine) {
4913
+ let depth = 0;
4914
+ for (let i = startLine; i < lines.length; i++) {
4915
+ for (const ch of lines[i]) {
4916
+ if (ch === "{") depth++;
4917
+ if (ch === "}") {
4918
+ depth--;
4919
+ if (depth === 0) return i;
4920
+ }
4921
+ }
4922
+ }
4923
+ return lines.length - 1;
4924
+ }
4925
+ collectBeanConstraints(lines, startLine, endLine) {
4926
+ const constraints = [];
4927
+ const validationAnnotations = [
4928
+ "@NotNull",
4929
+ "@NotBlank",
4930
+ "@NotEmpty",
4931
+ "@Size",
4932
+ "@Min",
4933
+ "@Max",
4934
+ "@DecimalMin",
4935
+ "@DecimalMax",
4936
+ "@Email",
4937
+ "@Pattern",
4938
+ "@Positive",
4939
+ "@Negative",
4940
+ "@Past",
4941
+ "@Future"
4942
+ ];
4943
+ let pendingAnnotations = [];
4944
+ for (let i = startLine + 1; i < endLine; i++) {
4945
+ const line = lines[i].trim();
4946
+ for (const anno of validationAnnotations) {
4947
+ if (line.startsWith(anno)) {
4948
+ pendingAnnotations.push(line.replace(/;?\s*$/, ""));
4949
+ }
4950
+ }
4951
+ if (pendingAnnotations.length > 0) {
4952
+ const fieldMatch = line.match(
4953
+ /(?:private|public|protected)\s+(?:[\w<>[\]]+)\s+(\w+)\s*[;=]/
4954
+ );
4955
+ if (fieldMatch) {
4956
+ constraints.push(`${fieldMatch[1]}: ${pendingAnnotations.join(", ")}`);
4957
+ pendingAnnotations = [];
4958
+ }
4959
+ }
4960
+ }
4961
+ return constraints;
4962
+ }
4963
+ };
4964
+ ApiPathExtractor = class {
4965
+ name = "api-paths";
4966
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4967
+ extract(content, filePath, language) {
4968
+ switch (language) {
4969
+ case "typescript":
4970
+ case "javascript":
4971
+ return this.extractJsTs(content, filePath, language);
4972
+ case "python":
4973
+ return this.extractPython(content, filePath, language);
4974
+ case "go":
4975
+ return this.extractGo(content, filePath, language);
4976
+ case "rust":
4977
+ return this.extractRust(content, filePath, language);
4978
+ case "java":
4979
+ return this.extractJava(content, filePath, language);
4980
+ }
4981
+ }
4982
+ extractJsTs(content, filePath, language) {
4983
+ const records = [];
4984
+ const lines = content.split("\n");
4985
+ const routePattern = /(?:app|router|server|fastify)\.(get|post|put|patch|delete|head|options)\s*\(\s*(['"`])([^'"`]+)\2/i;
4986
+ for (let i = 0; i < lines.length; i++) {
4987
+ const line = lines[i];
4988
+ const match = line.match(routePattern);
4989
+ if (match) {
4990
+ const method = match[1].toUpperCase();
4991
+ const routePath = match[3];
4992
+ const patternKey = `${method} ${routePath}`;
4993
+ records.push({
4994
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4995
+ extractor: "api-paths",
4996
+ language,
4997
+ filePath,
4998
+ line: i + 1,
4999
+ nodeType: "business_process",
5000
+ name: patternKey,
5001
+ content: `${method} ${routePath}`,
5002
+ confidence: 0.9,
5003
+ metadata: { method, path: routePath, framework: "express" }
5004
+ });
5005
+ }
5006
+ }
5007
+ return records;
5008
+ }
5009
+ extractPython(content, filePath, language) {
5010
+ const records = [];
5011
+ const lines = content.split("\n");
5012
+ const decoratorPattern = /@(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*["']([^"']+)["']/i;
5013
+ const flaskPattern = /@(?:app|blueprint)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/i;
5014
+ for (let i = 0; i < lines.length; i++) {
5015
+ const line = lines[i];
5016
+ const fastApiMatch = line.match(decoratorPattern);
5017
+ if (fastApiMatch) {
5018
+ const method = fastApiMatch[1].toUpperCase();
5019
+ const routePath = fastApiMatch[2];
5020
+ const patternKey = `${method} ${routePath}`;
5021
+ records.push({
5022
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
5023
+ extractor: "api-paths",
5024
+ language,
5025
+ filePath,
5026
+ line: i + 1,
5027
+ nodeType: "business_process",
5028
+ name: patternKey,
5029
+ content: `${method} ${routePath}`,
5030
+ confidence: 0.9,
5031
+ metadata: { method, path: routePath, framework: "fastapi" }
5032
+ });
5033
+ continue;
5034
+ }
5035
+ const flaskMatch = line.match(flaskPattern);
5036
+ if (flaskMatch) {
5037
+ const routePath = flaskMatch[1];
5038
+ const methods = flaskMatch[2] ? flaskMatch[2].split(",").map((m) => m.trim().replace(/["']/g, "")) : ["GET"];
5039
+ for (const method of methods) {
5040
+ const patternKey = `${method} ${routePath}`;
5041
+ records.push({
5042
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
5043
+ extractor: "api-paths",
5044
+ language,
5045
+ filePath,
5046
+ line: i + 1,
5047
+ nodeType: "business_process",
5048
+ name: patternKey,
5049
+ content: `${method} ${routePath}`,
5050
+ confidence: 0.9,
5051
+ metadata: { method, path: routePath, framework: "flask" }
5052
+ });
5053
+ }
5054
+ }
5055
+ }
5056
+ return records;
5057
+ }
5058
+ extractGo(content, filePath, language) {
5059
+ const records = [];
5060
+ const lines = content.split("\n");
5061
+ const ginPattern = /(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"/i;
5062
+ const httpPattern = /http\.HandleFunc\s*\(\s*"([^"]+)"/;
5063
+ for (let i = 0; i < lines.length; i++) {
5064
+ const line = lines[i];
5065
+ const ginMatch = line.match(ginPattern);
5066
+ if (ginMatch) {
5067
+ const method = ginMatch[1].toUpperCase();
5068
+ const routePath = ginMatch[2];
5069
+ const patternKey = `${method} ${routePath}`;
5070
+ records.push({
5071
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
5072
+ extractor: "api-paths",
5073
+ language,
5074
+ filePath,
5075
+ line: i + 1,
5076
+ nodeType: "business_process",
5077
+ name: patternKey,
5078
+ content: `${method} ${routePath}`,
5079
+ confidence: 0.9,
5080
+ metadata: { method, path: routePath, framework: "gin" }
5081
+ });
5082
+ continue;
5083
+ }
5084
+ const httpMatch = line.match(httpPattern);
5085
+ if (httpMatch) {
5086
+ const routePath = httpMatch[1];
5087
+ const patternKey = `ANY ${routePath}`;
5088
+ records.push({
5089
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
5090
+ extractor: "api-paths",
5091
+ language,
5092
+ filePath,
5093
+ line: i + 1,
5094
+ nodeType: "business_process",
5095
+ name: patternKey,
5096
+ content: `ANY ${routePath}`,
5097
+ confidence: 0.6,
5098
+ metadata: { method: "ANY", path: routePath, framework: "net/http" }
5099
+ });
5100
+ }
5101
+ }
5102
+ return records;
5103
+ }
5104
+ extractRust(content, filePath, language) {
5105
+ const records = [];
5106
+ const lines = content.split("\n");
5107
+ const actixPattern = /#\[(get|post|put|patch|delete|head|options)\s*\(\s*"([^"]+)"\s*\)\s*\]/i;
5108
+ for (let i = 0; i < lines.length; i++) {
5109
+ const line = lines[i];
5110
+ const match = line.match(actixPattern);
5111
+ if (match) {
5112
+ const method = match[1].toUpperCase();
5113
+ const routePath = match[2];
5114
+ const patternKey = `${method} ${routePath}`;
5115
+ records.push({
5116
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
5117
+ extractor: "api-paths",
5118
+ language,
5119
+ filePath,
5120
+ line: i + 1,
5121
+ nodeType: "business_process",
5122
+ name: patternKey,
5123
+ content: `${method} ${routePath}`,
5124
+ confidence: 0.9,
5125
+ metadata: { method, path: routePath, framework: "actix" }
5126
+ });
5127
+ }
5128
+ }
5129
+ return records;
5130
+ }
5131
+ extractJava(content, filePath, language) {
5132
+ const records = [];
5133
+ const lines = content.split("\n");
5134
+ const springPattern = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/;
5135
+ let basePath = "";
5136
+ for (const line of lines) {
5137
+ const baseMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\)/);
5138
+ if (baseMatch && line.match(/class\s/) === null) {
5139
+ basePath = baseMatch[1];
5140
+ }
5141
+ }
5142
+ for (let i = 0; i < lines.length; i++) {
5143
+ const line = lines[i];
5144
+ const match = line.match(springPattern);
5145
+ if (match) {
5146
+ const annotation = match[1];
5147
+ const routePath = basePath + match[2];
5148
+ const methodMap = {
5149
+ GetMapping: "GET",
5150
+ PostMapping: "POST",
5151
+ PutMapping: "PUT",
5152
+ PatchMapping: "PATCH",
5153
+ DeleteMapping: "DELETE",
5154
+ RequestMapping: "ANY"
5155
+ };
5156
+ const method = methodMap[annotation] ?? "ANY";
5157
+ const patternKey = `${method} ${routePath}`;
5158
+ records.push({
5159
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
5160
+ extractor: "api-paths",
5161
+ language,
5162
+ filePath,
5163
+ line: i + 1,
5164
+ nodeType: "business_process",
5165
+ name: patternKey,
5166
+ content: `${method} ${routePath}`,
5167
+ confidence: 0.9,
5168
+ metadata: { method, path: routePath, framework: "spring" }
5169
+ });
5170
+ }
5171
+ }
5172
+ return records;
5173
+ }
5174
+ };
5175
+ ALL_EXTRACTORS = [
5176
+ new TestDescriptionExtractor(),
5177
+ new EnumConstantExtractor(),
5178
+ new ValidationRuleExtractor(),
5179
+ new ApiPathExtractor()
5180
+ ];
5181
+ ImageAnalysisExtractor = class {
5182
+ provider;
5183
+ /** Maximum file size in bytes. Reserved for future file-size filtering. */
5184
+ maxFileSizeBytes;
5185
+ constructor(options) {
5186
+ this.provider = options.analysisProvider;
5187
+ this.maxFileSizeBytes = (options.maxFileSizeMB ?? 10) * 1024 * 1024;
5188
+ }
5189
+ async analyze(store, imagePaths) {
5190
+ const start = Date.now();
5191
+ let nodesAdded = 0;
5192
+ let edgesAdded = 0;
5193
+ const errors = [];
5194
+ for (const imagePath of imagePaths) {
5195
+ try {
5196
+ const counts = await this.processImage(store, imagePath);
5197
+ nodesAdded += counts.nodes;
5198
+ edgesAdded += counts.edges;
5199
+ } catch (err) {
5200
+ errors.push(
5201
+ `Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
5202
+ );
5203
+ }
5204
+ }
5205
+ return {
5206
+ nodesAdded,
5207
+ nodesUpdated: 0,
5208
+ edgesAdded,
5209
+ edgesUpdated: 0,
5210
+ errors,
5211
+ durationMs: Date.now() - start
5212
+ };
5213
+ }
5214
+ /** Analyze a single image and add annotation + concept nodes to the store. */
5215
+ async processImage(store, imagePath) {
5216
+ const response = await this.provider.analyze({
5217
+ prompt: "Analyze this image and provide a structured description of its visual contents.",
5218
+ systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
5219
+ responseSchema: {},
5220
+ // Schema handled by provider
5221
+ maxTokens: 1e3
5222
+ });
5223
+ const result = response.result;
5224
+ const annotationId = `img:${hash(imagePath)}`;
5225
+ let nodes = 0;
5226
+ let edges = 0;
5227
+ store.addNode({
5228
+ id: annotationId,
5229
+ type: "image_annotation",
5230
+ name: result.description.slice(0, 200),
5231
+ path: imagePath,
5232
+ content: result.description,
5233
+ hash: hash(result.description),
5234
+ metadata: {
5235
+ source: "image-analysis",
5236
+ detectedElements: result.detectedElements,
5237
+ extractedText: result.extractedText,
5238
+ designPatterns: result.designPatterns,
5239
+ accessibilityNotes: result.accessibilityNotes,
5240
+ model: response.model
5241
+ }
5242
+ });
5243
+ nodes++;
5244
+ edges += this.linkToFileNode(store, annotationId, imagePath);
5245
+ for (const pattern of result.designPatterns) {
5246
+ edges += this.addDesignPatternConcept(store, annotationId, imagePath, pattern);
5247
+ nodes++;
5248
+ }
5249
+ return { nodes, edges };
5250
+ }
5251
+ /** Link annotation to its source file node if it exists. Returns 1 if linked, 0 otherwise. */
5252
+ linkToFileNode(store, annotationId, imagePath) {
5253
+ const fileNode = store.findNodes({ type: "file" }).find((n) => n.path === imagePath);
5254
+ if (!fileNode) return 0;
5255
+ store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
5256
+ return 1;
5257
+ }
5258
+ /** Create a business_concept node for a design pattern and link it. Returns edges added. */
5259
+ addDesignPatternConcept(store, annotationId, imagePath, pattern) {
5260
+ const conceptId = `img:concept:${hash(pattern + imagePath)}`;
5261
+ store.addNode({
5262
+ id: conceptId,
5263
+ type: "business_concept",
5264
+ name: pattern,
5265
+ content: `Design pattern detected in ${imagePath}: ${pattern}`,
5266
+ hash: hash(pattern),
5267
+ metadata: {
5268
+ source: "image-analysis",
5269
+ sourceImage: imagePath,
5270
+ domain: "design"
5271
+ }
5272
+ });
5273
+ store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
5274
+ return 1;
5275
+ }
5276
+ };
5277
+ KNOWLEDGE_NODE_TYPES = [
5278
+ "business_fact",
5279
+ "business_rule",
5280
+ "business_process",
5281
+ "business_term",
5282
+ "business_concept",
5283
+ "business_metric",
5284
+ "design_token",
5285
+ "design_constraint",
5286
+ "aesthetic_intent",
5287
+ "image_annotation"
5288
+ ];
5289
+ SIMILARITY_THRESHOLD = 0.8;
5290
+ SEVERITY_MAP = {
5291
+ value_mismatch: "critical",
5292
+ definition_conflict: "high",
5293
+ status_divergence: "medium",
5294
+ temporal_conflict: "medium"
5295
+ };
5296
+ ContradictionDetector = class {
5297
+ detect(store) {
5298
+ const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
5299
+ const byName = groupByName(nodes);
5300
+ const acc = {
5301
+ contradictions: [],
5302
+ sourcePairCounts: {},
5303
+ seen: /* @__PURE__ */ new Set()
5304
+ };
5305
+ collectExactMatches(byName, acc);
5306
+ collectFuzzyMatches(byName, acc);
5307
+ return {
5308
+ contradictions: acc.contradictions,
5309
+ sourcePairCounts: acc.sourcePairCounts,
5310
+ totalChecked: nodes.length
5311
+ };
5312
+ }
5313
+ };
5314
+ KNOWLEDGE_TYPES = KNOWLEDGE_NODE_TYPES;
5315
+ CODE_TYPES2 = [
5316
+ "file",
5317
+ "function",
5318
+ "class",
5319
+ "method",
5320
+ "interface",
5321
+ "variable"
5322
+ ];
5323
+ KNOWLEDGE_EDGE_TYPES = [
5324
+ "governs",
5325
+ "documents",
5326
+ "measures",
5327
+ "applies_to",
5328
+ "references",
5329
+ "uses_token",
5330
+ "declares_intent",
5331
+ "annotates"
5332
+ ];
5333
+ CoverageScorer = class {
5334
+ score(store) {
5335
+ const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
5336
+ const domainMap = groupByDomain(knowledgeNodes, () => "unclassified");
5337
+ const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
5338
+ const codeDomains = groupByDomain(codeNodes, (n) => this.domainFromPath(n.path));
5339
+ const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
5340
+ const domains = [];
5341
+ for (const domain of allDomains) {
5342
+ domains.push(
5343
+ scoreDomain(domain, domainMap.get(domain) ?? [], codeDomains.get(domain) ?? [], store)
5344
+ );
5345
+ }
5346
+ const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
5347
+ return {
5348
+ domains,
5349
+ overallScore,
5350
+ overallGrade: toGrade(overallScore),
5351
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
5352
+ };
5353
+ }
5354
+ domainFromPath(filePath) {
5355
+ if (!filePath) return "unclassified";
5356
+ const parts = filePath.split("/");
5357
+ const pkgIdx = parts.indexOf("packages");
5358
+ if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
5359
+ const srcIdx = parts.indexOf("src");
5360
+ if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
5361
+ return parts[0] ?? "unclassified";
5362
+ }
5363
+ };
5364
+ BUSINESS_NODE_TYPES = [
5365
+ "business_concept",
5366
+ "business_rule",
5367
+ "business_process",
5368
+ "business_term",
5369
+ "business_metric",
5370
+ "business_fact"
5371
+ ];
5372
+ SNAPSHOT_NODE_TYPES = [
5373
+ ...BUSINESS_NODE_TYPES,
5374
+ "design_token",
5375
+ "design_constraint",
5376
+ "aesthetic_intent",
5377
+ "image_annotation"
5378
+ ];
5379
+ KnowledgePipelineRunner = class {
5380
+ constructor(store) {
5381
+ this.store = store;
5382
+ }
5383
+ store;
5384
+ async run(options) {
5385
+ const remediations = [];
5386
+ const extraction = await this.extract(options);
5387
+ let driftResult = this.runReconciliation(options);
5388
+ const contradictions = new ContradictionDetector().detect(this.store);
5389
+ let gapReport = await this.detect(options);
5390
+ const coverage = new CoverageScorer().score(this.store);
5391
+ const iterations = options.fix ? await this.runRemediationLoop(options, driftResult, gapReport, remediations) : 1;
5392
+ if (options.fix && iterations > 1) {
5393
+ driftResult = this.runReconciliation(options);
5394
+ gapReport = await this.detect(options);
5395
+ }
5396
+ await this.stageNewFindings(driftResult, options);
5397
+ return this.buildResult(
5398
+ driftResult,
5399
+ iterations,
5400
+ extraction,
5401
+ gapReport,
5402
+ remediations,
5403
+ contradictions,
5404
+ coverage
5405
+ );
5406
+ }
5407
+ /** Run phases 1-2 (extract snapshot, reconcile) and return drift result. */
5408
+ runReconciliation(options) {
5409
+ const preSnapshot = this.buildSnapshot(options.domain);
5410
+ const postSnapshot = this.buildSnapshot(options.domain);
5411
+ return this.reconcile(preSnapshot, postSnapshot);
5412
+ }
5413
+ /** Run the remediation convergence loop; returns total iteration count. */
5414
+ async runRemediationLoop(options, driftResult, _gapReport, remediations) {
5415
+ const maxIterations = options.maxIterations ?? 5;
5416
+ let iterations = 1;
5417
+ let currentDrift = driftResult;
5418
+ let previousFindingCount = currentDrift.findings.length;
5419
+ while (iterations < maxIterations) {
5420
+ if (currentDrift.findings.length === 0) break;
5421
+ this.remediate(currentDrift, remediations, options);
5422
+ await this.extract(options);
5423
+ currentDrift = this.runReconciliation(options);
5424
+ await this.detect(options);
5425
+ iterations++;
5426
+ if (currentDrift.findings.length >= previousFindingCount) break;
5427
+ previousFindingCount = currentDrift.findings.length;
5428
+ }
5429
+ return iterations;
5430
+ }
5431
+ /** Assemble the final pipeline result. */
5432
+ buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage) {
5433
+ return {
5434
+ verdict: this.computeVerdict(driftResult),
5435
+ driftScore: driftResult.driftScore,
5436
+ iterations,
5437
+ findings: driftResult.summary,
5438
+ extraction,
5439
+ gaps,
5440
+ remediations,
5441
+ contradictions,
5442
+ coverage
5443
+ };
5444
+ }
5445
+ // ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
5446
+ async extract(options) {
5447
+ const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
5448
+ await fs9.mkdir(extractedDir, { recursive: true });
5449
+ const runner = createExtractionRunner();
5450
+ const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
5451
+ const diagramParser = new DiagramParser(this.store);
5452
+ const diagramResult = await diagramParser.ingest(options.projectDir);
5453
+ let imageCount = 0;
5454
+ const imagePaths = options.imagePaths ?? [];
5455
+ if (options.analyzeImages && options.analysisProvider && imagePaths.length > 0) {
5456
+ const imageExtractor = new ImageAnalysisExtractor({
5457
+ analysisProvider: options.analysisProvider
5458
+ });
5459
+ const imageResult = await imageExtractor.analyze(this.store, imagePaths);
5460
+ imageCount = imageResult.nodesAdded;
5461
+ }
5462
+ const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5463
+ const bkIngestor = new BusinessKnowledgeIngestor(this.store);
5464
+ let bkResult;
5465
+ try {
5466
+ bkResult = await bkIngestor.ingest(knowledgeDir);
5467
+ } catch {
5468
+ bkResult = {
5469
+ nodesAdded: 0,
5470
+ nodesUpdated: 0,
5471
+ edgesAdded: 0,
5472
+ edgesUpdated: 0,
5473
+ errors: [],
5474
+ durationMs: 0
5475
+ };
5476
+ }
5477
+ const linker = new KnowledgeLinker(this.store, extractedDir);
5478
+ const linkResult = await linker.link();
5479
+ return {
5480
+ codeSignals: extractionResult.nodesAdded,
5481
+ diagrams: diagramResult.nodesAdded,
5482
+ linkerFacts: linkResult.factsCreated,
5483
+ businessKnowledge: bkResult.nodesAdded,
5484
+ images: imageCount
5485
+ };
5486
+ }
5487
+ // ── Phase 2: RECONCILE ────────────────────────────────────────────────────
5488
+ buildSnapshot(domain) {
5489
+ let nodes = SNAPSHOT_NODE_TYPES.flatMap((type) => this.store.findNodes({ type }));
5490
+ if (domain) {
5491
+ nodes = nodes.filter((n) => n.metadata?.domain === domain);
5492
+ }
5493
+ return {
5494
+ entries: nodes.map((n) => ({
5495
+ id: n.id,
5496
+ type: n.type,
5497
+ contentHash: n.hash ?? n.id,
5498
+ source: n.metadata?.source ?? "unknown",
5499
+ name: n.name
5500
+ })),
5501
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5502
+ };
5503
+ }
5504
+ reconcile(current, fresh) {
5505
+ const detector = new StructuralDriftDetector();
5506
+ return detector.detect(current, fresh);
5507
+ }
5508
+ // ── Phase 3: DETECT ───────────────────────────────────────────────────────
5509
+ async detect(options) {
5510
+ const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5511
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir);
5512
+ const gapReport = await aggregator.generateGapReport(knowledgeDir);
5513
+ await aggregator.writeGapReport(gapReport);
5514
+ return gapReport;
5515
+ }
5516
+ // ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
5517
+ remediate(driftResult, remediations, options) {
5518
+ for (const finding of driftResult.findings) {
5519
+ switch (finding.classification) {
5520
+ case "stale":
5521
+ this.store.removeNode(finding.entryId);
5522
+ remediations.push(`removed stale: ${finding.entryId}`);
5523
+ break;
5524
+ case "new":
5525
+ break;
5526
+ case "drifted":
5527
+ if (!options.ci) {
5528
+ remediations.push(`flagged drifted: ${finding.entryId}`);
5529
+ }
5530
+ break;
5531
+ case "contradicting":
5532
+ break;
5533
+ }
5534
+ }
5535
+ }
5536
+ async stageNewFindings(driftResult, options) {
5537
+ const newFindings = driftResult.findings.filter((f) => f.classification === "new");
5538
+ if (newFindings.length === 0) return;
5539
+ const stagedEntries = newFindings.filter((f) => f.fresh != null).map((f) => ({
5540
+ id: f.fresh.id,
5541
+ source: this.classifySource(f.fresh.source),
5542
+ nodeType: f.fresh.type,
5543
+ name: f.fresh.name,
5544
+ confidence: 0.7,
5545
+ contentHash: f.fresh.contentHash,
5546
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5547
+ }));
5548
+ if (stagedEntries.length > 0) {
5549
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir);
5550
+ await aggregator.aggregate(stagedEntries, [], []);
5551
+ }
5552
+ }
5553
+ classifySource(source) {
5554
+ if (source === "linker" || source === "knowledge-linker") return "linker";
5555
+ if (source === "diagram") return "diagram";
5556
+ return "extractor";
5557
+ }
5558
+ // ── Verdict ───────────────────────────────────────────────────────────────
5559
+ computeVerdict(driftResult) {
5560
+ const { summary } = driftResult;
5561
+ const unresolved = summary.drifted + summary.stale + summary.contradicting;
5562
+ if (unresolved === 0 && summary.new === 0) return "pass";
5563
+ if (unresolved === 0) return "warn";
5564
+ return "fail";
5565
+ }
5566
+ };
5567
+ CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
5568
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
5569
+ SANITIZE_RULES = [
5570
+ // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
5571
+ {
5572
+ pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
5573
+ replacement: ""
5574
+ },
5575
+ // Strip markdown-style system prompt markers (including trailing space)
5576
+ {
5577
+ pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
5578
+ replacement: ""
5579
+ },
5580
+ // Strip common injection prefixes
5581
+ {
5582
+ pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
5583
+ replacement: "[filtered]"
5584
+ },
5585
+ // Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
5586
+ {
5587
+ pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
5588
+ replacement: "[filtered]"
5589
+ }
5590
+ ];
5591
+ SyncManager = class {
5592
+ constructor(store, graphDir) {
5593
+ this.store = store;
5594
+ this.metadataPath = path11.join(graphDir, "sync-metadata.json");
5595
+ }
5596
+ store;
5597
+ registrations = /* @__PURE__ */ new Map();
5598
+ metadataPath;
5599
+ registerConnector(connector, config) {
5600
+ this.registrations.set(connector.name, { connector, config });
5601
+ }
5602
+ async sync(connectorName) {
5603
+ const registration = this.registrations.get(connectorName);
5604
+ if (!registration) {
5605
+ return {
5606
+ nodesAdded: 0,
5607
+ nodesUpdated: 0,
5608
+ edgesAdded: 0,
5609
+ edgesUpdated: 0,
5610
+ errors: [`Connector "${connectorName}" not registered`],
5611
+ durationMs: 0
5612
+ };
5613
+ }
5614
+ const { connector, config } = registration;
5615
+ const result = await connector.ingest(this.store, config);
5616
+ const metadata = await this.loadMetadata();
5617
+ metadata.connectors[connectorName] = {
5618
+ lastSyncTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
5619
+ lastResult: result
5620
+ };
5621
+ await this.saveMetadata(metadata);
5622
+ return result;
5623
+ }
5624
+ async syncAll() {
5625
+ const combined = {
5626
+ nodesAdded: 0,
5627
+ nodesUpdated: 0,
5628
+ edgesAdded: 0,
5629
+ edgesUpdated: 0,
5630
+ errors: [],
5631
+ durationMs: 0
5632
+ };
5633
+ for (const [name] of this.registrations) {
5634
+ const result = await this.sync(name);
5635
+ combined.nodesAdded += result.nodesAdded;
5636
+ combined.nodesUpdated += result.nodesUpdated;
5637
+ combined.edgesAdded += result.edgesAdded;
2744
5638
  combined.edgesUpdated += result.edgesUpdated;
2745
5639
  combined.errors.push(...result.errors);
2746
5640
  combined.durationMs += result.durationMs;
2747
5641
  }
5642
+ try {
5643
+ const linker = new KnowledgeLinker(this.store);
5644
+ const linkResult = await linker.link();
5645
+ combined.nodesAdded += linkResult.factsCreated + linkResult.conceptsClustered;
5646
+ combined.errors.push(...linkResult.errors);
5647
+ } catch (err) {
5648
+ combined.errors.push(
5649
+ `KnowledgeLinker error: ${err instanceof Error ? err.message : String(err)}`
5650
+ );
5651
+ }
2748
5652
  return combined;
2749
5653
  }
2750
5654
  async getMetadata() {
@@ -2752,23 +5656,29 @@ var init_dist = __esm({
2752
5656
  }
2753
5657
  async loadMetadata() {
2754
5658
  try {
2755
- const raw = await fs5.readFile(this.metadataPath, "utf-8");
5659
+ const raw = await fs10.readFile(this.metadataPath, "utf-8");
2756
5660
  return JSON.parse(raw);
2757
5661
  } catch {
2758
5662
  return { connectors: {} };
2759
5663
  }
2760
5664
  }
2761
5665
  async saveMetadata(metadata) {
2762
- await fs5.mkdir(path6.dirname(this.metadataPath), { recursive: true });
2763
- await fs5.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5666
+ await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
5667
+ await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
2764
5668
  }
2765
5669
  };
5670
+ SUMMARIZE_PROMPT = `Summarize the following content to fit within the specified length.
5671
+ Preserve all business rules, SLAs, requirements, decisions, and regulatory references.
5672
+ Remove redundant details and boilerplate while keeping all actionable information.
5673
+
5674
+ Content to summarize:
5675
+ `;
2766
5676
  JiraConnector = class {
2767
5677
  name = "jira";
2768
5678
  source = "jira";
2769
5679
  httpClient;
2770
5680
  constructor(httpClient) {
2771
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5681
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2772
5682
  }
2773
5683
  async ingest(store, config) {
2774
5684
  const start = Date.now();
@@ -2795,7 +5705,7 @@ var init_dist = __esm({
2795
5705
  const jql = buildJql(config);
2796
5706
  const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
2797
5707
  try {
2798
- const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
5708
+ const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
2799
5709
  return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
2800
5710
  } catch (err) {
2801
5711
  return buildIngestResult(
@@ -2806,7 +5716,7 @@ var init_dist = __esm({
2806
5716
  );
2807
5717
  }
2808
5718
  }
2809
- async fetchAllIssues(store, baseUrl, jql, headers) {
5719
+ async fetchAllIssues(store, baseUrl, jql, headers, config) {
2810
5720
  let nodesAdded = 0;
2811
5721
  let edgesAdded = 0;
2812
5722
  let startAt = 0;
@@ -2819,7 +5729,7 @@ var init_dist = __esm({
2819
5729
  const data = await response.json();
2820
5730
  total = data.total;
2821
5731
  for (const issue of data.issues) {
2822
- const counts = this.processIssue(store, issue);
5732
+ const counts = await this.processIssue(store, issue, baseUrl, headers, config);
2823
5733
  nodesAdded += counts.nodesAdded;
2824
5734
  edgesAdded += counts.edgesAdded;
2825
5735
  }
@@ -2827,19 +5737,47 @@ var init_dist = __esm({
2827
5737
  }
2828
5738
  return { nodesAdded, edgesAdded };
2829
5739
  }
2830
- processIssue(store, issue) {
5740
+ async processIssue(store, issue, baseUrl, headers, config) {
2831
5741
  const nodeId = `issue:jira:${issue.key}`;
5742
+ const comments = await this.fetchComments(baseUrl, issue.key, headers);
5743
+ const parts = [issue.fields.summary];
5744
+ if (issue.fields.description) {
5745
+ parts.push(issue.fields.description);
5746
+ }
5747
+ if (comments.length > 0) {
5748
+ for (const comment of comments) {
5749
+ parts.push(`${comment.author.displayName} (${comment.created}): ${comment.body}`);
5750
+ }
5751
+ }
5752
+ const rawContent = parts.join("\n");
5753
+ const acceptanceCriteria = parseAcceptanceCriteria(issue.fields.description ?? "");
5754
+ const customFields = extractCustomFields(issue.fields);
5755
+ const maxLen = config.maxContentLength ?? 4e3;
5756
+ const condensed = await condenseContent(rawContent, { maxLength: maxLen });
5757
+ const metadata = {
5758
+ key: issue.key,
5759
+ status: issue.fields.status?.name,
5760
+ priority: issue.fields.priority?.name,
5761
+ assignee: issue.fields.assignee?.displayName,
5762
+ labels: issue.fields.labels ?? [],
5763
+ commentCount: comments.length
5764
+ };
5765
+ if (acceptanceCriteria.length > 0) {
5766
+ metadata.acceptanceCriteria = acceptanceCriteria;
5767
+ }
5768
+ if (Object.keys(customFields).length > 0) {
5769
+ metadata.customFields = customFields;
5770
+ }
5771
+ if (condensed.method !== "passthrough") {
5772
+ metadata.condensed = condensed.method;
5773
+ metadata.originalLength = condensed.originalLength;
5774
+ }
2832
5775
  store.addNode({
2833
5776
  id: nodeId,
2834
5777
  type: "issue",
2835
5778
  name: sanitizeExternalText(issue.fields.summary, 500),
2836
- metadata: {
2837
- key: issue.key,
2838
- status: issue.fields.status?.name,
2839
- priority: issue.fields.priority?.name,
2840
- assignee: issue.fields.assignee?.displayName,
2841
- labels: issue.fields.labels ?? []
2842
- }
5779
+ content: condensed.content,
5780
+ metadata
2843
5781
  });
2844
5782
  const searchText = sanitizeExternalText(
2845
5783
  [issue.fields.summary, issue.fields.description ?? ""].join(" ")
@@ -2847,13 +5785,24 @@ var init_dist = __esm({
2847
5785
  const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
2848
5786
  return { nodesAdded: 1, edgesAdded };
2849
5787
  }
5788
+ async fetchComments(baseUrl, issueKey, headers) {
5789
+ try {
5790
+ const url = `${baseUrl}/rest/api/2/issue/${issueKey}/comment`;
5791
+ const response = await this.httpClient(url, { headers });
5792
+ if (!response.ok) return [];
5793
+ const data = await response.json();
5794
+ return data.comments ?? [];
5795
+ } catch {
5796
+ return [];
5797
+ }
5798
+ }
2850
5799
  };
2851
5800
  SlackConnector = class {
2852
5801
  name = "slack";
2853
5802
  source = "slack";
2854
5803
  httpClient;
2855
5804
  constructor(httpClient) {
2856
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5805
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2857
5806
  }
2858
5807
  async ingest(store, config) {
2859
5808
  const start = Date.now();
@@ -2876,7 +5825,7 @@ var init_dist = __esm({
2876
5825
  const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
2877
5826
  for (const channel of channels) {
2878
5827
  try {
2879
- const result = await this.processChannel(store, channel, apiKey, oldest);
5828
+ const result = await this.processChannel(store, channel, apiKey, oldest, config);
2880
5829
  nodesAdded += result.nodesAdded;
2881
5830
  edgesAdded += result.edgesAdded;
2882
5831
  errors.push(...result.errors);
@@ -2895,7 +5844,7 @@ var init_dist = __esm({
2895
5844
  durationMs: Date.now() - start
2896
5845
  };
2897
5846
  }
2898
- async processChannel(store, channel, apiKey, oldest) {
5847
+ async processChannel(store, channel, apiKey, oldest, config) {
2899
5848
  const errors = [];
2900
5849
  let nodesAdded = 0;
2901
5850
  let edgesAdded = 0;
@@ -2920,34 +5869,81 @@ var init_dist = __esm({
2920
5869
  if (!data.ok) {
2921
5870
  return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
2922
5871
  }
5872
+ const maxLen = config.maxContentLength ?? 2e3;
2923
5873
  for (const message of data.messages) {
2924
5874
  const nodeId = `conversation:slack:${channel}:${message.ts}`;
2925
- const sanitizedText = sanitizeExternalText(message.text);
2926
- const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
5875
+ let assembledText = message.text;
5876
+ let threadReplyCount;
5877
+ if (message.reply_count && message.reply_count > 0 && message.thread_ts) {
5878
+ const replies = await this.fetchThreadReplies(channel, message.thread_ts, apiKey);
5879
+ if (replies.length > 0) {
5880
+ const threadReplies = replies.slice(1);
5881
+ threadReplyCount = threadReplies.length;
5882
+ if (threadReplies.length > 0) {
5883
+ const replyLines = threadReplies.map((r) => `${r.user} (${r.ts}): ${r.text}`);
5884
+ assembledText = `${message.text}
5885
+ ${replyLines.join("\n")}`;
5886
+ }
5887
+ }
5888
+ }
5889
+ const condensed = await condenseContent(assembledText, { maxLength: maxLen });
5890
+ const snippet = condensed.content.length > 100 ? condensed.content.slice(0, 100) : condensed.content;
5891
+ const reactions = message.reactions ? message.reactions.reduce(
5892
+ (acc, r) => ({ ...acc, [r.name]: r.count }),
5893
+ {}
5894
+ ) : void 0;
5895
+ const metadata = {
5896
+ author: message.user,
5897
+ channel,
5898
+ timestamp: message.ts
5899
+ };
5900
+ if (threadReplyCount !== void 0) {
5901
+ metadata.threadReplyCount = threadReplyCount;
5902
+ }
5903
+ if (reactions) {
5904
+ metadata.reactions = reactions;
5905
+ }
5906
+ if (condensed.method !== "passthrough") {
5907
+ metadata.condensed = condensed.method;
5908
+ metadata.originalLength = condensed.originalLength;
5909
+ }
2927
5910
  store.addNode({
2928
5911
  id: nodeId,
2929
5912
  type: "conversation",
2930
5913
  name: snippet,
2931
- metadata: {
2932
- author: message.user,
2933
- channel,
2934
- timestamp: message.ts
2935
- }
5914
+ content: condensed.content,
5915
+ metadata
2936
5916
  });
2937
5917
  nodesAdded++;
2938
- edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
5918
+ edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
2939
5919
  checkPaths: true
2940
5920
  });
2941
5921
  }
2942
5922
  return { nodesAdded, edgesAdded, errors };
2943
5923
  }
5924
+ async fetchThreadReplies(channel, threadTs, apiKey) {
5925
+ try {
5926
+ const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${threadTs}`;
5927
+ const response = await this.httpClient(url, {
5928
+ headers: {
5929
+ Authorization: `Bearer ${apiKey}`,
5930
+ "Content-Type": "application/json"
5931
+ }
5932
+ });
5933
+ if (!response.ok) return [];
5934
+ const data = await response.json();
5935
+ return data.ok ? data.messages : [];
5936
+ } catch {
5937
+ return [];
5938
+ }
5939
+ }
2944
5940
  };
2945
5941
  ConfluenceConnector = class {
2946
5942
  name = "confluence";
2947
5943
  source = "confluence";
2948
5944
  httpClient;
2949
5945
  constructor(httpClient) {
2950
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5946
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2951
5947
  }
2952
5948
  async ingest(store, config) {
2953
5949
  const start = Date.now();
@@ -2960,7 +5956,14 @@ var init_dist = __esm({
2960
5956
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
2961
5957
  const baseUrl = process.env[baseUrlEnv] ?? "";
2962
5958
  const spaceKey = config.spaceKey ?? "";
2963
- const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
5959
+ const counts = await this.fetchAllPagesHandled(
5960
+ store,
5961
+ baseUrl,
5962
+ apiKey,
5963
+ spaceKey,
5964
+ errors,
5965
+ config
5966
+ );
2964
5967
  return {
2965
5968
  nodesAdded: counts.nodesAdded,
2966
5969
  nodesUpdated: 0,
@@ -2970,9 +5973,9 @@ var init_dist = __esm({
2970
5973
  durationMs: Date.now() - start
2971
5974
  };
2972
5975
  }
2973
- async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
5976
+ async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
2974
5977
  try {
2975
- const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
5978
+ const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
2976
5979
  errors.push(...result.errors);
2977
5980
  return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
2978
5981
  } catch (err) {
@@ -2980,7 +5983,7 @@ var init_dist = __esm({
2980
5983
  return { nodesAdded: 0, edgesAdded: 0 };
2981
5984
  }
2982
5985
  }
2983
- async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
5986
+ async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
2984
5987
  const errors = [];
2985
5988
  let nodesAdded = 0;
2986
5989
  let edgesAdded = 0;
@@ -2995,7 +5998,7 @@ var init_dist = __esm({
2995
5998
  }
2996
5999
  const data = await response.json();
2997
6000
  for (const page of data.results) {
2998
- const counts = this.processPage(store, page, spaceKey);
6001
+ const counts = await this.processPage(store, page, spaceKey, config);
2999
6002
  nodesAdded += counts.nodesAdded;
3000
6003
  edgesAdded += counts.edgesAdded;
3001
6004
  }
@@ -3003,22 +6006,43 @@ var init_dist = __esm({
3003
6006
  }
3004
6007
  return { nodesAdded, edgesAdded, errors };
3005
6008
  }
3006
- processPage(store, page, spaceKey) {
6009
+ async processPage(store, page, spaceKey, config) {
3007
6010
  const nodeId = `confluence:${page.id}`;
6011
+ let edgesAdded = 0;
6012
+ const labels = page.metadata?.labels?.results?.map((l) => l.name) ?? [];
6013
+ const parentPageId = page.ancestors && page.ancestors.length > 0 ? page.ancestors[page.ancestors.length - 1].id : void 0;
6014
+ const rawContent = `${page.title} ${page.body?.storage?.value ?? ""}`;
6015
+ const maxLen = config.maxContentLength ?? 8e3;
6016
+ const condensed = await condenseContent(rawContent, { maxLength: maxLen });
6017
+ const metadata = {
6018
+ source: "confluence",
6019
+ spaceKey,
6020
+ pageId: page.id,
6021
+ status: page.status,
6022
+ url: page._links?.webui ?? "",
6023
+ labels
6024
+ };
6025
+ if (parentPageId) {
6026
+ metadata.parentPageId = parentPageId;
6027
+ }
6028
+ if (condensed.method !== "passthrough") {
6029
+ metadata.condensed = condensed.method;
6030
+ metadata.originalLength = condensed.originalLength;
6031
+ }
3008
6032
  store.addNode({
3009
6033
  id: nodeId,
3010
6034
  type: "document",
3011
6035
  name: sanitizeExternalText(page.title, 500),
3012
- metadata: {
3013
- source: "confluence",
3014
- spaceKey,
3015
- pageId: page.id,
3016
- status: page.status,
3017
- url: page._links?.webui ?? ""
3018
- }
6036
+ content: condensed.content,
6037
+ metadata
3019
6038
  });
6039
+ if (parentPageId) {
6040
+ const parentNodeId = `confluence:${parentPageId}`;
6041
+ store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
6042
+ edgesAdded++;
6043
+ }
3020
6044
  const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
3021
- const edgesAdded = linkToCode(store, text, nodeId, "documents");
6045
+ edgesAdded += linkToCode(store, text, nodeId, "documents");
3022
6046
  return { nodesAdded: 1, edgesAdded };
3023
6047
  }
3024
6048
  };
@@ -3027,7 +6051,7 @@ var init_dist = __esm({
3027
6051
  source = "github-actions";
3028
6052
  httpClient;
3029
6053
  constructor(httpClient) {
3030
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
6054
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
3031
6055
  }
3032
6056
  async ingest(store, config) {
3033
6057
  const start = Date.now();
@@ -3035,7 +6059,7 @@ var init_dist = __esm({
3035
6059
  const apiKeyEnv = config.apiKeyEnv ?? "GITHUB_TOKEN";
3036
6060
  const apiKey = process.env[apiKeyEnv];
3037
6061
  if (!apiKey) {
3038
- return emptyResult2(
6062
+ return emptyResult5(
3039
6063
  [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
3040
6064
  start
3041
6065
  );
@@ -3078,6 +6102,255 @@ var init_dist = __esm({
3078
6102
  return { nodesAdded, edgesAdded };
3079
6103
  }
3080
6104
  };
6105
+ CONSTRAINT_KEYWORDS = [
6106
+ "constraint",
6107
+ "must",
6108
+ "required",
6109
+ "minimum",
6110
+ "maximum",
6111
+ "spacing",
6112
+ "padding",
6113
+ "margin",
6114
+ "breakpoint",
6115
+ "responsive",
6116
+ "accessible",
6117
+ "a11y",
6118
+ "wcag"
6119
+ ];
6120
+ FigmaConnector = class {
6121
+ name = "figma";
6122
+ source = "figma";
6123
+ httpClient;
6124
+ constructor(httpClient) {
6125
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
6126
+ }
6127
+ async ingest(store, config) {
6128
+ const start = Date.now();
6129
+ const validated = validateFigmaConfig(config);
6130
+ if ("error" in validated) return buildIngestResult2(0, 0, [validated.error], start);
6131
+ const { apiKey, baseUrl, fileIds } = validated;
6132
+ const headers = { "X-FIGMA-TOKEN": apiKey };
6133
+ const maxLen = config.maxContentLength ?? 4e3;
6134
+ let nodesAdded = 0;
6135
+ let edgesAdded = 0;
6136
+ const errors = [];
6137
+ for (const fileId of fileIds) {
6138
+ try {
6139
+ const counts = await this.processFile(store, baseUrl, fileId, headers, maxLen);
6140
+ nodesAdded += counts.nodesAdded;
6141
+ edgesAdded += counts.edgesAdded;
6142
+ } catch (err) {
6143
+ errors.push(
6144
+ `Figma API error for file ${fileId}: ${err instanceof Error ? err.message : String(err)}`
6145
+ );
6146
+ }
6147
+ }
6148
+ return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
6149
+ }
6150
+ async processFile(store, baseUrl, fileId, headers, maxLen) {
6151
+ let nodesAdded = 0;
6152
+ let edgesAdded = 0;
6153
+ const styleCounts = await this.ingestStyles(store, baseUrl, fileId, headers, maxLen);
6154
+ nodesAdded += styleCounts.nodesAdded;
6155
+ edgesAdded += styleCounts.edgesAdded;
6156
+ const componentCounts = await this.ingestComponents(store, baseUrl, fileId, headers, maxLen);
6157
+ nodesAdded += componentCounts.nodesAdded;
6158
+ edgesAdded += componentCounts.edgesAdded;
6159
+ return { nodesAdded, edgesAdded };
6160
+ }
6161
+ async ingestStyles(store, baseUrl, fileId, headers, maxLen) {
6162
+ let nodesAdded = 0;
6163
+ let edgesAdded = 0;
6164
+ const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
6165
+ const stylesResponse = await this.httpClient(stylesUrl, { headers });
6166
+ if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
6167
+ const stylesData = await stylesResponse.json();
6168
+ for (const style of stylesData.meta.styles) {
6169
+ const counts = await this.processStyle(store, style, fileId, maxLen);
6170
+ nodesAdded += counts.nodesAdded;
6171
+ edgesAdded += counts.edgesAdded;
6172
+ }
6173
+ return { nodesAdded, edgesAdded };
6174
+ }
6175
+ async processStyle(store, style, fileId, maxLen) {
6176
+ const nodeId = `figma:token:${style.key}`;
6177
+ const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
6178
+ maxLength: maxLen
6179
+ });
6180
+ const metadata = buildNodeMetadata(condensed, {
6181
+ source: "figma",
6182
+ key: style.key,
6183
+ styleType: style.style_type,
6184
+ fileId
6185
+ });
6186
+ store.addNode({
6187
+ id: nodeId,
6188
+ type: "design_token",
6189
+ name: sanitizeExternalText(style.name, 500),
6190
+ content: condensed.content,
6191
+ metadata
6192
+ });
6193
+ const searchText = sanitizeExternalText([style.name, style.description].join(" "));
6194
+ const edgesAdded = linkToCode(store, searchText, nodeId, "references");
6195
+ return { nodesAdded: 1, edgesAdded };
6196
+ }
6197
+ async ingestComponents(store, baseUrl, fileId, headers, maxLen) {
6198
+ let nodesAdded = 0;
6199
+ let edgesAdded = 0;
6200
+ const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
6201
+ const componentsResponse = await this.httpClient(componentsUrl, { headers });
6202
+ if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
6203
+ const componentsData = await componentsResponse.json();
6204
+ for (const component of componentsData.meta.components) {
6205
+ const counts = await this.processComponent(store, component, fileId, maxLen);
6206
+ nodesAdded += counts.nodesAdded;
6207
+ edgesAdded += counts.edgesAdded;
6208
+ }
6209
+ return { nodesAdded, edgesAdded };
6210
+ }
6211
+ async processComponent(store, component, fileId, maxLen) {
6212
+ let nodesAdded = 0;
6213
+ let edgesAdded = 0;
6214
+ const description = component.description || "";
6215
+ if (description) {
6216
+ const counts = await this.addComponentIntent(store, component, description, fileId, maxLen);
6217
+ nodesAdded += counts.nodesAdded;
6218
+ edgesAdded += counts.edgesAdded;
6219
+ }
6220
+ if (description && hasConstraintKeyword(description)) {
6221
+ const counts = await this.addComponentConstraint(
6222
+ store,
6223
+ component,
6224
+ description,
6225
+ fileId,
6226
+ maxLen
6227
+ );
6228
+ nodesAdded += counts.nodesAdded;
6229
+ edgesAdded += counts.edgesAdded;
6230
+ }
6231
+ return { nodesAdded, edgesAdded };
6232
+ }
6233
+ async addComponentIntent(store, component, description, fileId, maxLen) {
6234
+ const intentId = `figma:intent:${component.key}`;
6235
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6236
+ maxLength: maxLen
6237
+ });
6238
+ const metadata = buildNodeMetadata(condensed, {
6239
+ source: "figma",
6240
+ key: component.key,
6241
+ fileId
6242
+ });
6243
+ store.addNode({
6244
+ id: intentId,
6245
+ type: "aesthetic_intent",
6246
+ name: sanitizeExternalText(component.name, 500),
6247
+ content: condensed.content,
6248
+ metadata
6249
+ });
6250
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6251
+ const edgesAdded = linkToCode(store, searchText, intentId, "references");
6252
+ return { nodesAdded: 1, edgesAdded };
6253
+ }
6254
+ async addComponentConstraint(store, component, description, fileId, maxLen) {
6255
+ const constraintId = `figma:constraint:${component.key}`;
6256
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6257
+ maxLength: maxLen
6258
+ });
6259
+ const metadata = buildNodeMetadata(condensed, {
6260
+ source: "figma",
6261
+ key: component.key,
6262
+ fileId
6263
+ });
6264
+ store.addNode({
6265
+ id: constraintId,
6266
+ type: "design_constraint",
6267
+ name: sanitizeExternalText(component.name, 500),
6268
+ content: condensed.content,
6269
+ metadata
6270
+ });
6271
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6272
+ const edgesAdded = linkToCode(store, searchText, constraintId, "references");
6273
+ return { nodesAdded: 1, edgesAdded };
6274
+ }
6275
+ };
6276
+ MIN_CONTENT_LENGTH = 10;
6277
+ CONCEPT_ITEM_TYPES = /* @__PURE__ */ new Set(["sticky_note", "text"]);
6278
+ MiroConnector = class {
6279
+ name = "miro";
6280
+ source = "miro";
6281
+ httpClient;
6282
+ constructor(httpClient) {
6283
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
6284
+ }
6285
+ async ingest(store, config) {
6286
+ const start = Date.now();
6287
+ const resolved = resolveConfig(config, start);
6288
+ if (!resolved.ok) return resolved.result;
6289
+ const { baseUrl, boardIds, headers } = resolved;
6290
+ let totalNodesAdded = 0;
6291
+ let totalEdgesAdded = 0;
6292
+ const errors = [];
6293
+ for (const boardId of boardIds) {
6294
+ try {
6295
+ const counts = await this.processBoard(store, baseUrl, boardId, headers);
6296
+ totalNodesAdded += counts.nodesAdded;
6297
+ totalEdgesAdded += counts.edgesAdded;
6298
+ } catch (err) {
6299
+ errors.push(
6300
+ `Miro API error for board ${boardId}: ${err instanceof Error ? err.message : String(err)}`
6301
+ );
6302
+ }
6303
+ }
6304
+ return buildIngestResult3(totalNodesAdded, totalEdgesAdded, errors, start);
6305
+ }
6306
+ async processBoard(store, baseUrl, boardId, headers) {
6307
+ let nodesAdded = 0;
6308
+ let edgesAdded = 0;
6309
+ const boardResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}`, { headers });
6310
+ if (!boardResponse.ok) throw new Error(`Failed to fetch board ${boardId}`);
6311
+ const board = await boardResponse.json();
6312
+ const boardNodeId = `miro:board:${boardId}`;
6313
+ store.addNode({
6314
+ id: boardNodeId,
6315
+ type: "document",
6316
+ name: sanitizeExternalText(board.name, 500),
6317
+ content: sanitizeExternalText(board.description ?? "", 2e3),
6318
+ metadata: {
6319
+ source: "miro",
6320
+ boardId: board.id
6321
+ }
6322
+ });
6323
+ nodesAdded++;
6324
+ const itemsResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}/items`, {
6325
+ headers
6326
+ });
6327
+ if (!itemsResponse.ok) throw new Error(`Failed to fetch items for board ${boardId}`);
6328
+ const itemsData = await itemsResponse.json();
6329
+ for (const item of itemsData.data) {
6330
+ const rawContent = item.data?.content ?? "";
6331
+ const plainContent = stripHtml(rawContent);
6332
+ if (plainContent.length < MIN_CONTENT_LENGTH) continue;
6333
+ if (!CONCEPT_ITEM_TYPES.has(item.type)) continue;
6334
+ const itemNodeId = `miro:item:${item.id}`;
6335
+ store.addNode({
6336
+ id: itemNodeId,
6337
+ type: "business_concept",
6338
+ name: sanitizeExternalText(plainContent, 500),
6339
+ content: sanitizeExternalText(plainContent, 2e3),
6340
+ metadata: {
6341
+ source: "miro",
6342
+ boardId: board.id,
6343
+ itemType: item.type
6344
+ }
6345
+ });
6346
+ nodesAdded++;
6347
+ store.addEdge({ from: boardNodeId, to: itemNodeId, type: "contains" });
6348
+ edgesAdded++;
6349
+ edgesAdded += linkToCode(store, plainContent, itemNodeId, "documents");
6350
+ }
6351
+ return { nodesAdded, edgesAdded };
6352
+ }
6353
+ };
3081
6354
  STOP_WORDS = /* @__PURE__ */ new Set([
3082
6355
  "the",
3083
6356
  "a",
@@ -4146,9 +7419,9 @@ var init_dist = __esm({
4146
7419
  extractPaths(trimmed, add) {
4147
7420
  const consumed = /* @__PURE__ */ new Set();
4148
7421
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
4149
- const path8 = match[0];
4150
- add(path8);
4151
- consumed.add(path8);
7422
+ const path13 = match[0];
7423
+ add(path13);
7424
+ consumed.add(path13);
4152
7425
  }
4153
7426
  return consumed;
4154
7427
  }
@@ -4218,8 +7491,8 @@ var init_dist = __esm({
4218
7491
  if (isPathLike && node.path.includes(raw)) {
4219
7492
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
4220
7493
  }
4221
- const basename6 = node.path.split("/").pop() ?? "";
4222
- if (basename6.includes(raw)) {
7494
+ const basename7 = node.path.split("/").pop() ?? "";
7495
+ if (basename7.includes(raw)) {
4223
7496
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
4224
7497
  }
4225
7498
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -4296,13 +7569,13 @@ var init_dist = __esm({
4296
7569
  const context = Array.isArray(d?.context) ? d.context : [];
4297
7570
  const firstEntity = entities[0];
4298
7571
  const nodeType = firstEntity?.node.type ?? "node";
4299
- const path8 = firstEntity?.node.path ?? "unknown";
7572
+ const path13 = firstEntity?.node.path ?? "unknown";
4300
7573
  let neighborCount = 0;
4301
7574
  const firstContext = context[0];
4302
7575
  if (firstContext && Array.isArray(firstContext.nodes)) {
4303
7576
  neighborCount = firstContext.nodes.length;
4304
7577
  }
4305
- return `**${entityName}** is a ${nodeType} at \`${path8}\`. Connected to ${neighborCount} nodes.`;
7578
+ return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
4306
7579
  }
4307
7580
  formatAnomaly(data) {
4308
7581
  const d = data;
@@ -4833,8 +8106,8 @@ var init_dist = __esm({
4833
8106
  const { edges } = this.computeDependencyGraph();
4834
8107
  const violations = [];
4835
8108
  for (const edge of edges) {
4836
- const fromRelative = relative3(rootDir, edge.from).replaceAll("\\", "/");
4837
- const toRelative = relative3(rootDir, edge.to).replaceAll("\\", "/");
8109
+ const fromRelative = relative5(rootDir, edge.from).replaceAll("\\", "/");
8110
+ const toRelative = relative5(rootDir, edge.to).replaceAll("\\", "/");
4838
8111
  const fromLayer = this.resolveLayer(fromRelative, layers);
4839
8112
  const toLayer = this.resolveLayer(toRelative, layers);
4840
8113
  if (!fromLayer || !toLayer) continue;
@@ -4927,8 +8200,8 @@ var init_dist = __esm({
4927
8200
  async ingestAll(designDir) {
4928
8201
  const start = Date.now();
4929
8202
  const [tokensResult, intentResult] = await Promise.all([
4930
- this.ingestTokens(path7.join(designDir, "tokens.json")),
4931
- this.ingestDesignIntent(path7.join(designDir, "DESIGN.md"))
8203
+ this.ingestTokens(path12.join(designDir, "tokens.json")),
8204
+ this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
4932
8205
  ]);
4933
8206
  const merged = mergeResults(tokensResult, intentResult);
4934
8207
  return { ...merged, durationMs: Date.now() - start };
@@ -5158,10 +8431,10 @@ var init_dist = __esm({
5158
8431
  includeTypes: ["file"]
5159
8432
  });
5160
8433
  for (const n of queryResult.nodes) {
5161
- const path8 = n.path ?? n.id.replace(/^file:/, "");
5162
- if (!fileSet.has(path8)) {
5163
- if (!result.has(path8)) {
5164
- result.set(path8, file);
8434
+ const path13 = n.path ?? n.id.replace(/^file:/, "");
8435
+ if (!fileSet.has(path13)) {
8436
+ if (!result.has(path13)) {
8437
+ result.set(path13, file);
5165
8438
  }
5166
8439
  }
5167
8440
  }
@@ -5562,12 +8835,32 @@ export {
5562
8835
  KnowledgeIngestor,
5563
8836
  BusinessKnowledgeIngestor,
5564
8837
  RequirementIngestor,
8838
+ MermaidParser,
8839
+ D2Parser,
8840
+ PlantUmlParser,
8841
+ DiagramParser,
8842
+ StructuralDriftDetector,
8843
+ KnowledgeStagingAggregator,
8844
+ detectLanguage,
8845
+ ExtractionRunner,
8846
+ TestDescriptionExtractor,
8847
+ EnumConstantExtractor,
8848
+ ValidationRuleExtractor,
8849
+ ApiPathExtractor,
8850
+ ALL_EXTRACTORS,
8851
+ createExtractionRunner,
8852
+ ImageAnalysisExtractor,
8853
+ ContradictionDetector,
8854
+ CoverageScorer,
8855
+ KnowledgePipelineRunner,
5565
8856
  linkToCode,
5566
8857
  SyncManager,
5567
8858
  JiraConnector,
5568
8859
  SlackConnector,
5569
8860
  ConfluenceConnector,
5570
8861
  CIConnector,
8862
+ FigmaConnector,
8863
+ MiroConnector,
5571
8864
  FusionLayer,
5572
8865
  GraphEntropyAdapter,
5573
8866
  GraphComplexityAdapter,