@harness-engineering/cli 1.25.6 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/agents/skills/claude-code/harness-knowledge-pipeline/SKILL.md +277 -0
  2. package/dist/agents/skills/claude-code/harness-knowledge-pipeline/skill.yaml +60 -0
  3. package/dist/agents/skills/codex/harness-knowledge-pipeline/SKILL.md +277 -0
  4. package/dist/agents/skills/codex/harness-knowledge-pipeline/skill.yaml +60 -0
  5. package/dist/agents/skills/cursor/harness-knowledge-pipeline/SKILL.md +277 -0
  6. package/dist/agents/skills/cursor/harness-knowledge-pipeline/skill.yaml +60 -0
  7. package/dist/agents/skills/gemini-cli/harness-knowledge-pipeline/SKILL.md +277 -0
  8. package/dist/agents/skills/gemini-cli/harness-knowledge-pipeline/skill.yaml +60 -0
  9. package/dist/agents/skills/package.json +2 -2
  10. package/dist/{agents-md-2PYJM2MK.js → agents-md-OPOZA5L7.js} +3 -3
  11. package/dist/{architecture-VCLB7A23.js → architecture-PMVMO6SF.js} +4 -4
  12. package/dist/{assess-project-64C6LIKN.js → assess-project-ZW7EKNIT.js} +1 -1
  13. package/dist/bin/harness-mcp.js +15 -15
  14. package/dist/bin/harness.js +25 -25
  15. package/dist/{check-phase-gate-RT6PGEHY.js → check-phase-gate-NTLAU3LO.js} +4 -4
  16. package/dist/{chunk-JUXFYB2K.js → chunk-4KSKV5MK.js} +1 -1
  17. package/dist/{chunk-7BAGSY5Q.js → chunk-6SCA4NEZ.js} +1 -1
  18. package/dist/{chunk-42ZZLMYD.js → chunk-77EZGPSR.js} +1 -1
  19. package/dist/{chunk-CBZECDCW.js → chunk-7JNZXJKY.js} +5 -5
  20. package/dist/{chunk-43VBX44J.js → chunk-CAS3TS4Y.js} +110 -92
  21. package/dist/{chunk-FL6A72LV.js → chunk-EXTWTJRF.js} +1 -1
  22. package/dist/{chunk-XLO4AXXM.js → chunk-FI3D6D7U.js} +9 -9
  23. package/dist/{chunk-ZAKUCM7O.js → chunk-G6SYTGOH.js} +2 -2
  24. package/dist/{chunk-RKZW3FDF.js → chunk-HAOBHL3W.js} +1 -1
  25. package/dist/{chunk-4YEBV2FT.js → chunk-IFHAUFJX.js} +2 -2
  26. package/dist/{chunk-XMEEYMGE.js → chunk-J3O64D2A.js} +1 -1
  27. package/dist/{chunk-WEOGCL7B.js → chunk-JBJ4AVE4.js} +3 -3
  28. package/dist/{chunk-VELT5VAG.js → chunk-JWSVLC4I.js} +1 -1
  29. package/dist/{chunk-BUJOMC3O.js → chunk-JZ6GORAK.js} +38 -2
  30. package/dist/{chunk-JQCS75DY.js → chunk-KFE2X7LG.js} +4 -4
  31. package/dist/{chunk-U2OMWI7Z.js → chunk-KSFUJT5B.js} +1 -1
  32. package/dist/{chunk-Y5JA4J2M.js → chunk-KXIAHHHP.js} +2 -2
  33. package/dist/{chunk-R2BI5UPK.js → chunk-NIOB6FTG.js} +1 -1
  34. package/dist/{chunk-X2JJ3CPG.js → chunk-NVGQO4VM.js} +8 -8
  35. package/dist/{chunk-EY6F2QXW.js → chunk-QYSWNPXZ.js} +1 -1
  36. package/dist/{chunk-SZZ5UQL7.js → chunk-RNAGPTKF.js} +6 -6
  37. package/dist/{chunk-KJC4SE7C.js → chunk-UBRD5FH4.js} +9 -9
  38. package/dist/{chunk-RDQGCHKD.js → chunk-VV2PJT4K.js} +3358 -227
  39. package/dist/{chunk-SMY35HJM.js → chunk-Z2JORLRT.js} +1 -1
  40. package/dist/{chunk-R7P2FMJT.js → chunk-ZBQ3ZY75.js} +436 -284
  41. package/dist/{ci-workflow-SZL3KVUK.js → ci-workflow-H52EMCL6.js} +3 -3
  42. package/dist/{dist-GRW3X2ZQ.js → dist-PGQ32ZI7.js} +41 -1
  43. package/dist/{dist-T3DGV5UN.js → dist-TG63OKA5.js} +2 -2
  44. package/dist/{docs-WDLJORLK.js → docs-GP4VUSIB.js} +4 -4
  45. package/dist/{engine-2YWYRCKK.js → engine-KQG5RVXX.js} +3 -3
  46. package/dist/{entropy-PJGTOORX.js → entropy-RXESIYKC.js} +3 -3
  47. package/dist/{feedback-M5KCJKL2.js → feedback-HS7ZYXOU.js} +1 -1
  48. package/dist/{generate-agent-definitions-MFDW6LZT.js → generate-agent-definitions-I4B7CVOB.js} +4 -4
  49. package/dist/{graph-loader-QMKXT454.js → graph-loader-TIVI6NQ7.js} +1 -1
  50. package/dist/index.d.ts +6 -0
  51. package/dist/index.js +25 -25
  52. package/dist/{loader-6O52FYHE.js → loader-CWAH6B4H.js} +3 -3
  53. package/dist/{mcp-GGNFWKVC.js → mcp-KHK5KYER.js} +15 -15
  54. package/dist/{performance-337U5URQ.js → performance-WDW5WS3J.js} +4 -4
  55. package/dist/{review-pipeline-DB5RD4SN.js → review-pipeline-QOZGWCTY.js} +3 -3
  56. package/dist/{runtime-B74EN2WD.js → runtime-SFEFRPLX.js} +3 -3
  57. package/dist/{scan-DXQUHGTT.js → scan-IJ7EYTF7.js} +1 -1
  58. package/dist/{security-3AYN6FVU.js → security-2K7UUK6T.js} +1 -1
  59. package/dist/{validate-35CD7VWN.js → validate-HXM6DXRZ.js} +4 -4
  60. package/dist/{validate-cross-check-LJKXBQYH.js → validate-cross-check-ULQLEOME.js} +3 -3
  61. package/package.json +9 -9
@@ -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,100 @@ 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 emptyMermaidResult(diagramType = "unknown") {
367
+ return {
368
+ entities: [],
369
+ relationships: [],
370
+ metadata: { format: "mermaid", diagramType }
371
+ };
372
+ }
373
+ function applyReactionBoost(candidate, nodeType, metadata) {
374
+ if (nodeType !== "conversation" || !metadata.reactions) return;
375
+ const reactions = metadata.reactions;
376
+ const totalReactions = Object.values(reactions).reduce((sum, count) => sum + count, 0);
377
+ if (totalReactions > 0) {
378
+ candidate.confidence = Math.min(1, Math.round((candidate.confidence + 0.1) * 100) / 100);
379
+ }
380
+ }
381
+ function detectLanguage(filePath) {
382
+ const name = path9.basename(filePath);
383
+ for (const skip of SKIP_EXTENSIONS2) {
384
+ if (name.endsWith(skip)) return void 0;
385
+ }
386
+ const ext = path9.extname(filePath);
387
+ return EXT_TO_LANGUAGE[ext];
388
+ }
389
+ function createExtractionRunner() {
390
+ return new ExtractionRunner(ALL_EXTRACTORS);
391
+ }
392
+ function levenshteinDistance(a, b) {
393
+ const m = a.length;
394
+ const n = b.length;
395
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
396
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
397
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
398
+ for (let i = 1; i <= m; i++) {
399
+ for (let j = 1; j <= n; j++) {
400
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
401
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
402
+ }
403
+ }
404
+ return dp[m][n];
405
+ }
406
+ function levenshteinRatio(a, b) {
407
+ const maxLen = Math.max(a.length, b.length);
408
+ if (maxLen === 0) return 1;
409
+ return 1 - levenshteinDistance(a, b) / maxLen;
410
+ }
411
+ function classifyConflict(a, b) {
412
+ const factTypes = /* @__PURE__ */ new Set([
413
+ "business_fact",
414
+ "business_metric",
415
+ "design_token"
416
+ ]);
417
+ const termTypes = /* @__PURE__ */ new Set(["business_term", "business_concept"]);
418
+ if (factTypes.has(a.type) || factTypes.has(b.type)) return "value_mismatch";
419
+ if (termTypes.has(a.type) || termTypes.has(b.type)) return "definition_conflict";
420
+ if (a.lastModified && b.lastModified && a.lastModified !== b.lastModified)
421
+ return "temporal_conflict";
422
+ return "status_divergence";
423
+ }
424
+ function toGrade(score) {
425
+ if (score >= 80) return "A";
426
+ if (score >= 60) return "B";
427
+ if (score >= 40) return "C";
428
+ if (score >= 20) return "D";
429
+ return "F";
430
+ }
431
+ function withRetry(client, options) {
432
+ const maxRetries = options?.maxRetries ?? 3;
433
+ const baseDelayMs = options?.baseDelayMs ?? 1e3;
434
+ const maxDelayMs = options?.maxDelayMs ?? 3e4;
435
+ return async (url, requestOptions) => {
436
+ let lastError;
437
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
438
+ try {
439
+ const response = await client(url, requestOptions);
440
+ if (response.ok || !RETRYABLE_STATUSES.has(response.status ?? 0)) {
441
+ return response;
442
+ }
443
+ if (attempt === maxRetries) {
444
+ return response;
445
+ }
446
+ } catch (err) {
447
+ lastError = err instanceof Error ? err : new Error(String(err));
448
+ if (attempt === maxRetries) {
449
+ throw lastError;
450
+ }
451
+ }
452
+ const exponentialDelay = baseDelayMs * 2 ** attempt;
453
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
454
+ const jitteredDelay = cappedDelay * (0.5 + Math.random() * 0.5);
455
+ await new Promise((resolve) => setTimeout(resolve, jitteredDelay));
456
+ }
457
+ throw lastError ?? new Error("Request failed after retries");
458
+ };
459
+ }
356
460
  function sanitizeExternalText(text, maxLength = 2e3) {
357
461
  let sanitized = text;
358
462
  for (const rule of SANITIZE_RULES) {
@@ -383,6 +487,44 @@ function linkToCode(store, content, sourceNodeId, edgeType, options) {
383
487
  }
384
488
  return edgesCreated;
385
489
  }
490
+ async function condenseContent(raw, options, summarizeFn) {
491
+ const originalLength = raw.length;
492
+ const { maxLength } = options;
493
+ const summarizationThreshold = options.summarizationThreshold ?? maxLength * 2;
494
+ if (raw.length <= maxLength) {
495
+ return { content: raw, method: "passthrough", originalLength };
496
+ }
497
+ if (raw.length < summarizationThreshold) {
498
+ return {
499
+ content: sanitizeExternalText(raw, maxLength),
500
+ method: "truncated",
501
+ originalLength
502
+ };
503
+ }
504
+ if (options.modelEndpoint && summarizeFn) {
505
+ try {
506
+ const summarized = await summarizeFn(SUMMARIZE_PROMPT + raw, {
507
+ endpoint: options.modelEndpoint,
508
+ model: options.modelName ?? "default",
509
+ maxTokens: Math.ceil(maxLength / 4)
510
+ // rough token estimate
511
+ });
512
+ const finalContent = summarized.length > maxLength ? sanitizeExternalText(summarized, maxLength) : summarized;
513
+ return { content: finalContent, method: "summarized", originalLength };
514
+ } catch {
515
+ return {
516
+ content: sanitizeExternalText(raw, maxLength),
517
+ method: "truncated",
518
+ originalLength
519
+ };
520
+ }
521
+ }
522
+ return {
523
+ content: sanitizeExternalText(raw, maxLength),
524
+ method: "truncated",
525
+ originalLength
526
+ };
527
+ }
386
528
  function buildIngestResult(nodesAdded, edgesAdded, errors, start) {
387
529
  return {
388
530
  nodesAdded,
@@ -408,6 +550,28 @@ function buildJql(config) {
408
550
  }
409
551
  return jql;
410
552
  }
553
+ function parseAcceptanceCriteria(description) {
554
+ const criteria = [];
555
+ const checkboxRegex = /[-*]\s*\[[ x]\]\s*(.+)/gi;
556
+ let match;
557
+ while ((match = checkboxRegex.exec(description)) !== null) {
558
+ criteria.push(match[1].trim());
559
+ }
560
+ const gwtRegex = /Given\b.+?When\b.+?Then\b.+/gi;
561
+ while ((match = gwtRegex.exec(description)) !== null) {
562
+ criteria.push(match[0].trim());
563
+ }
564
+ return criteria;
565
+ }
566
+ function extractCustomFields(fields) {
567
+ const result = {};
568
+ for (const [key, value] of Object.entries(fields)) {
569
+ if (key.startsWith("customfield_") && value != null && typeof value === "string") {
570
+ result[key] = value;
571
+ }
572
+ }
573
+ return result;
574
+ }
411
575
  function missingApiKeyResult(envVar, start) {
412
576
  return {
413
577
  nodesAdded: 0,
@@ -473,6 +637,33 @@ function ingestRun(store, run) {
473
637
  }
474
638
  return { nodesAdded, edgesAdded };
475
639
  }
640
+ function hasConstraintKeyword(text) {
641
+ const lower = text.toLowerCase();
642
+ return CONSTRAINT_KEYWORDS.some((kw) => lower.includes(kw));
643
+ }
644
+ function buildIngestResult2(nodesAdded, edgesAdded, errors, start) {
645
+ return {
646
+ nodesAdded,
647
+ nodesUpdated: 0,
648
+ edgesAdded,
649
+ edgesUpdated: 0,
650
+ errors,
651
+ durationMs: Date.now() - start
652
+ };
653
+ }
654
+ function stripHtml(html) {
655
+ return html.replace(/<[^>]*>/g, "");
656
+ }
657
+ function buildIngestResult3(nodesAdded, edgesAdded, errors, start) {
658
+ return {
659
+ nodesAdded,
660
+ nodesUpdated: 0,
661
+ edgesAdded,
662
+ edgesUpdated: 0,
663
+ errors,
664
+ durationMs: Date.now() - start
665
+ };
666
+ }
476
667
  function isSkippableWord(cleaned, allConsumed) {
477
668
  if (allConsumed.has(cleaned)) return true;
478
669
  const lower = cleaned.toLowerCase();
@@ -700,7 +891,7 @@ function isDTCGToken(obj) {
700
891
  }
701
892
  async function readFileOrNull(filePath) {
702
893
  try {
703
- return await fs6.readFile(filePath, "utf-8");
894
+ return await fs11.readFile(filePath, "utf-8");
704
895
  } catch {
705
896
  return null;
706
897
  }
@@ -782,7 +973,7 @@ function parseAntiPatterns(content) {
782
973
  }
783
974
  return patterns;
784
975
  }
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;
976
+ 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, DiagramParser, HEURISTIC_PATTERNS, SCANNABLE_TYPES, KnowledgeLinker, StructuralDriftDetector, KnowledgeStagingAggregator, SKIP_DIRS, EXT_TO_LANGUAGE, SKIP_EXTENSIONS2, EXTRACTOR_EDGE_TYPE, ExtractionRunner, PATTERNS, TestDescriptionExtractor, EnumConstantExtractor, ValidationRuleExtractor, ApiPathExtractor, ALL_EXTRACTORS, ImageAnalysisExtractor, KNOWLEDGE_NODE_TYPES, SIMILARITY_THRESHOLD, 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
977
  var init_dist = __esm({
787
978
  "../graph/dist/index.mjs"() {
788
979
  NODE_TYPES = [
@@ -822,6 +1013,7 @@ var init_dist = __esm({
822
1013
  "design_token",
823
1014
  "aesthetic_intent",
824
1015
  "design_constraint",
1016
+ "image_annotation",
825
1017
  // Traceability
826
1018
  "requirement",
827
1019
  // Business Knowledge
@@ -830,6 +1022,7 @@ var init_dist = __esm({
830
1022
  "business_concept",
831
1023
  "business_term",
832
1024
  "business_metric",
1025
+ "business_fact",
833
1026
  // Cache
834
1027
  "packed_summary"
835
1028
  ];
@@ -869,6 +1062,7 @@ var init_dist = __esm({
869
1062
  // Business Knowledge relationships
870
1063
  "governs",
871
1064
  "measures",
1065
+ "annotates",
872
1066
  // Cache relationships
873
1067
  "caches"
874
1068
  ];
@@ -1334,7 +1528,17 @@ var init_dist = __esm({
1334
1528
  typescript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/,
1335
1529
  javascript: /(?:export\s+)?(?:const|let|var)\s+(\w+)/
1336
1530
  };
1337
- SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"]);
1531
+ SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
1532
+ ".ts",
1533
+ ".tsx",
1534
+ ".js",
1535
+ ".jsx",
1536
+ ".py",
1537
+ ".go",
1538
+ ".rs",
1539
+ ".java",
1540
+ ".groovy"
1541
+ ]);
1338
1542
  SKIP_EXTENSIONS = /* @__PURE__ */ new Set([".d.ts"]);
1339
1543
  CodeIngestor = class {
1340
1544
  constructor(store) {
@@ -1348,23 +1552,34 @@ var init_dist = __esm({
1348
1552
  let edgesAdded = 0;
1349
1553
  const files = await this.findSourceFiles(rootDir);
1350
1554
  const nameToFiles = /* @__PURE__ */ new Map();
1351
- const fileContents = /* @__PURE__ */ new Map();
1352
1555
  for (const filePath of files) {
1353
1556
  try {
1354
- const result = await this.processFile(filePath, rootDir, nameToFiles, fileContents);
1557
+ const result = await this.processFile(filePath, rootDir, nameToFiles);
1355
1558
  nodesAdded += result.nodesAdded;
1356
1559
  edgesAdded += result.edgesAdded;
1357
1560
  } catch (err) {
1358
1561
  errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
1359
1562
  }
1360
1563
  }
1361
- const callsEdges = this.extractCallsEdges(nameToFiles, fileContents);
1362
- for (const edge of callsEdges) {
1363
- this.store.addEdge(edge);
1364
- edgesAdded++;
1564
+ for (const filePath of files) {
1565
+ try {
1566
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
1567
+ const content = await fs.readFile(filePath, "utf-8");
1568
+ const callsEdges = this.extractCallsEdgesForFile(relativePath, content, nameToFiles);
1569
+ for (const edge of callsEdges) {
1570
+ this.store.addEdge(edge);
1571
+ edgesAdded++;
1572
+ }
1573
+ } catch {
1574
+ }
1575
+ }
1576
+ for (const filePath of files) {
1577
+ try {
1578
+ const content = await fs.readFile(filePath, "utf-8");
1579
+ edgesAdded += this.extractReqAnnotationsForFile(filePath, content, rootDir);
1580
+ } catch {
1581
+ }
1365
1582
  }
1366
- edgesAdded += this.extractReqAnnotations(fileContents, rootDir);
1367
- fileContents.clear();
1368
1583
  return {
1369
1584
  nodesAdded,
1370
1585
  nodesUpdated: 0,
@@ -1374,14 +1589,13 @@ var init_dist = __esm({
1374
1589
  durationMs: Date.now() - start
1375
1590
  };
1376
1591
  }
1377
- async processFile(filePath, rootDir, nameToFiles, fileContents) {
1592
+ async processFile(filePath, rootDir, nameToFiles) {
1378
1593
  let nodesAdded = 0;
1379
1594
  let edgesAdded = 0;
1380
1595
  const relativePath = path.relative(rootDir, filePath).replace(/\\/g, "/");
1381
1596
  const content = await fs.readFile(filePath, "utf-8");
1382
1597
  const stat2 = await fs.stat(filePath);
1383
1598
  const fileId = `file:${relativePath}`;
1384
- fileContents.set(relativePath, content);
1385
1599
  const fileNode = {
1386
1600
  id: fileId,
1387
1601
  type: "file",
@@ -1421,7 +1635,7 @@ var init_dist = __esm({
1421
1635
  const entries = await fs.readdir(dir, { withFileTypes: true });
1422
1636
  for (const entry of entries) {
1423
1637
  const fullPath = path.join(dir, entry.name);
1424
- if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist") {
1638
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== "__pycache__" && entry.name !== ".venv" && entry.name !== ".turbo" && entry.name !== ".harness" && entry.name !== ".next" && entry.name !== ".vscode" && entry.name !== ".idea" && entry.name !== "vendor" && entry.name !== "out" && entry.name !== ".gradle" && entry.name !== ".gradle-home" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage" && entry.name !== ".nyc_output") {
1425
1639
  results.push(...await this.findSourceFiles(fullPath));
1426
1640
  } else if (entry.isFile() && isSupportedSourceFile(entry.name)) {
1427
1641
  results.push(fullPath);
@@ -1700,33 +1914,31 @@ var init_dist = __esm({
1700
1914
  return false;
1701
1915
  }
1702
1916
  /**
1703
- * Second pass: scan each file for identifiers matching known callable names,
1917
+ * Scan a file for identifiers matching known callable names,
1704
1918
  * then create file-to-file "calls" edges. Uses regex heuristic (not AST).
1705
1919
  */
1706
- extractCallsEdges(nameToFiles, fileContents) {
1920
+ extractCallsEdgesForFile(relativePath, content, nameToFiles) {
1707
1921
  const edges = [];
1708
1922
  const seen = /* @__PURE__ */ new Set();
1709
- for (const [filePath, content] of fileContents) {
1710
- const callerFileId = `file:${filePath}`;
1711
- const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
1712
- let match;
1713
- while ((match = callPattern.exec(content)) !== null) {
1714
- const name = match[1];
1715
- const targetFiles = nameToFiles.get(name);
1716
- if (!targetFiles) continue;
1717
- for (const targetFile of targetFiles) {
1718
- if (targetFile === filePath) continue;
1719
- const targetFileId = `file:${targetFile}`;
1720
- const key = `${callerFileId}|${targetFileId}`;
1721
- if (seen.has(key)) continue;
1722
- seen.add(key);
1723
- edges.push({
1724
- from: callerFileId,
1725
- to: targetFileId,
1726
- type: "calls",
1727
- metadata: { confidence: "regex" }
1728
- });
1729
- }
1923
+ const callerFileId = `file:${relativePath}`;
1924
+ const callPattern = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
1925
+ let match;
1926
+ while ((match = callPattern.exec(content)) !== null) {
1927
+ const name = match[1];
1928
+ const targetFiles = nameToFiles.get(name);
1929
+ if (!targetFiles) continue;
1930
+ for (const targetFile of targetFiles) {
1931
+ if (targetFile === relativePath) continue;
1932
+ const targetFileId = `file:${targetFile}`;
1933
+ const key = `${callerFileId}|${targetFileId}`;
1934
+ if (seen.has(key)) continue;
1935
+ seen.add(key);
1936
+ edges.push({
1937
+ from: callerFileId,
1938
+ to: targetFileId,
1939
+ type: "calls",
1940
+ metadata: { confidence: "regex" }
1941
+ });
1730
1942
  }
1731
1943
  }
1732
1944
  return edges;
@@ -1871,6 +2083,7 @@ var init_dist = __esm({
1871
2083
  if (/\.go$/.test(filePath)) return "go";
1872
2084
  if (/\.rs$/.test(filePath)) return "rust";
1873
2085
  if (/\.java$/.test(filePath)) return "java";
2086
+ if (/\.groovy$/.test(filePath)) return "java";
1874
2087
  return "unknown";
1875
2088
  }
1876
2089
  /**
@@ -1878,40 +2091,38 @@ var init_dist = __esm({
1878
2091
  * linking requirement nodes to the annotated files.
1879
2092
  * Format: // @req <feature-name>#<index>
1880
2093
  */
1881
- extractReqAnnotations(fileContents, rootDir) {
2094
+ extractReqAnnotationsForFile(filePath, content, rootDir) {
1882
2095
  const REQ_TAG = /(?:\/\/|#|\/\*)\s*@req\s+([\w-]+)#(\d+)/g;
1883
2096
  const reqNodes = this.store.findNodes({ type: "requirement" });
1884
2097
  let edgesAdded = 0;
1885
- for (const [filePath, content] of fileContents) {
1886
- let match;
1887
- REQ_TAG.lastIndex = 0;
1888
- while ((match = REQ_TAG.exec(content)) !== null) {
1889
- const featureName = match[1];
1890
- const reqIndex = parseInt(match[2], 10);
1891
- const reqNode = reqNodes.find(
1892
- (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
2098
+ let match;
2099
+ REQ_TAG.lastIndex = 0;
2100
+ while ((match = REQ_TAG.exec(content)) !== null) {
2101
+ const featureName = match[1];
2102
+ const reqIndex = parseInt(match[2], 10);
2103
+ const reqNode = reqNodes.find(
2104
+ (n) => n.metadata.featureName === featureName && n.metadata.index === reqIndex
2105
+ );
2106
+ if (!reqNode) {
2107
+ console.warn(
2108
+ `@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
1893
2109
  );
1894
- if (!reqNode) {
1895
- console.warn(
1896
- `@req annotation references non-existent requirement: ${featureName}#${reqIndex} in ${filePath}`
1897
- );
1898
- continue;
1899
- }
1900
- const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
1901
- const fileNodeId = `file:${relPath}`;
1902
- this.store.addEdge({
1903
- from: reqNode.id,
1904
- to: fileNodeId,
1905
- type: "verified_by",
1906
- confidence: 1,
1907
- metadata: {
1908
- method: "annotation",
1909
- tag: `@req ${featureName}#${reqIndex}`,
1910
- confidence: 1
1911
- }
1912
- });
1913
- edgesAdded++;
2110
+ continue;
1914
2111
  }
2112
+ const relPath = path.relative(rootDir, filePath).replace(/\\/g, "/");
2113
+ const fileNodeId = `file:${relPath}`;
2114
+ this.store.addEdge({
2115
+ from: reqNode.id,
2116
+ to: fileNodeId,
2117
+ type: "verified_by",
2118
+ confidence: 1,
2119
+ metadata: {
2120
+ method: "annotation",
2121
+ tag: `@req ${featureName}#${reqIndex}`,
2122
+ confidence: 1
2123
+ }
2124
+ });
2125
+ edgesAdded++;
1915
2126
  }
1916
2127
  return edgesAdded;
1917
2128
  }
@@ -2272,7 +2483,7 @@ var init_dist = __esm({
2272
2483
  const entries = await fs2.readdir(dir, { withFileTypes: true });
2273
2484
  for (const entry of entries) {
2274
2485
  const fullPath = path3.join(dir, entry.name);
2275
- if (entry.isDirectory()) {
2486
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
2276
2487
  results.push(...await this.findMarkdownFiles(fullPath));
2277
2488
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
2278
2489
  results.push(fullPath);
@@ -2411,7 +2622,7 @@ var init_dist = __esm({
2411
2622
  const entries = await fs3.readdir(dir, { withFileTypes: true });
2412
2623
  for (const entry of entries) {
2413
2624
  const fullPath = path4.join(dir, entry.name);
2414
- if (entry.isDirectory()) {
2625
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "target" && entry.name !== "build" && entry.name !== ".git" && entry.name !== ".gradle" && entry.name !== ".harness" && entry.name !== "vendor" && entry.name !== "bin" && entry.name !== "obj" && entry.name !== "venv" && entry.name !== "_build" && entry.name !== "deps" && entry.name !== "coverage") {
2415
2626
  results.push(...await this.findMarkdownFiles(fullPath));
2416
2627
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
2417
2628
  results.push(fullPath);
@@ -2654,131 +2865,2648 @@ var init_dist = __esm({
2654
2865
  return count;
2655
2866
  }
2656
2867
  };
2657
- CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2658
- SANITIZE_RULES = [
2659
- // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
2660
- {
2661
- pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
2662
- replacement: ""
2663
- },
2664
- // Strip markdown-style system prompt markers (including trailing space)
2665
- {
2666
- pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
2667
- replacement: ""
2668
- },
2669
- // Strip common injection prefixes
2670
- {
2671
- pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
2672
- replacement: "[filtered]"
2673
- },
2674
- // Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
2675
- {
2676
- pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
2677
- replacement: "[filtered]"
2868
+ MermaidParser = class {
2869
+ canParse(_content, ext) {
2870
+ return ext === ".mmd" || ext === ".mermaid";
2678
2871
  }
2679
- ];
2680
- SyncManager = class {
2681
- constructor(store, graphDir) {
2682
- this.store = store;
2683
- this.metadataPath = path6.join(graphDir, "sync-metadata.json");
2872
+ parse(content, _filePath) {
2873
+ const trimmed = content.trim();
2874
+ if (!trimmed) {
2875
+ return emptyMermaidResult();
2876
+ }
2877
+ const diagramType = this.detectDiagramType(trimmed);
2878
+ switch (diagramType) {
2879
+ case "flowchart":
2880
+ return this.parseFlowchart(trimmed, diagramType);
2881
+ case "sequence":
2882
+ return this.parseSequence(trimmed, diagramType);
2883
+ default:
2884
+ return emptyMermaidResult(diagramType);
2885
+ }
2684
2886
  }
2685
- store;
2686
- registrations = /* @__PURE__ */ new Map();
2687
- metadataPath;
2688
- registerConnector(connector, config) {
2689
- this.registrations.set(connector.name, { connector, config });
2887
+ detectDiagramType(content) {
2888
+ const lines = content.split("\n");
2889
+ for (const line of lines) {
2890
+ const trimmedLine = line.trim();
2891
+ if (!trimmedLine) continue;
2892
+ if (/^(?:graph|flowchart)\b/i.test(trimmedLine)) return "flowchart";
2893
+ if (/^sequenceDiagram\b/.test(trimmedLine)) return "sequence";
2894
+ if (/^classDiagram\b/.test(trimmedLine)) return "class";
2895
+ if (/^erDiagram\b/.test(trimmedLine)) return "er";
2896
+ break;
2897
+ }
2898
+ return "unknown";
2690
2899
  }
2691
- async sync(connectorName) {
2692
- const registration = this.registrations.get(connectorName);
2693
- if (!registration) {
2694
- return {
2695
- nodesAdded: 0,
2696
- nodesUpdated: 0,
2697
- edgesAdded: 0,
2698
- edgesUpdated: 0,
2699
- errors: [`Connector "${connectorName}" not registered`],
2700
- durationMs: 0
2701
- };
2900
+ parseFlowchart(content, diagramType) {
2901
+ const entities = /* @__PURE__ */ new Map();
2902
+ const relationships = [];
2903
+ const nodeRegex = /([A-Za-z0-9_]+)\s*[[({]([^\])}]+)[\])}]/g;
2904
+ let match;
2905
+ match = nodeRegex.exec(content);
2906
+ while (match !== null) {
2907
+ const id = match[1] ?? "";
2908
+ const label = (match[2] ?? "").trim();
2909
+ if (id && !entities.has(id)) {
2910
+ const isDecision = this.isDecisionNode(content, id);
2911
+ entities.set(id, {
2912
+ id,
2913
+ label,
2914
+ ...isDecision ? { type: "decision" } : {}
2915
+ });
2916
+ }
2917
+ match = nodeRegex.exec(content);
2918
+ }
2919
+ const stripped = content.replace(/[[({][^\])}]*[\])}]/g, "");
2920
+ const labeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\|([^|]+)\|\s*([A-Za-z0-9_]+)/g;
2921
+ const unlabeledEdgeRegex = /([A-Za-z0-9_]+)\s*--+>?\s+([A-Za-z0-9_]+)/g;
2922
+ const edgeKeys = /* @__PURE__ */ new Set();
2923
+ match = labeledEdgeRegex.exec(stripped);
2924
+ while (match !== null) {
2925
+ const from = match[1] ?? "";
2926
+ const label = (match[2] ?? "").trim();
2927
+ const to = match[3] ?? "";
2928
+ if (from && to) {
2929
+ const key = `${from}->${to}:${label}`;
2930
+ if (!edgeKeys.has(key)) {
2931
+ edgeKeys.add(key);
2932
+ relationships.push({ from, to, label });
2933
+ }
2934
+ }
2935
+ match = labeledEdgeRegex.exec(stripped);
2936
+ }
2937
+ match = unlabeledEdgeRegex.exec(stripped);
2938
+ while (match !== null) {
2939
+ const from = match[1] ?? "";
2940
+ const to = match[2] ?? "";
2941
+ if (from && to) {
2942
+ const hasLabeled = relationships.some((r) => r.from === from && r.to === to);
2943
+ if (!hasLabeled) {
2944
+ const key = `${from}->${to}`;
2945
+ if (!edgeKeys.has(key)) {
2946
+ edgeKeys.add(key);
2947
+ relationships.push({ from, to });
2948
+ }
2949
+ }
2950
+ }
2951
+ match = unlabeledEdgeRegex.exec(stripped);
2702
2952
  }
2703
- const { connector, config } = registration;
2704
- const result = await connector.ingest(this.store, config);
2705
- const metadata = await this.loadMetadata();
2706
- metadata.connectors[connectorName] = {
2707
- lastSyncTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
2708
- lastResult: result
2953
+ return {
2954
+ entities: Array.from(entities.values()),
2955
+ relationships,
2956
+ metadata: { format: "mermaid", diagramType }
2709
2957
  };
2710
- await this.saveMetadata(metadata);
2711
- return result;
2712
2958
  }
2713
- async syncAll() {
2714
- const combined = {
2715
- nodesAdded: 0,
2716
- nodesUpdated: 0,
2717
- edgesAdded: 0,
2718
- edgesUpdated: 0,
2719
- errors: [],
2720
- durationMs: 0
2959
+ isDecisionNode(content, nodeId) {
2960
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2961
+ return decisionRegex.test(content);
2962
+ }
2963
+ parseSequence(content, diagramType) {
2964
+ const entities = [];
2965
+ const relationships = [];
2966
+ const seenParticipants = /* @__PURE__ */ new Set();
2967
+ const participantRegex = /participant\s+(\w+)(?:\s+as\s+(.+))?/g;
2968
+ let match;
2969
+ match = participantRegex.exec(content);
2970
+ while (match !== null) {
2971
+ const id = match[1] ?? "";
2972
+ const label = match[2]?.trim() || id;
2973
+ if (id && !seenParticipants.has(id)) {
2974
+ seenParticipants.add(id);
2975
+ entities.push({ id, label });
2976
+ }
2977
+ match = participantRegex.exec(content);
2978
+ }
2979
+ const forwardMsgRegex = /(\w+)\s*->>?\+?\s*(\w+)\s*:\s*(.+)/g;
2980
+ match = forwardMsgRegex.exec(content);
2981
+ while (match !== null) {
2982
+ const from = match[1] ?? "";
2983
+ const to = match[2] ?? "";
2984
+ const label = (match[3] ?? "").trim();
2985
+ if (from && to) {
2986
+ relationships.push({ from, to, label });
2987
+ }
2988
+ match = forwardMsgRegex.exec(content);
2989
+ }
2990
+ const returnMsgRegex = /(\w+)\s*-->>?-?\s*(\w+)\s*:\s*(.+)/g;
2991
+ match = returnMsgRegex.exec(content);
2992
+ while (match !== null) {
2993
+ const from = match[1] ?? "";
2994
+ const to = match[2] ?? "";
2995
+ const label = (match[3] ?? "").trim();
2996
+ if (from && to) {
2997
+ relationships.push({ from, to, label });
2998
+ }
2999
+ match = returnMsgRegex.exec(content);
3000
+ }
3001
+ return {
3002
+ entities,
3003
+ relationships,
3004
+ metadata: { format: "mermaid", diagramType }
2721
3005
  };
2722
- for (const [name] of this.registrations) {
2723
- const result = await this.sync(name);
2724
- combined.nodesAdded += result.nodesAdded;
2725
- combined.nodesUpdated += result.nodesUpdated;
2726
- combined.edgesAdded += result.edgesAdded;
2727
- combined.edgesUpdated += result.edgesUpdated;
2728
- combined.errors.push(...result.errors);
2729
- combined.durationMs += result.durationMs;
3006
+ }
3007
+ };
3008
+ D2Parser = class {
3009
+ canParse(_content, ext) {
3010
+ return ext === ".d2";
3011
+ }
3012
+ parse(content, _filePath) {
3013
+ const trimmed = content.trim();
3014
+ if (!trimmed) {
3015
+ return {
3016
+ entities: [],
3017
+ relationships: [],
3018
+ metadata: { format: "d2", diagramType: "architecture" }
3019
+ };
2730
3020
  }
2731
- return combined;
3021
+ const entities = /* @__PURE__ */ new Map();
3022
+ const relationships = [];
3023
+ let braceDepth = 0;
3024
+ for (const line of trimmed.split("\n")) {
3025
+ const stripped = line.trim();
3026
+ if (!stripped || stripped.startsWith("#")) continue;
3027
+ if (stripped.endsWith("{")) {
3028
+ if (braceDepth === 0) {
3029
+ const shapeMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+?)\s*\{$/);
3030
+ if (shapeMatch) {
3031
+ const id = shapeMatch[1];
3032
+ const label = shapeMatch[2];
3033
+ if (id && label) {
3034
+ entities.set(id, { id, label });
3035
+ }
3036
+ }
3037
+ }
3038
+ braceDepth++;
3039
+ continue;
3040
+ }
3041
+ if (stripped === "}") {
3042
+ braceDepth = Math.max(0, braceDepth - 1);
3043
+ continue;
3044
+ }
3045
+ if (braceDepth > 0) continue;
3046
+ const connMatch = stripped.match(
3047
+ /^([a-zA-Z0-9_.-]+)\s*->\s*([a-zA-Z0-9_.-]+)(?:\s*:\s*(.+?))?$/
3048
+ );
3049
+ if (connMatch) {
3050
+ const from = connMatch[1] ?? "";
3051
+ const to = connMatch[2] ?? "";
3052
+ const label = connMatch[3]?.trim();
3053
+ if (from && to) {
3054
+ relationships.push({ from, to, ...label ? { label } : {} });
3055
+ }
3056
+ continue;
3057
+ }
3058
+ const simpleMatch = stripped.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
3059
+ if (simpleMatch) {
3060
+ const id = simpleMatch[1];
3061
+ const label = (simpleMatch[2] ?? "").trim();
3062
+ if (id && label && !entities.has(id)) {
3063
+ entities.set(id, { id, label });
3064
+ }
3065
+ }
3066
+ }
3067
+ return {
3068
+ entities: Array.from(entities.values()),
3069
+ relationships,
3070
+ metadata: { format: "d2", diagramType: "architecture" }
3071
+ };
2732
3072
  }
2733
- async getMetadata() {
2734
- return this.loadMetadata();
3073
+ };
3074
+ PlantUmlParser = class {
3075
+ canParse(_content, ext) {
3076
+ return ext === ".puml" || ext === ".plantuml";
2735
3077
  }
2736
- async loadMetadata() {
2737
- try {
2738
- const raw = await fs5.readFile(this.metadataPath, "utf-8");
2739
- return JSON.parse(raw);
2740
- } catch {
2741
- return { connectors: {} };
3078
+ parse(content, _filePath) {
3079
+ const trimmed = content.trim();
3080
+ if (!trimmed) {
3081
+ return {
3082
+ entities: [],
3083
+ relationships: [],
3084
+ metadata: { format: "plantuml", diagramType: "unknown" }
3085
+ };
3086
+ }
3087
+ const diagramType = this.detectDiagramType(trimmed);
3088
+ const entities = /* @__PURE__ */ new Map();
3089
+ const relationships = [];
3090
+ const body = trimmed.replace(/@startuml\b.*\n?/, "").replace(/@enduml\b.*/, "").trim();
3091
+ const classRegex = /class\s+(\w+)/g;
3092
+ let match;
3093
+ match = classRegex.exec(body);
3094
+ while (match !== null) {
3095
+ const id = match[1] ?? "";
3096
+ if (id && !entities.has(id)) {
3097
+ entities.set(id, { id, label: id });
3098
+ }
3099
+ match = classRegex.exec(body);
3100
+ }
3101
+ const componentRegex1 = /\[([^\]]+)\]/g;
3102
+ match = componentRegex1.exec(body);
3103
+ while (match !== null) {
3104
+ const label = (match[1] ?? "").trim();
3105
+ if (label) {
3106
+ const id = label.replace(/\s+/g, "_");
3107
+ if (!entities.has(id)) {
3108
+ entities.set(id, { id, label });
3109
+ }
3110
+ }
3111
+ match = componentRegex1.exec(body);
3112
+ }
3113
+ const relRegex = /(\w+)\s*(?:-->|->|<--|<-|\.\.>|--)\s*(\w+)(?:\s*:\s*(.+))?/g;
3114
+ match = relRegex.exec(body);
3115
+ while (match !== null) {
3116
+ const from = match[1] ?? "";
3117
+ const to = match[2] ?? "";
3118
+ const label = match[3]?.trim();
3119
+ if (from && to) {
3120
+ relationships.push({ from, to, ...label ? { label } : {} });
3121
+ }
3122
+ match = relRegex.exec(body);
2742
3123
  }
3124
+ return {
3125
+ entities: Array.from(entities.values()),
3126
+ relationships,
3127
+ metadata: { format: "plantuml", diagramType }
3128
+ };
2743
3129
  }
2744
- async saveMetadata(metadata) {
2745
- await fs5.mkdir(path6.dirname(this.metadataPath), { recursive: true });
2746
- await fs5.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
3130
+ detectDiagramType(content) {
3131
+ if (/class\s+\w+/.test(content)) return "class";
3132
+ if (/\[.+\]/.test(content) || /component\s+/.test(content)) return "component";
3133
+ if (/participant\s+/.test(content) || /actor\s+/.test(content)) return "sequence";
3134
+ return "unknown";
2747
3135
  }
2748
3136
  };
2749
- JiraConnector = class {
2750
- name = "jira";
2751
- source = "jira";
2752
- httpClient;
2753
- constructor(httpClient) {
2754
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
3137
+ DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
3138
+ DiagramParser = class {
3139
+ constructor(store) {
3140
+ this.store = store;
2755
3141
  }
2756
- async ingest(store, config) {
2757
- const start = Date.now();
2758
- const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
2759
- const apiKey = process.env[apiKeyEnv];
2760
- if (!apiKey) {
2761
- return buildIngestResult(
2762
- 0,
2763
- 0,
2764
- [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2765
- start
2766
- );
3142
+ store;
3143
+ parsers = [
3144
+ new MermaidParser(),
3145
+ new D2Parser(),
3146
+ new PlantUmlParser()
3147
+ ];
3148
+ parse(content, filePath) {
3149
+ const ext = path6.extname(filePath).toLowerCase();
3150
+ for (const parser of this.parsers) {
3151
+ if (parser.canParse(content, ext)) {
3152
+ return parser.parse(content, filePath);
3153
+ }
2767
3154
  }
2768
- const baseUrlEnv = config.baseUrlEnv ?? "JIRA_BASE_URL";
2769
- const baseUrl = process.env[baseUrlEnv];
2770
- if (!baseUrl) {
2771
- return buildIngestResult(
2772
- 0,
2773
- 0,
2774
- [`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
2775
- start
2776
- );
3155
+ return {
3156
+ entities: [],
3157
+ relationships: [],
3158
+ metadata: { format: "mermaid", diagramType: "unknown" }
3159
+ };
3160
+ }
3161
+ async ingest(projectDir) {
3162
+ const start = Date.now();
3163
+ let nodesAdded = 0;
3164
+ let edgesAdded = 0;
3165
+ const errors = [];
3166
+ const diagramFiles = await this.findDiagramFiles(projectDir);
3167
+ for (const filePath of diagramFiles) {
3168
+ try {
3169
+ const content = await fs5.readFile(filePath, "utf-8");
3170
+ const relPath = path6.relative(projectDir, filePath).replaceAll("\\", "/");
3171
+ const result = this.parse(content, filePath);
3172
+ if (result.entities.length === 0) continue;
3173
+ const pathHash = hash(relPath);
3174
+ for (const entity of result.entities) {
3175
+ const nodeId = `diagram:${pathHash}:${entity.id}`;
3176
+ this.store.addNode({
3177
+ id: nodeId,
3178
+ type: "business_concept",
3179
+ name: entity.label,
3180
+ path: relPath,
3181
+ metadata: {
3182
+ source: "diagram",
3183
+ format: result.metadata.format,
3184
+ diagramType: result.metadata.diagramType,
3185
+ confidence: 0.85,
3186
+ ...entity.type ? { entityType: entity.type } : {}
3187
+ }
3188
+ });
3189
+ nodesAdded++;
3190
+ }
3191
+ for (const rel of result.relationships) {
3192
+ const fromId = `diagram:${pathHash}:${rel.from}`;
3193
+ const toId = `diagram:${pathHash}:${rel.to}`;
3194
+ this.store.addEdge({
3195
+ from: fromId,
3196
+ to: toId,
3197
+ type: "references",
3198
+ metadata: {
3199
+ ...rel.label ? { label: rel.label } : {}
3200
+ }
3201
+ });
3202
+ edgesAdded++;
3203
+ }
3204
+ } catch (err) {
3205
+ errors.push(
3206
+ `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3207
+ );
3208
+ }
3209
+ }
3210
+ return {
3211
+ nodesAdded,
3212
+ nodesUpdated: 0,
3213
+ edgesAdded,
3214
+ edgesUpdated: 0,
3215
+ errors,
3216
+ durationMs: Date.now() - start
3217
+ };
3218
+ }
3219
+ async findDiagramFiles(dir) {
3220
+ const files = [];
3221
+ const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".harness"]);
3222
+ const walk = async (currentDir) => {
3223
+ let entries;
3224
+ try {
3225
+ entries = await fs5.readdir(currentDir, { withFileTypes: true });
3226
+ } catch {
3227
+ return;
3228
+ }
3229
+ for (const entry of entries) {
3230
+ if (entry.isDirectory()) {
3231
+ if (!SKIP_DIRS2.has(entry.name)) {
3232
+ await walk(path6.join(currentDir, entry.name));
3233
+ }
3234
+ } else if (entry.isFile()) {
3235
+ const ext = path6.extname(entry.name).toLowerCase();
3236
+ if (DIAGRAM_EXTENSIONS.has(ext)) {
3237
+ files.push(path6.join(currentDir, entry.name));
3238
+ }
3239
+ }
3240
+ }
3241
+ };
3242
+ await walk(dir);
3243
+ return files;
3244
+ }
3245
+ };
3246
+ HEURISTIC_PATTERNS = [
3247
+ {
3248
+ name: "business-rule-imperative",
3249
+ pattern: /\b(?:must|shall|required)\b.*\b(?:system|service|api|user|data|app|application|client|server|process|handler|module)\b/i,
3250
+ signal: "Business rule",
3251
+ confidence: 0.7,
3252
+ nodeType: "business_fact"
3253
+ },
3254
+ {
3255
+ name: "sla-slo-pattern",
3256
+ 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,
3257
+ signal: "Business constraint (SLA/SLO)",
3258
+ confidence: 0.8,
3259
+ nodeType: "business_fact"
3260
+ },
3261
+ {
3262
+ name: "monetary-amount",
3263
+ pattern: /\$[\d,]+(?:\.\d{2})?\s*(?:\b(?:cost|revenue|budget|price|fee|license|subscription|annual|monthly)\b)?/i,
3264
+ signal: "Business fact (monetary)",
3265
+ confidence: 0.6,
3266
+ nodeType: "business_fact"
3267
+ },
3268
+ {
3269
+ name: "acceptance-criteria",
3270
+ pattern: /\b(?:Given\b.*\bWhen\b.*\bThen\b|\[[ x]\])/i,
3271
+ signal: "Business rule (acceptance criteria)",
3272
+ confidence: 0.8,
3273
+ nodeType: "business_fact"
3274
+ },
3275
+ {
3276
+ name: "regulatory-reference",
3277
+ pattern: /\b(?:GDPR|SOC\s*2|PCI(?:\s*-?\s*DSS)?|HIPAA|CCPA|FERPA|SOX)\b/i,
3278
+ signal: "Business rule (regulatory)",
3279
+ confidence: 0.9,
3280
+ nodeType: "business_fact"
3281
+ }
3282
+ ];
3283
+ SCANNABLE_TYPES = ["issue", "conversation", "document"];
3284
+ KnowledgeLinker = class {
3285
+ constructor(store, outputDir) {
3286
+ this.store = store;
3287
+ this.outputDir = outputDir;
3288
+ }
3289
+ store;
3290
+ outputDir;
3291
+ async link() {
3292
+ const errors = [];
3293
+ const candidates = this.scanAllNodes(errors);
3294
+ await this.writeJsonl(candidates);
3295
+ const conceptsClustered = this.clusterBySource(candidates);
3296
+ const promoted = this.promoteAndDeduplicate(candidates);
3297
+ await this.writeStagedJsonl(promoted.staged);
3298
+ return {
3299
+ factsCreated: promoted.factsCreated,
3300
+ conceptsClustered,
3301
+ duplicatesMerged: promoted.duplicatesMerged,
3302
+ stagedForReview: promoted.staged.length,
3303
+ errors
3304
+ };
3305
+ }
3306
+ /** Stage 1: Scan all scannable node types for heuristic pattern matches. */
3307
+ scanAllNodes(errors) {
3308
+ const candidates = [];
3309
+ for (const type of SCANNABLE_TYPES) {
3310
+ const nodes = this.store.findNodes({ type });
3311
+ for (const node of nodes) {
3312
+ if (!node.content) continue;
3313
+ try {
3314
+ const matches = this.scanPatterns(node.content, node.id, type);
3315
+ for (const match of matches) {
3316
+ applyReactionBoost(match, type, node.metadata);
3317
+ candidates.push(match);
3318
+ }
3319
+ } catch (err) {
3320
+ errors.push(
3321
+ `Scan failed for ${node.id}: ${err instanceof Error ? err.message : String(err)}`
3322
+ );
3323
+ }
3324
+ }
3325
+ }
3326
+ return candidates;
3327
+ }
3328
+ /** Stage 2: Group candidates by source node and create business_concept for 3+. */
3329
+ clusterBySource(candidates) {
3330
+ const sourceGroups = /* @__PURE__ */ new Map();
3331
+ for (const candidate of candidates) {
3332
+ const group = sourceGroups.get(candidate.sourceNodeId) ?? [];
3333
+ group.push(candidate);
3334
+ sourceGroups.set(candidate.sourceNodeId, group);
3335
+ }
3336
+ let clustered = 0;
3337
+ for (const [sourceNodeId, group] of sourceGroups) {
3338
+ if (group.length >= 3) {
3339
+ this.store.addNode({
3340
+ id: `concept:linker:${hash(sourceNodeId)}`,
3341
+ type: "business_concept",
3342
+ name: `Business concept cluster from ${sourceNodeId}`,
3343
+ metadata: {
3344
+ source: "knowledge-linker",
3345
+ sources: [sourceNodeId],
3346
+ factCount: group.length
3347
+ }
3348
+ });
3349
+ clustered++;
3350
+ }
3351
+ }
3352
+ return clustered;
3353
+ }
3354
+ /** Stage 3: Promote high-confidence candidates, stage medium, discard low. */
3355
+ promoteAndDeduplicate(candidates) {
3356
+ let factsCreated = 0;
3357
+ let duplicatesMerged = 0;
3358
+ const staged = [];
3359
+ for (const candidate of candidates) {
3360
+ if (candidate.confidence >= 0.8) {
3361
+ const merged = this.tryMergeDuplicate(candidate);
3362
+ if (merged) {
3363
+ duplicatesMerged++;
3364
+ continue;
3365
+ }
3366
+ this.createFactNode(candidate);
3367
+ factsCreated++;
3368
+ } else if (candidate.confidence >= 0.5) {
3369
+ staged.push(candidate);
3370
+ }
3371
+ }
3372
+ return { factsCreated, duplicatesMerged, staged };
3373
+ }
3374
+ /** Check for existing duplicate fact and merge sources if found. Returns true if merged. */
3375
+ tryMergeDuplicate(candidate) {
3376
+ if (this.store.getNode(candidate.id)) return true;
3377
+ const existingFacts = this.store.findNodes({ type: "business_fact" });
3378
+ const duplicate = existingFacts.find(
3379
+ (f) => f.content === candidate.content && f.id !== candidate.id
3380
+ );
3381
+ if (!duplicate) return false;
3382
+ const existingSources = duplicate.metadata.sources ?? [];
3383
+ if (!existingSources.includes(candidate.sourceNodeId)) {
3384
+ existingSources.push(candidate.sourceNodeId);
3385
+ duplicate.metadata.sources = existingSources;
3386
+ }
3387
+ return true;
3388
+ }
3389
+ /** Create a business_fact node and governs edge for a promoted candidate. */
3390
+ createFactNode(candidate) {
3391
+ this.store.addNode({
3392
+ id: candidate.id,
3393
+ type: candidate.nodeType,
3394
+ name: candidate.name,
3395
+ content: candidate.content,
3396
+ metadata: {
3397
+ source: "knowledge-linker",
3398
+ pattern: candidate.pattern,
3399
+ signal: candidate.signal,
3400
+ confidence: candidate.confidence,
3401
+ sourceNodeId: candidate.sourceNodeId,
3402
+ sourceNodeType: candidate.sourceNodeType,
3403
+ sources: [candidate.sourceNodeId]
3404
+ }
3405
+ });
3406
+ this.store.addEdge({
3407
+ from: candidate.id,
3408
+ to: candidate.sourceNodeId,
3409
+ type: "governs",
3410
+ confidence: candidate.confidence,
3411
+ metadata: { source: "knowledge-linker" }
3412
+ });
3413
+ }
3414
+ /**
3415
+ * Write candidates to JSONL file for audit trail.
3416
+ */
3417
+ async writeJsonl(candidates) {
3418
+ if (!this.outputDir) return;
3419
+ await fs6.mkdir(this.outputDir, { recursive: true });
3420
+ const filePath = path7.join(this.outputDir, "linker.jsonl");
3421
+ const lines = candidates.map(
3422
+ (c) => JSON.stringify({
3423
+ id: c.id,
3424
+ sourceNodeId: c.sourceNodeId,
3425
+ sourceNodeType: c.sourceNodeType,
3426
+ name: c.name,
3427
+ confidence: c.confidence,
3428
+ pattern: c.pattern,
3429
+ signal: c.signal,
3430
+ nodeType: c.nodeType
3431
+ })
3432
+ );
3433
+ await fs6.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3434
+ }
3435
+ /**
3436
+ * Write medium-confidence candidates to staged JSONL for human review.
3437
+ */
3438
+ async writeStagedJsonl(candidates) {
3439
+ if (!this.outputDir || candidates.length === 0) return;
3440
+ const stagedDir = path7.join(this.outputDir, "staged");
3441
+ await fs6.mkdir(stagedDir, { recursive: true });
3442
+ const filePath = path7.join(stagedDir, "linker-staged.jsonl");
3443
+ const lines = candidates.map(
3444
+ (c) => JSON.stringify({
3445
+ id: c.id,
3446
+ sourceNodeId: c.sourceNodeId,
3447
+ sourceNodeType: c.sourceNodeType,
3448
+ name: c.name,
3449
+ confidence: c.confidence,
3450
+ pattern: c.pattern,
3451
+ signal: c.signal,
3452
+ nodeType: c.nodeType
3453
+ })
3454
+ );
3455
+ await fs6.writeFile(filePath, lines.join("\n") + "\n");
3456
+ }
3457
+ /**
3458
+ * Apply heuristic patterns to content and return candidate extractions.
3459
+ */
3460
+ scanPatterns(content, nodeId, nodeType) {
3461
+ const candidates = [];
3462
+ for (const heuristic of HEURISTIC_PATTERNS) {
3463
+ if (heuristic.pattern.test(content)) {
3464
+ const candidateId = `extracted:linker:${hash(nodeId + ":" + heuristic.name)}`;
3465
+ candidates.push({
3466
+ id: candidateId,
3467
+ sourceNodeId: nodeId,
3468
+ sourceNodeType: nodeType,
3469
+ name: `${heuristic.signal} from ${nodeId}`,
3470
+ content,
3471
+ confidence: heuristic.confidence,
3472
+ pattern: heuristic.name,
3473
+ signal: heuristic.signal,
3474
+ nodeType: heuristic.nodeType
3475
+ });
3476
+ }
3477
+ }
3478
+ return candidates;
3479
+ }
3480
+ };
3481
+ StructuralDriftDetector = class {
3482
+ detect(current, fresh) {
3483
+ const findings = [];
3484
+ const currentById = new Map(current.entries.map((e) => [e.id, e]));
3485
+ const freshById = new Map(fresh.entries.map((e) => [e.id, e]));
3486
+ for (const [id, entry] of freshById) {
3487
+ if (!currentById.has(id)) {
3488
+ findings.push({
3489
+ entryId: id,
3490
+ classification: "new",
3491
+ fresh: entry,
3492
+ severity: "low"
3493
+ });
3494
+ }
3495
+ }
3496
+ for (const [id, entry] of currentById) {
3497
+ if (!freshById.has(id)) {
3498
+ findings.push({
3499
+ entryId: id,
3500
+ classification: "stale",
3501
+ current: entry,
3502
+ severity: "high"
3503
+ });
3504
+ }
3505
+ }
3506
+ for (const [id, freshEntry] of freshById) {
3507
+ const currentEntry = currentById.get(id);
3508
+ if (currentEntry && currentEntry.contentHash !== freshEntry.contentHash) {
3509
+ findings.push({
3510
+ entryId: id,
3511
+ classification: "drifted",
3512
+ current: currentEntry,
3513
+ fresh: freshEntry,
3514
+ severity: "medium"
3515
+ });
3516
+ }
3517
+ }
3518
+ const byName = /* @__PURE__ */ new Map();
3519
+ for (const entry of fresh.entries) {
3520
+ const group = byName.get(entry.name) ?? [];
3521
+ group.push(entry);
3522
+ byName.set(entry.name, group);
3523
+ }
3524
+ for (const [, group] of byName) {
3525
+ if (group.length > 1) {
3526
+ const sources = new Set(group.map((e) => e.source));
3527
+ const hashes = new Set(group.map((e) => e.contentHash));
3528
+ if (sources.size > 1 && hashes.size > 1) {
3529
+ const alreadyContradicting = new Set(
3530
+ findings.filter((f) => f.classification === "contradicting").map((f) => f.entryId)
3531
+ );
3532
+ for (const entry of group) {
3533
+ if (!alreadyContradicting.has(entry.id)) {
3534
+ findings.push({
3535
+ entryId: entry.id,
3536
+ classification: "contradicting",
3537
+ fresh: entry,
3538
+ severity: "critical"
3539
+ });
3540
+ break;
3541
+ }
3542
+ }
3543
+ }
3544
+ }
3545
+ }
3546
+ const totalEntries = (/* @__PURE__ */ new Set([...currentById.keys(), ...freshById.keys()])).size;
3547
+ const driftScore = totalEntries > 0 ? findings.length / totalEntries : 0;
3548
+ const summary = {
3549
+ new: findings.filter((f) => f.classification === "new").length,
3550
+ drifted: findings.filter((f) => f.classification === "drifted").length,
3551
+ stale: findings.filter((f) => f.classification === "stale").length,
3552
+ contradicting: findings.filter((f) => f.classification === "contradicting").length
3553
+ };
3554
+ return { findings, driftScore, summary };
3555
+ }
3556
+ };
3557
+ KnowledgeStagingAggregator = class {
3558
+ constructor(projectDir) {
3559
+ this.projectDir = projectDir;
3560
+ }
3561
+ projectDir;
3562
+ async aggregate(extractorResults, linkerResults, diagramResults) {
3563
+ const all = [...extractorResults, ...linkerResults, ...diagramResults];
3564
+ const byHash = /* @__PURE__ */ new Map();
3565
+ for (const entry of all) {
3566
+ const existing = byHash.get(entry.contentHash);
3567
+ if (!existing || entry.confidence > existing.confidence) {
3568
+ byHash.set(entry.contentHash, entry);
3569
+ }
3570
+ }
3571
+ const deduplicated = Array.from(byHash.values());
3572
+ if (deduplicated.length === 0) {
3573
+ return { staged: 0 };
3574
+ }
3575
+ const stagedDir = path8.join(this.projectDir, ".harness", "knowledge", "staged");
3576
+ await fs7.mkdir(stagedDir, { recursive: true });
3577
+ const jsonl = deduplicated.map((entry) => JSON.stringify(entry)).join("\n") + "\n";
3578
+ await fs7.writeFile(path8.join(stagedDir, "pipeline-staged.jsonl"), jsonl, "utf-8");
3579
+ return { staged: deduplicated.length };
3580
+ }
3581
+ async generateGapReport(knowledgeDir) {
3582
+ const domains = [];
3583
+ let totalEntries = 0;
3584
+ try {
3585
+ const entries = await fs7.readdir(knowledgeDir, { withFileTypes: true });
3586
+ const domainDirs = entries.filter((e) => e.isDirectory());
3587
+ for (const dir of domainDirs) {
3588
+ const domainPath = path8.join(knowledgeDir, dir.name);
3589
+ const files = await fs7.readdir(domainPath);
3590
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
3591
+ const entryCount = mdFiles.length;
3592
+ totalEntries += entryCount;
3593
+ domains.push({ domain: dir.name, entryCount });
3594
+ }
3595
+ } catch {
3596
+ }
3597
+ return {
3598
+ domains,
3599
+ totalEntries,
3600
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
3601
+ };
3602
+ }
3603
+ async writeGapReport(report) {
3604
+ const gapsDir = path8.join(this.projectDir, ".harness", "knowledge");
3605
+ await fs7.mkdir(gapsDir, { recursive: true });
3606
+ const lines = [
3607
+ "# Knowledge Gaps Report",
3608
+ "",
3609
+ `Generated: ${report.generatedAt}`,
3610
+ "",
3611
+ "## Coverage by Domain",
3612
+ "",
3613
+ "| Domain | Entries |",
3614
+ "| ------ | ------- |"
3615
+ ];
3616
+ for (const domain of report.domains) {
3617
+ lines.push(`| ${domain.domain} | ${domain.entryCount} |`);
3618
+ }
3619
+ lines.push("", `## Total Entries: ${report.totalEntries}`, "");
3620
+ await fs7.writeFile(path8.join(gapsDir, "gaps.md"), lines.join("\n"), "utf-8");
3621
+ }
3622
+ };
3623
+ SKIP_DIRS = /* @__PURE__ */ new Set([
3624
+ "node_modules",
3625
+ "dist",
3626
+ "target",
3627
+ "build",
3628
+ ".git",
3629
+ "__pycache__",
3630
+ ".venv",
3631
+ ".turbo",
3632
+ ".harness",
3633
+ ".next",
3634
+ ".vscode",
3635
+ ".idea",
3636
+ "vendor",
3637
+ "out",
3638
+ ".gradle",
3639
+ ".gradle-home",
3640
+ "bin",
3641
+ "obj",
3642
+ "venv",
3643
+ "_build",
3644
+ "deps",
3645
+ "coverage",
3646
+ ".nyc_output"
3647
+ ]);
3648
+ EXT_TO_LANGUAGE = {
3649
+ ".ts": "typescript",
3650
+ ".tsx": "typescript",
3651
+ ".js": "javascript",
3652
+ ".jsx": "javascript",
3653
+ ".py": "python",
3654
+ ".go": "go",
3655
+ ".rs": "rust",
3656
+ ".java": "java"
3657
+ };
3658
+ SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([".d.ts"]);
3659
+ EXTRACTOR_EDGE_TYPE = {
3660
+ "test-descriptions": "governs",
3661
+ "enum-constants": "documents",
3662
+ "validation-rules": "governs",
3663
+ "api-paths": "documents"
3664
+ };
3665
+ ExtractionRunner = class {
3666
+ constructor(extractors) {
3667
+ this.extractors = extractors;
3668
+ }
3669
+ extractors;
3670
+ /**
3671
+ * Run all extractors against a project directory.
3672
+ * @param projectDir - Project root directory
3673
+ * @param store - GraphStore for node/edge persistence
3674
+ * @param outputDir - Directory for JSONL output (e.g. .harness/knowledge/extracted/)
3675
+ */
3676
+ async run(projectDir, store, outputDir) {
3677
+ const start = Date.now();
3678
+ const errors = [];
3679
+ let nodesAdded = 0;
3680
+ let nodesUpdated = 0;
3681
+ let edgesAdded = 0;
3682
+ await fs8.mkdir(outputDir, { recursive: true });
3683
+ const files = await this.findSourceFiles(projectDir);
3684
+ const recordsByExtractor = /* @__PURE__ */ new Map();
3685
+ for (const ext of this.extractors) {
3686
+ recordsByExtractor.set(ext.name, []);
3687
+ }
3688
+ for (const filePath of files) {
3689
+ const language = detectLanguage(filePath);
3690
+ if (!language) continue;
3691
+ const ext = path9.extname(filePath);
3692
+ let content;
3693
+ try {
3694
+ content = await fs8.readFile(filePath, "utf-8");
3695
+ } catch (err) {
3696
+ errors.push(
3697
+ `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
3698
+ );
3699
+ continue;
3700
+ }
3701
+ const relativePath = path9.relative(projectDir, filePath).replaceAll("\\", "/");
3702
+ for (const extractor2 of this.extractors) {
3703
+ if (!extractor2.supportedExtensions.includes(ext)) continue;
3704
+ try {
3705
+ const records = extractor2.extract(content, relativePath, language);
3706
+ recordsByExtractor.get(extractor2.name).push(...records);
3707
+ } catch (err) {
3708
+ errors.push(
3709
+ `Extractor ${extractor2.name} failed on ${relativePath}: ${err instanceof Error ? err.message : String(err)}`
3710
+ );
3711
+ }
3712
+ }
3713
+ }
3714
+ const allCurrentIds = /* @__PURE__ */ new Set();
3715
+ for (const [extractorName, records] of recordsByExtractor) {
3716
+ await this.writeJsonl(records, outputDir, extractorName);
3717
+ for (const record of records) {
3718
+ allCurrentIds.add(record.id);
3719
+ const result = this.persistRecord(record, store);
3720
+ nodesAdded += result.nodesAdded;
3721
+ nodesUpdated += result.nodesUpdated;
3722
+ edgesAdded += result.edgesAdded;
3723
+ }
3724
+ }
3725
+ const staleCount = this.markStale(store, allCurrentIds);
3726
+ nodesUpdated += staleCount;
3727
+ return {
3728
+ nodesAdded,
3729
+ nodesUpdated,
3730
+ edgesAdded,
3731
+ edgesUpdated: 0,
3732
+ errors,
3733
+ durationMs: Date.now() - start
3734
+ };
3735
+ }
3736
+ /** Write extraction records to JSONL file. */
3737
+ async writeJsonl(records, outputDir, extractorName) {
3738
+ const filePath = path9.join(outputDir, `${extractorName}.jsonl`);
3739
+ const lines = records.map((r) => JSON.stringify(r));
3740
+ await fs8.writeFile(filePath, lines.join("\n") + (lines.length > 0 ? "\n" : ""));
3741
+ }
3742
+ /** Create or update a graph node from an extraction record. */
3743
+ persistRecord(record, store) {
3744
+ const existing = store.getNode(record.id);
3745
+ const node = {
3746
+ id: record.id,
3747
+ type: record.nodeType,
3748
+ name: record.name,
3749
+ path: record.filePath,
3750
+ location: {
3751
+ fileId: `file:${hash(record.filePath)}`,
3752
+ startLine: record.line,
3753
+ endLine: record.line
3754
+ },
3755
+ content: record.content,
3756
+ metadata: {
3757
+ ...record.metadata,
3758
+ source: "code-extractor",
3759
+ extractor: record.extractor,
3760
+ confidence: record.confidence,
3761
+ language: record.language,
3762
+ stale: false
3763
+ }
3764
+ };
3765
+ store.addNode(node);
3766
+ const fileNodeId = `file:${hash(record.filePath)}`;
3767
+ const edgeType = EXTRACTOR_EDGE_TYPE[record.extractor] ?? "documents";
3768
+ const edge = {
3769
+ from: record.id,
3770
+ to: fileNodeId,
3771
+ type: edgeType,
3772
+ confidence: record.confidence,
3773
+ metadata: { source: "code-extractor" }
3774
+ };
3775
+ store.addEdge(edge);
3776
+ return {
3777
+ nodesAdded: existing ? 0 : 1,
3778
+ nodesUpdated: existing ? 1 : 0,
3779
+ edgesAdded: existing ? 0 : 1
3780
+ };
3781
+ }
3782
+ /**
3783
+ * Mark nodes from previous extractions that are no longer present as stale.
3784
+ * Returns the number of nodes marked stale.
3785
+ */
3786
+ markStale(store, currentIds) {
3787
+ let count = 0;
3788
+ const businessTypes = ["business_rule", "business_process", "business_term"];
3789
+ for (const type of businessTypes) {
3790
+ const nodes = store.findNodes({ type });
3791
+ for (const node of nodes) {
3792
+ if (node.metadata.source === "code-extractor" && !node.metadata.stale && !currentIds.has(node.id)) {
3793
+ store.addNode({
3794
+ ...node,
3795
+ metadata: {
3796
+ ...node.metadata,
3797
+ stale: true,
3798
+ staleAt: (/* @__PURE__ */ new Date()).toISOString()
3799
+ }
3800
+ });
3801
+ count++;
3802
+ }
3803
+ }
3804
+ }
3805
+ return count;
3806
+ }
3807
+ /** Recursively find source files, skipping common non-source directories. */
3808
+ async findSourceFiles(dir) {
3809
+ const results = [];
3810
+ let entries;
3811
+ try {
3812
+ entries = await fs8.readdir(dir, { withFileTypes: true });
3813
+ } catch {
3814
+ return results;
3815
+ }
3816
+ for (const entry of entries) {
3817
+ const fullPath = path9.join(dir, entry.name);
3818
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
3819
+ results.push(...await this.findSourceFiles(fullPath));
3820
+ } else if (entry.isFile() && detectLanguage(fullPath) !== void 0) {
3821
+ results.push(fullPath);
3822
+ }
3823
+ }
3824
+ return results;
3825
+ }
3826
+ };
3827
+ PATTERNS = {
3828
+ typescript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
3829
+ javascript: [/(?:describe|it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/g],
3830
+ python: [/def\s+(test_\w+)\s*\(/g, /"""((?:(?!""").)*?)"""/gs, /class\s+(Test\w+)/g],
3831
+ go: [/func\s+(Test\w+)\s*\(/g, /t\.Run\(\s*"([^"]+)"/g],
3832
+ rust: [/#\[test\]\s*\n\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g, /\/\/\/\s*(.+)/g],
3833
+ java: [
3834
+ /@DisplayName\s*\(\s*"([^"]+)"\s*\)/g,
3835
+ /@Test\s*\n\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|[\w<>]+)\s+(\w+)\s*\(/g
3836
+ ]
3837
+ };
3838
+ TestDescriptionExtractor = class {
3839
+ name = "test-descriptions";
3840
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
3841
+ extract(content, filePath, language) {
3842
+ const records = [];
3843
+ const patterns = PATTERNS[language];
3844
+ if (!patterns) return records;
3845
+ const lines = content.split("\n");
3846
+ if (language === "typescript" || language === "javascript") {
3847
+ return this.extractJsTs(content, filePath, language, lines);
3848
+ }
3849
+ if (language === "python") {
3850
+ return this.extractPython(content, filePath, language, lines);
3851
+ }
3852
+ if (language === "go") {
3853
+ return this.extractGo(content, filePath, language, lines);
3854
+ }
3855
+ if (language === "rust") {
3856
+ return this.extractRust(content, filePath, language, lines);
3857
+ }
3858
+ if (language === "java") {
3859
+ return this.extractJava(content, filePath, language, lines);
3860
+ }
3861
+ return records;
3862
+ }
3863
+ extractJsTs(_content, filePath, language, lines) {
3864
+ const records = [];
3865
+ const describeStack = [];
3866
+ for (let i = 0; i < lines.length; i++) {
3867
+ const line = lines[i];
3868
+ const describeMatch = line.match(/describe\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
3869
+ if (describeMatch) {
3870
+ describeStack.push(describeMatch[2]);
3871
+ }
3872
+ const itMatch = line.match(/(?:it|test)\s*\(\s*(['"`])((?:(?!\1).)*)\1/);
3873
+ if (itMatch) {
3874
+ const testName = itMatch[2];
3875
+ const fullPath = [...describeStack, testName].join(" > ");
3876
+ const patternKey = fullPath;
3877
+ records.push({
3878
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
3879
+ extractor: "test-descriptions",
3880
+ language,
3881
+ filePath,
3882
+ line: i + 1,
3883
+ nodeType: "business_rule",
3884
+ name: testName,
3885
+ content: fullPath,
3886
+ confidence: 0.7,
3887
+ metadata: {
3888
+ suite: describeStack.length > 0 ? describeStack[describeStack.length - 1] : void 0,
3889
+ framework: "vitest"
3890
+ }
3891
+ });
3892
+ }
3893
+ if (line.match(/^\s*\}\s*\)\s*;?\s*$/) && describeStack.length > 0) {
3894
+ describeStack.pop();
3895
+ }
3896
+ }
3897
+ return records;
3898
+ }
3899
+ extractPython(_content, filePath, language, lines) {
3900
+ const records = [];
3901
+ let currentClass;
3902
+ for (let i = 0; i < lines.length; i++) {
3903
+ const line = lines[i];
3904
+ const classMatch = line.match(/^class\s+(Test\w+)/);
3905
+ if (classMatch) {
3906
+ currentClass = classMatch[1];
3907
+ }
3908
+ const funcMatch = line.match(/^\s*def\s+(test_\w+)\s*\(/);
3909
+ if (funcMatch) {
3910
+ const testName = funcMatch[1];
3911
+ const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
3912
+ let docstring;
3913
+ if (i + 1 < lines.length) {
3914
+ const nextLine = lines[i + 1].trim();
3915
+ const docMatch = nextLine.match(/^"""(.+?)"""/);
3916
+ if (docMatch) {
3917
+ docstring = docMatch[1];
3918
+ }
3919
+ }
3920
+ const patternKey = currentClass ? `${currentClass}.${testName}` : testName;
3921
+ records.push({
3922
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
3923
+ extractor: "test-descriptions",
3924
+ language,
3925
+ filePath,
3926
+ line: i + 1,
3927
+ nodeType: "business_rule",
3928
+ name: docstring ?? humanName,
3929
+ content: patternKey,
3930
+ confidence: docstring ? 0.7 : 0.5,
3931
+ metadata: {
3932
+ suite: currentClass,
3933
+ framework: "pytest",
3934
+ functionName: testName
3935
+ }
3936
+ });
3937
+ }
3938
+ if (currentClass && /^\S/.test(line) && !line.startsWith("class ") && !line.startsWith("#") && line.trim() !== "") {
3939
+ currentClass = void 0;
3940
+ }
3941
+ }
3942
+ return records;
3943
+ }
3944
+ extractGo(_content, filePath, language, lines) {
3945
+ const records = [];
3946
+ let currentTest;
3947
+ for (let i = 0; i < lines.length; i++) {
3948
+ const line = lines[i];
3949
+ const funcMatch = line.match(/^func\s+(Test\w+)\s*\(/);
3950
+ if (funcMatch) {
3951
+ currentTest = funcMatch[1];
3952
+ const humanName = currentTest.replace(/^Test/, "").replace(/([A-Z])/g, " $1").trim();
3953
+ records.push({
3954
+ id: `extracted:test-descriptions:${hash(filePath + ":" + currentTest)}`,
3955
+ extractor: "test-descriptions",
3956
+ language,
3957
+ filePath,
3958
+ line: i + 1,
3959
+ nodeType: "business_rule",
3960
+ name: humanName,
3961
+ content: currentTest,
3962
+ confidence: 0.5,
3963
+ metadata: { framework: "testing" }
3964
+ });
3965
+ }
3966
+ const runMatch = line.match(/t\.Run\(\s*"([^"]+)"/);
3967
+ if (runMatch && currentTest) {
3968
+ const subtestName = runMatch[1];
3969
+ const patternKey = `${currentTest} > ${subtestName}`;
3970
+ records.push({
3971
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
3972
+ extractor: "test-descriptions",
3973
+ language,
3974
+ filePath,
3975
+ line: i + 1,
3976
+ nodeType: "business_rule",
3977
+ name: subtestName,
3978
+ content: patternKey,
3979
+ confidence: 0.7,
3980
+ metadata: {
3981
+ suite: currentTest,
3982
+ framework: "testing"
3983
+ }
3984
+ });
3985
+ }
3986
+ }
3987
+ return records;
3988
+ }
3989
+ extractRust(_content, filePath, language, lines) {
3990
+ const records = [];
3991
+ for (let i = 0; i < lines.length; i++) {
3992
+ const line = lines[i];
3993
+ if (line.trim() === "#[test]") {
3994
+ for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
3995
+ const fnLine = lines[j];
3996
+ const fnMatch = fnLine.match(/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
3997
+ if (fnMatch) {
3998
+ const testName = fnMatch[1];
3999
+ const humanName = testName.replace(/^test_/, "").replace(/_/g, " ");
4000
+ let docComment;
4001
+ if (i > 0) {
4002
+ const prevLine = lines[i - 1].trim();
4003
+ const docMatch = prevLine.match(/^\/\/\/\s*(.+)/);
4004
+ if (docMatch) {
4005
+ docComment = docMatch[1];
4006
+ }
4007
+ }
4008
+ records.push({
4009
+ id: `extracted:test-descriptions:${hash(filePath + ":" + testName)}`,
4010
+ extractor: "test-descriptions",
4011
+ language,
4012
+ filePath,
4013
+ line: j + 1,
4014
+ nodeType: "business_rule",
4015
+ name: docComment ?? humanName,
4016
+ content: testName,
4017
+ confidence: docComment ? 0.7 : 0.5,
4018
+ metadata: { framework: "rust-test" }
4019
+ });
4020
+ break;
4021
+ }
4022
+ }
4023
+ }
4024
+ }
4025
+ return records;
4026
+ }
4027
+ extractJava(_content, filePath, language, lines) {
4028
+ const records = [];
4029
+ for (let i = 0; i < lines.length; i++) {
4030
+ const line = lines[i];
4031
+ const testMatch = line.match(/@Test\s*$/);
4032
+ if (testMatch) {
4033
+ let displayName;
4034
+ for (let k = Math.max(0, i - 3); k < i; k++) {
4035
+ const prevLine = lines[k];
4036
+ const dm = prevLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
4037
+ if (dm) displayName = dm[1];
4038
+ }
4039
+ for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
4040
+ const scanLine = lines[j];
4041
+ const adjacentDisplay = scanLine.match(/@DisplayName\s*\(\s*"([^"]+)"\s*\)/);
4042
+ if (adjacentDisplay) {
4043
+ displayName = adjacentDisplay[1];
4044
+ continue;
4045
+ }
4046
+ const methodMatch = scanLine.match(
4047
+ /^\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:void|[\w<>[\]]+)\s+(\w+)\s*\(/
4048
+ );
4049
+ if (methodMatch) {
4050
+ const methodName = methodMatch[1];
4051
+ const name = displayName ?? methodName;
4052
+ const patternKey = displayName ?? methodName;
4053
+ records.push({
4054
+ id: `extracted:test-descriptions:${hash(filePath + ":" + patternKey)}`,
4055
+ extractor: "test-descriptions",
4056
+ language,
4057
+ filePath,
4058
+ line: j + 1,
4059
+ nodeType: "business_rule",
4060
+ name,
4061
+ content: patternKey,
4062
+ confidence: displayName ? 0.7 : 0.5,
4063
+ metadata: { framework: "junit5" }
4064
+ });
4065
+ break;
4066
+ }
4067
+ }
4068
+ }
4069
+ }
4070
+ return records;
4071
+ }
4072
+ };
4073
+ EnumConstantExtractor = class {
4074
+ name = "enum-constants";
4075
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4076
+ extract(content, filePath, language) {
4077
+ switch (language) {
4078
+ case "typescript":
4079
+ return this.extractTypeScript(content, filePath, language);
4080
+ case "javascript":
4081
+ return this.extractJavaScript(content, filePath, language);
4082
+ case "python":
4083
+ return this.extractPython(content, filePath, language);
4084
+ case "go":
4085
+ return this.extractGo(content, filePath, language);
4086
+ case "rust":
4087
+ return this.extractRust(content, filePath, language);
4088
+ case "java":
4089
+ return this.extractJava(content, filePath, language);
4090
+ }
4091
+ }
4092
+ extractTypeScript(content, filePath, language) {
4093
+ const records = [];
4094
+ const lines = content.split("\n");
4095
+ for (let i = 0; i < lines.length; i++) {
4096
+ const line = lines[i];
4097
+ const enumMatch = line.match(/(?:export\s+)?enum\s+(\w+)/);
4098
+ if (enumMatch) {
4099
+ const enumName = enumMatch[1];
4100
+ const members = this.collectEnumMembers(lines, i + 1);
4101
+ records.push({
4102
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4103
+ extractor: "enum-constants",
4104
+ language,
4105
+ filePath,
4106
+ line: i + 1,
4107
+ nodeType: "business_term",
4108
+ name: enumName,
4109
+ content: `enum ${enumName} { ${members.join(", ")} }`,
4110
+ confidence: 0.8,
4111
+ metadata: { kind: "enum", members }
4112
+ });
4113
+ }
4114
+ const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*\{/);
4115
+ if (constMatch && content.includes("as const")) {
4116
+ const blockEnd = this.findClosingBrace(lines, i);
4117
+ const block = lines.slice(i, blockEnd + 1).join("\n");
4118
+ if (block.includes("as const")) {
4119
+ const constName = constMatch[1];
4120
+ const members = this.collectObjectKeys(lines, i + 1);
4121
+ records.push({
4122
+ id: `extracted:enum-constants:${hash(filePath + ":" + constName)}`,
4123
+ extractor: "enum-constants",
4124
+ language,
4125
+ filePath,
4126
+ line: i + 1,
4127
+ nodeType: "business_term",
4128
+ name: constName,
4129
+ content: `const ${constName} = { ${members.join(", ")} } as const`,
4130
+ confidence: 0.8,
4131
+ metadata: { kind: "as-const", members }
4132
+ });
4133
+ }
4134
+ }
4135
+ const unionMatch = line.match(
4136
+ /(?:export\s+)?type\s+(\w+)\s*=\s*(['"`][\w]+['"`](?:\s*\|\s*['"`][\w]+['"`])+)/
4137
+ );
4138
+ if (unionMatch) {
4139
+ const typeName = unionMatch[1];
4140
+ const values = unionMatch[2].split("|").map((v) => v.trim().replace(/['"`]/g, ""));
4141
+ records.push({
4142
+ id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
4143
+ extractor: "enum-constants",
4144
+ language,
4145
+ filePath,
4146
+ line: i + 1,
4147
+ nodeType: "business_term",
4148
+ name: typeName,
4149
+ content: `type ${typeName} = ${values.map((v) => `'${v}'`).join(" | ")}`,
4150
+ confidence: 0.7,
4151
+ metadata: { kind: "union-type", members: values }
4152
+ });
4153
+ }
4154
+ }
4155
+ return records;
4156
+ }
4157
+ extractJavaScript(content, filePath, language) {
4158
+ const records = [];
4159
+ const lines = content.split("\n");
4160
+ for (let i = 0; i < lines.length; i++) {
4161
+ const line = lines[i];
4162
+ const freezeMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*Object\.freeze\s*\(\s*\{/);
4163
+ if (freezeMatch) {
4164
+ const name = freezeMatch[1];
4165
+ const members = this.collectObjectKeys(lines, i + 1);
4166
+ records.push({
4167
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
4168
+ extractor: "enum-constants",
4169
+ language,
4170
+ filePath,
4171
+ line: i + 1,
4172
+ nodeType: "business_term",
4173
+ name,
4174
+ content: `const ${name} = Object.freeze({ ${members.join(", ")} })`,
4175
+ confidence: 0.8,
4176
+ metadata: { kind: "frozen-object", members }
4177
+ });
4178
+ }
4179
+ const constMatch = line.match(/(?:export\s+)?const\s+([A-Z][A-Z_\d]*)\s*=\s*\{/);
4180
+ if (constMatch && !freezeMatch) {
4181
+ const name = constMatch[1];
4182
+ const members = this.collectObjectKeys(lines, i + 1);
4183
+ if (members.length > 0 && members.every((m) => /^[A-Z][A-Z_\d]*$/.test(m))) {
4184
+ records.push({
4185
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
4186
+ extractor: "enum-constants",
4187
+ language,
4188
+ filePath,
4189
+ line: i + 1,
4190
+ nodeType: "business_term",
4191
+ name,
4192
+ content: `const ${name} = { ${members.join(", ")} }`,
4193
+ confidence: 0.6,
4194
+ metadata: { kind: "const-object", members }
4195
+ });
4196
+ }
4197
+ }
4198
+ }
4199
+ return records;
4200
+ }
4201
+ extractPython(content, filePath, language) {
4202
+ const records = [];
4203
+ const lines = content.split("\n");
4204
+ for (let i = 0; i < lines.length; i++) {
4205
+ const line = lines[i];
4206
+ const enumMatch = line.match(
4207
+ /^class\s+(\w+)\s*\(\s*(?:str\s*,\s*)?(?:Enum|StrEnum|IntEnum|Flag|IntFlag)\s*\)/
4208
+ );
4209
+ if (enumMatch) {
4210
+ const enumName = enumMatch[1];
4211
+ const members = this.collectPythonEnumMembers(lines, i + 1);
4212
+ records.push({
4213
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4214
+ extractor: "enum-constants",
4215
+ language,
4216
+ filePath,
4217
+ line: i + 1,
4218
+ nodeType: "business_term",
4219
+ name: enumName,
4220
+ content: `class ${enumName}(Enum) { ${members.join(", ")} }`,
4221
+ confidence: 0.8,
4222
+ metadata: { kind: "enum", members }
4223
+ });
4224
+ }
4225
+ const literalMatch = line.match(/(\w+)\s*=\s*Literal\s*\[([^\]]+)\]/);
4226
+ if (literalMatch) {
4227
+ const typeName = literalMatch[1];
4228
+ const values = literalMatch[2].split(",").map((v) => v.trim().replace(/["']/g, ""));
4229
+ records.push({
4230
+ id: `extracted:enum-constants:${hash(filePath + ":" + typeName)}`,
4231
+ extractor: "enum-constants",
4232
+ language,
4233
+ filePath,
4234
+ line: i + 1,
4235
+ nodeType: "business_term",
4236
+ name: typeName,
4237
+ content: `${typeName} = Literal[${values.map((v) => `"${v}"`).join(", ")}]`,
4238
+ confidence: 0.7,
4239
+ metadata: { kind: "literal-type", members: values }
4240
+ });
4241
+ }
4242
+ }
4243
+ return records;
4244
+ }
4245
+ extractGo(content, filePath, language) {
4246
+ const records = [];
4247
+ const lines = content.split("\n");
4248
+ for (let i = 0; i < lines.length; i++) {
4249
+ const line = lines[i];
4250
+ const typeMatch = line.match(/^type\s+(\w+)\s+(?:int|string|uint|int32|int64|uint32|uint64)/);
4251
+ if (typeMatch) {
4252
+ }
4253
+ const constBlockMatch = line.match(/^const\s*\(/);
4254
+ if (constBlockMatch) {
4255
+ const consts = [];
4256
+ let typeName;
4257
+ for (let j = i + 1; j < lines.length; j++) {
4258
+ const constLine = lines[j].trim();
4259
+ if (constLine === ")") break;
4260
+ if (constLine === "" || constLine.startsWith("//")) continue;
4261
+ const iotaMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*iota/);
4262
+ if (iotaMatch) {
4263
+ typeName = iotaMatch[2];
4264
+ consts.push(iotaMatch[1]);
4265
+ continue;
4266
+ }
4267
+ const typedMatch = constLine.match(/^(\w+)\s+(\w+)\s*=\s*"[^"]*"/);
4268
+ if (typedMatch) {
4269
+ typeName = typeName ?? typedMatch[2];
4270
+ consts.push(typedMatch[1]);
4271
+ continue;
4272
+ }
4273
+ const bareMatch = constLine.match(/^(\w+)\s*$/);
4274
+ if (bareMatch) {
4275
+ consts.push(bareMatch[1]);
4276
+ }
4277
+ }
4278
+ if (consts.length > 0) {
4279
+ const name = typeName ?? consts[0];
4280
+ records.push({
4281
+ id: `extracted:enum-constants:${hash(filePath + ":" + name)}`,
4282
+ extractor: "enum-constants",
4283
+ language,
4284
+ filePath,
4285
+ line: i + 1,
4286
+ nodeType: "business_term",
4287
+ name,
4288
+ content: `const ( ${consts.join(", ")} )`,
4289
+ confidence: typeName ? 0.8 : 0.6,
4290
+ metadata: { kind: typeName ? "typed-const" : "const-block", members: consts }
4291
+ });
4292
+ }
4293
+ }
4294
+ }
4295
+ return records;
4296
+ }
4297
+ extractRust(content, filePath, language) {
4298
+ const records = [];
4299
+ const lines = content.split("\n");
4300
+ for (let i = 0; i < lines.length; i++) {
4301
+ const line = lines[i];
4302
+ const enumMatch = line.match(/^(?:pub\s+)?enum\s+(\w+)/);
4303
+ if (enumMatch) {
4304
+ const enumName = enumMatch[1];
4305
+ const members = this.collectRustEnumVariants(lines, i + 1);
4306
+ records.push({
4307
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4308
+ extractor: "enum-constants",
4309
+ language,
4310
+ filePath,
4311
+ line: i + 1,
4312
+ nodeType: "business_term",
4313
+ name: enumName,
4314
+ content: `enum ${enumName} { ${members.join(", ")} }`,
4315
+ confidence: 0.8,
4316
+ metadata: { kind: "enum", members }
4317
+ });
4318
+ }
4319
+ }
4320
+ return records;
4321
+ }
4322
+ extractJava(content, filePath, language) {
4323
+ const records = [];
4324
+ const lines = content.split("\n");
4325
+ for (let i = 0; i < lines.length; i++) {
4326
+ const line = lines[i];
4327
+ const enumMatch = line.match(/(?:public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
4328
+ if (enumMatch) {
4329
+ const enumName = enumMatch[1];
4330
+ const members = this.collectJavaEnumConstants(lines, i + 1);
4331
+ records.push({
4332
+ id: `extracted:enum-constants:${hash(filePath + ":" + enumName)}`,
4333
+ extractor: "enum-constants",
4334
+ language,
4335
+ filePath,
4336
+ line: i + 1,
4337
+ nodeType: "business_term",
4338
+ name: enumName,
4339
+ content: `enum ${enumName} { ${members.join(", ")} }`,
4340
+ confidence: 0.8,
4341
+ metadata: { kind: "enum", members }
4342
+ });
4343
+ }
4344
+ }
4345
+ return records;
4346
+ }
4347
+ // --- Helper methods ---
4348
+ collectEnumMembers(lines, startLine) {
4349
+ const members = [];
4350
+ for (let i = startLine; i < lines.length; i++) {
4351
+ const line = lines[i].trim();
4352
+ if (line === "}") break;
4353
+ if (line === "" || line.startsWith("//")) continue;
4354
+ const match = line.match(/^(\w+)/);
4355
+ if (match) members.push(match[1]);
4356
+ }
4357
+ return members;
4358
+ }
4359
+ collectObjectKeys(lines, startLine) {
4360
+ const keys = [];
4361
+ for (let i = startLine; i < lines.length; i++) {
4362
+ const line = lines[i].trim();
4363
+ if (line.startsWith("}")) break;
4364
+ if (line === "" || line.startsWith("//")) continue;
4365
+ const match = line.match(/^(\w+)\s*:/);
4366
+ if (match) keys.push(match[1]);
4367
+ }
4368
+ return keys;
4369
+ }
4370
+ findClosingBrace(lines, startLine) {
4371
+ let depth = 0;
4372
+ for (let i = startLine; i < lines.length; i++) {
4373
+ for (const ch of lines[i]) {
4374
+ if (ch === "{") depth++;
4375
+ if (ch === "}") {
4376
+ depth--;
4377
+ if (depth === 0) return i;
4378
+ }
4379
+ }
4380
+ }
4381
+ return lines.length - 1;
4382
+ }
4383
+ collectPythonEnumMembers(lines, startLine) {
4384
+ const members = [];
4385
+ for (let i = startLine; i < lines.length; i++) {
4386
+ const line = lines[i];
4387
+ if (/^\S/.test(line) && line.trim() !== "") break;
4388
+ const match = line.match(/^\s+(\w+)\s*=/);
4389
+ if (match) members.push(match[1]);
4390
+ }
4391
+ return members;
4392
+ }
4393
+ collectRustEnumVariants(lines, startLine) {
4394
+ const variants = [];
4395
+ for (let i = startLine; i < lines.length; i++) {
4396
+ const line = lines[i].trim();
4397
+ if (line === "}") break;
4398
+ if (line === "" || line.startsWith("//")) continue;
4399
+ const match = line.match(/^(\w+)/);
4400
+ if (match) variants.push(match[1]);
4401
+ }
4402
+ return variants;
4403
+ }
4404
+ collectJavaEnumConstants(lines, startLine) {
4405
+ const constants = [];
4406
+ for (let i = startLine; i < lines.length; i++) {
4407
+ const line = lines[i].trim();
4408
+ if (line === "}") break;
4409
+ if (line === "" || line.startsWith("//") || line.startsWith("/*")) continue;
4410
+ if (line.match(/^\s*(?:private|public|protected|static)/)) break;
4411
+ const match = line.match(/^(\w+)[\s(,;]/);
4412
+ if (match) constants.push(match[1]);
4413
+ const singleMatch = line.match(/^(\w+)$/);
4414
+ if (singleMatch) constants.push(singleMatch[1]);
4415
+ }
4416
+ return constants;
4417
+ }
4418
+ };
4419
+ ValidationRuleExtractor = class {
4420
+ name = "validation-rules";
4421
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4422
+ extract(content, filePath, language) {
4423
+ switch (language) {
4424
+ case "typescript":
4425
+ case "javascript":
4426
+ return this.extractZod(content, filePath, language);
4427
+ case "python":
4428
+ return this.extractPydantic(content, filePath, language);
4429
+ case "go":
4430
+ return this.extractGoValidate(content, filePath, language);
4431
+ case "rust":
4432
+ return this.extractRustValidate(content, filePath, language);
4433
+ case "java":
4434
+ return this.extractBeanValidation(content, filePath, language);
4435
+ }
4436
+ }
4437
+ extractZod(content, filePath, language) {
4438
+ const records = [];
4439
+ const lines = content.split("\n");
4440
+ for (let i = 0; i < lines.length; i++) {
4441
+ const line = lines[i];
4442
+ const schemaMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*z\.object\s*\(\s*\{/);
4443
+ if (schemaMatch) {
4444
+ const schemaName = schemaMatch[1];
4445
+ const constraints = this.collectZodConstraints(lines, i + 1);
4446
+ records.push({
4447
+ id: `extracted:validation-rules:${hash(filePath + ":" + schemaName)}`,
4448
+ extractor: "validation-rules",
4449
+ language,
4450
+ filePath,
4451
+ line: i + 1,
4452
+ nodeType: "business_rule",
4453
+ name: schemaName,
4454
+ content: `${schemaName}: ${constraints.join("; ")}`,
4455
+ confidence: 0.8,
4456
+ metadata: { kind: "zod-schema", constraints, framework: "zod" }
4457
+ });
4458
+ }
4459
+ }
4460
+ return records;
4461
+ }
4462
+ collectZodConstraints(lines, startLine) {
4463
+ const constraints = [];
4464
+ for (let i = startLine; i < lines.length; i++) {
4465
+ const line = lines[i].trim();
4466
+ if (line.startsWith("}") || line.startsWith(")")) break;
4467
+ if (line === "" || line.startsWith("//")) continue;
4468
+ const fieldMatch = line.match(/(\w+)\s*:\s*z\.(.+?)(?:,?\s*$)/);
4469
+ if (fieldMatch) {
4470
+ const fieldName = fieldMatch[1];
4471
+ const chain = fieldMatch[2];
4472
+ constraints.push(`${fieldName}: ${chain}`);
4473
+ }
4474
+ }
4475
+ return constraints;
4476
+ }
4477
+ extractPydantic(content, filePath, language) {
4478
+ const records = [];
4479
+ const lines = content.split("\n");
4480
+ for (let i = 0; i < lines.length; i++) {
4481
+ const line = lines[i];
4482
+ const modelMatch = line.match(/^class\s+(\w+)\s*\(\s*BaseModel\s*\)/);
4483
+ if (modelMatch) {
4484
+ const modelName = modelMatch[1];
4485
+ const constraints = this.collectPydanticConstraints(lines, i + 1);
4486
+ records.push({
4487
+ id: `extracted:validation-rules:${hash(filePath + ":" + modelName)}`,
4488
+ extractor: "validation-rules",
4489
+ language,
4490
+ filePath,
4491
+ line: i + 1,
4492
+ nodeType: "business_rule",
4493
+ name: modelName,
4494
+ content: `${modelName}: ${constraints.join("; ")}`,
4495
+ confidence: 0.8,
4496
+ metadata: { kind: "pydantic-model", constraints, framework: "pydantic" }
4497
+ });
4498
+ }
4499
+ }
4500
+ return records;
4501
+ }
4502
+ collectPydanticConstraints(lines, startLine) {
4503
+ const constraints = [];
4504
+ for (let i = startLine; i < lines.length; i++) {
4505
+ const line = lines[i];
4506
+ if (/^\S/.test(line) && line.trim() !== "") break;
4507
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
4508
+ const fieldMatch = line.match(/^\s+(\w+)\s*:\s*(.+?)(?:\s*$)/);
4509
+ if (fieldMatch) {
4510
+ constraints.push(`${fieldMatch[1]}: ${fieldMatch[2]}`);
4511
+ }
4512
+ }
4513
+ return constraints;
4514
+ }
4515
+ extractGoValidate(content, filePath, language) {
4516
+ const records = [];
4517
+ const lines = content.split("\n");
4518
+ for (let i = 0; i < lines.length; i++) {
4519
+ const line = lines[i];
4520
+ const structMatch = line.match(/^type\s+(\w+)\s+struct\s*\{/);
4521
+ if (structMatch) {
4522
+ const structName = structMatch[1];
4523
+ const constraints = this.collectGoValidateTags(lines, i + 1);
4524
+ if (constraints.length > 0) {
4525
+ records.push({
4526
+ id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
4527
+ extractor: "validation-rules",
4528
+ language,
4529
+ filePath,
4530
+ line: i + 1,
4531
+ nodeType: "business_rule",
4532
+ name: structName,
4533
+ content: `${structName}: ${constraints.join("; ")}`,
4534
+ confidence: 0.8,
4535
+ metadata: { kind: "struct-tags", constraints, framework: "go-playground/validator" }
4536
+ });
4537
+ }
4538
+ }
4539
+ }
4540
+ return records;
4541
+ }
4542
+ collectGoValidateTags(lines, startLine) {
4543
+ const constraints = [];
4544
+ for (let i = startLine; i < lines.length; i++) {
4545
+ const line = lines[i].trim();
4546
+ if (line === "}") break;
4547
+ if (line === "" || line.startsWith("//")) continue;
4548
+ const tagMatch = line.match(/(\w+)\s+\S+\s+`[^`]*validate:"([^"]+)"[^`]*`/);
4549
+ if (tagMatch) {
4550
+ constraints.push(`${tagMatch[1]}: ${tagMatch[2]}`);
4551
+ }
4552
+ }
4553
+ return constraints;
4554
+ }
4555
+ extractRustValidate(content, filePath, language) {
4556
+ const records = [];
4557
+ const lines = content.split("\n");
4558
+ for (let i = 0; i < lines.length; i++) {
4559
+ const line = lines[i];
4560
+ const deriveMatch = line.match(/#\[derive\([^)]*Validate[^)]*\)\]/);
4561
+ if (deriveMatch) {
4562
+ for (let j = i + 1; j < lines.length && j <= i + 3; j++) {
4563
+ const structLine = lines[j];
4564
+ const structMatch = structLine.match(/^(?:pub\s+)?struct\s+(\w+)/);
4565
+ if (structMatch) {
4566
+ const structName = structMatch[1];
4567
+ const constraints = this.collectRustValidateAttrs(lines, j + 1);
4568
+ if (constraints.length > 0) {
4569
+ records.push({
4570
+ id: `extracted:validation-rules:${hash(filePath + ":" + structName)}`,
4571
+ extractor: "validation-rules",
4572
+ language,
4573
+ filePath,
4574
+ line: j + 1,
4575
+ nodeType: "business_rule",
4576
+ name: structName,
4577
+ content: `${structName}: ${constraints.join("; ")}`,
4578
+ confidence: 0.8,
4579
+ metadata: { kind: "validate-derive", constraints, framework: "validator" }
4580
+ });
4581
+ }
4582
+ break;
4583
+ }
4584
+ }
4585
+ }
4586
+ }
4587
+ return records;
4588
+ }
4589
+ collectRustValidateAttrs(lines, startLine) {
4590
+ const constraints = [];
4591
+ let pendingValidate;
4592
+ for (let i = startLine; i < lines.length; i++) {
4593
+ const line = lines[i].trim();
4594
+ if (line === "}") break;
4595
+ const attrMatch = line.match(/#\[validate\((.+?)\)\]/);
4596
+ if (attrMatch) {
4597
+ pendingValidate = attrMatch[1];
4598
+ continue;
4599
+ }
4600
+ if (pendingValidate) {
4601
+ const fieldMatch = line.match(/(?:pub\s+)?(\w+)\s*:/);
4602
+ if (fieldMatch) {
4603
+ constraints.push(`${fieldMatch[1]}: ${pendingValidate}`);
4604
+ pendingValidate = void 0;
4605
+ }
4606
+ }
4607
+ }
4608
+ return constraints;
4609
+ }
4610
+ extractBeanValidation(content, filePath, language) {
4611
+ const records = [];
4612
+ const lines = content.split("\n");
4613
+ const classRanges = this.findJavaClasses(lines);
4614
+ for (const { name: className, startLine, endLine } of classRanges) {
4615
+ const constraints = this.collectBeanConstraints(lines, startLine, endLine);
4616
+ if (constraints.length > 0) {
4617
+ records.push({
4618
+ id: `extracted:validation-rules:${hash(filePath + ":" + className)}`,
4619
+ extractor: "validation-rules",
4620
+ language,
4621
+ filePath,
4622
+ line: startLine + 1,
4623
+ nodeType: "business_rule",
4624
+ name: className,
4625
+ content: `${className}: ${constraints.join("; ")}`,
4626
+ confidence: 0.8,
4627
+ metadata: { kind: "bean-validation", constraints, framework: "javax.validation" }
4628
+ });
4629
+ }
4630
+ }
4631
+ return records;
4632
+ }
4633
+ findJavaClasses(lines) {
4634
+ const classes = [];
4635
+ for (let i = 0; i < lines.length; i++) {
4636
+ const line = lines[i];
4637
+ const match = line.match(/(?:public\s+|private\s+|protected\s+)?class\s+(\w+)/);
4638
+ if (match) {
4639
+ const endLine = this.findClosingBrace(lines, i);
4640
+ classes.push({ name: match[1], startLine: i, endLine });
4641
+ }
4642
+ }
4643
+ return classes;
4644
+ }
4645
+ findClosingBrace(lines, startLine) {
4646
+ let depth = 0;
4647
+ for (let i = startLine; i < lines.length; i++) {
4648
+ for (const ch of lines[i]) {
4649
+ if (ch === "{") depth++;
4650
+ if (ch === "}") {
4651
+ depth--;
4652
+ if (depth === 0) return i;
4653
+ }
4654
+ }
4655
+ }
4656
+ return lines.length - 1;
4657
+ }
4658
+ collectBeanConstraints(lines, startLine, endLine) {
4659
+ const constraints = [];
4660
+ const validationAnnotations = [
4661
+ "@NotNull",
4662
+ "@NotBlank",
4663
+ "@NotEmpty",
4664
+ "@Size",
4665
+ "@Min",
4666
+ "@Max",
4667
+ "@DecimalMin",
4668
+ "@DecimalMax",
4669
+ "@Email",
4670
+ "@Pattern",
4671
+ "@Positive",
4672
+ "@Negative",
4673
+ "@Past",
4674
+ "@Future"
4675
+ ];
4676
+ let pendingAnnotations = [];
4677
+ for (let i = startLine + 1; i < endLine; i++) {
4678
+ const line = lines[i].trim();
4679
+ for (const anno of validationAnnotations) {
4680
+ if (line.startsWith(anno)) {
4681
+ pendingAnnotations.push(line.replace(/;?\s*$/, ""));
4682
+ }
4683
+ }
4684
+ if (pendingAnnotations.length > 0) {
4685
+ const fieldMatch = line.match(
4686
+ /(?:private|public|protected)\s+(?:[\w<>[\]]+)\s+(\w+)\s*[;=]/
4687
+ );
4688
+ if (fieldMatch) {
4689
+ constraints.push(`${fieldMatch[1]}: ${pendingAnnotations.join(", ")}`);
4690
+ pendingAnnotations = [];
4691
+ }
4692
+ }
4693
+ }
4694
+ return constraints;
4695
+ }
4696
+ };
4697
+ ApiPathExtractor = class {
4698
+ name = "api-paths";
4699
+ supportedExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"];
4700
+ extract(content, filePath, language) {
4701
+ switch (language) {
4702
+ case "typescript":
4703
+ case "javascript":
4704
+ return this.extractJsTs(content, filePath, language);
4705
+ case "python":
4706
+ return this.extractPython(content, filePath, language);
4707
+ case "go":
4708
+ return this.extractGo(content, filePath, language);
4709
+ case "rust":
4710
+ return this.extractRust(content, filePath, language);
4711
+ case "java":
4712
+ return this.extractJava(content, filePath, language);
4713
+ }
4714
+ }
4715
+ extractJsTs(content, filePath, language) {
4716
+ const records = [];
4717
+ const lines = content.split("\n");
4718
+ const routePattern = /(?:app|router|server|fastify)\.(get|post|put|patch|delete|head|options)\s*\(\s*(['"`])([^'"`]+)\2/i;
4719
+ for (let i = 0; i < lines.length; i++) {
4720
+ const line = lines[i];
4721
+ const match = line.match(routePattern);
4722
+ if (match) {
4723
+ const method = match[1].toUpperCase();
4724
+ const routePath = match[3];
4725
+ const patternKey = `${method} ${routePath}`;
4726
+ records.push({
4727
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4728
+ extractor: "api-paths",
4729
+ language,
4730
+ filePath,
4731
+ line: i + 1,
4732
+ nodeType: "business_process",
4733
+ name: patternKey,
4734
+ content: `${method} ${routePath}`,
4735
+ confidence: 0.9,
4736
+ metadata: { method, path: routePath, framework: "express" }
4737
+ });
4738
+ }
4739
+ }
4740
+ return records;
4741
+ }
4742
+ extractPython(content, filePath, language) {
4743
+ const records = [];
4744
+ const lines = content.split("\n");
4745
+ const decoratorPattern = /@(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*["']([^"']+)["']/i;
4746
+ const flaskPattern = /@(?:app|blueprint)\.route\s*\(\s*["']([^"']+)["'](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?/i;
4747
+ for (let i = 0; i < lines.length; i++) {
4748
+ const line = lines[i];
4749
+ const fastApiMatch = line.match(decoratorPattern);
4750
+ if (fastApiMatch) {
4751
+ const method = fastApiMatch[1].toUpperCase();
4752
+ const routePath = fastApiMatch[2];
4753
+ const patternKey = `${method} ${routePath}`;
4754
+ records.push({
4755
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4756
+ extractor: "api-paths",
4757
+ language,
4758
+ filePath,
4759
+ line: i + 1,
4760
+ nodeType: "business_process",
4761
+ name: patternKey,
4762
+ content: `${method} ${routePath}`,
4763
+ confidence: 0.9,
4764
+ metadata: { method, path: routePath, framework: "fastapi" }
4765
+ });
4766
+ continue;
4767
+ }
4768
+ const flaskMatch = line.match(flaskPattern);
4769
+ if (flaskMatch) {
4770
+ const routePath = flaskMatch[1];
4771
+ const methods = flaskMatch[2] ? flaskMatch[2].split(",").map((m) => m.trim().replace(/["']/g, "")) : ["GET"];
4772
+ for (const method of methods) {
4773
+ const patternKey = `${method} ${routePath}`;
4774
+ records.push({
4775
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4776
+ extractor: "api-paths",
4777
+ language,
4778
+ filePath,
4779
+ line: i + 1,
4780
+ nodeType: "business_process",
4781
+ name: patternKey,
4782
+ content: `${method} ${routePath}`,
4783
+ confidence: 0.9,
4784
+ metadata: { method, path: routePath, framework: "flask" }
4785
+ });
4786
+ }
4787
+ }
4788
+ }
4789
+ return records;
4790
+ }
4791
+ extractGo(content, filePath, language) {
4792
+ const records = [];
4793
+ const lines = content.split("\n");
4794
+ const ginPattern = /(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*"([^"]+)"/i;
4795
+ const httpPattern = /http\.HandleFunc\s*\(\s*"([^"]+)"/;
4796
+ for (let i = 0; i < lines.length; i++) {
4797
+ const line = lines[i];
4798
+ const ginMatch = line.match(ginPattern);
4799
+ if (ginMatch) {
4800
+ const method = ginMatch[1].toUpperCase();
4801
+ const routePath = ginMatch[2];
4802
+ const patternKey = `${method} ${routePath}`;
4803
+ records.push({
4804
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4805
+ extractor: "api-paths",
4806
+ language,
4807
+ filePath,
4808
+ line: i + 1,
4809
+ nodeType: "business_process",
4810
+ name: patternKey,
4811
+ content: `${method} ${routePath}`,
4812
+ confidence: 0.9,
4813
+ metadata: { method, path: routePath, framework: "gin" }
4814
+ });
4815
+ continue;
4816
+ }
4817
+ const httpMatch = line.match(httpPattern);
4818
+ if (httpMatch) {
4819
+ const routePath = httpMatch[1];
4820
+ const patternKey = `ANY ${routePath}`;
4821
+ records.push({
4822
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4823
+ extractor: "api-paths",
4824
+ language,
4825
+ filePath,
4826
+ line: i + 1,
4827
+ nodeType: "business_process",
4828
+ name: patternKey,
4829
+ content: `ANY ${routePath}`,
4830
+ confidence: 0.6,
4831
+ metadata: { method: "ANY", path: routePath, framework: "net/http" }
4832
+ });
4833
+ }
4834
+ }
4835
+ return records;
4836
+ }
4837
+ extractRust(content, filePath, language) {
4838
+ const records = [];
4839
+ const lines = content.split("\n");
4840
+ const actixPattern = /#\[(get|post|put|patch|delete|head|options)\s*\(\s*"([^"]+)"\s*\)\s*\]/i;
4841
+ for (let i = 0; i < lines.length; i++) {
4842
+ const line = lines[i];
4843
+ const match = line.match(actixPattern);
4844
+ if (match) {
4845
+ const method = match[1].toUpperCase();
4846
+ const routePath = match[2];
4847
+ const patternKey = `${method} ${routePath}`;
4848
+ records.push({
4849
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4850
+ extractor: "api-paths",
4851
+ language,
4852
+ filePath,
4853
+ line: i + 1,
4854
+ nodeType: "business_process",
4855
+ name: patternKey,
4856
+ content: `${method} ${routePath}`,
4857
+ confidence: 0.9,
4858
+ metadata: { method, path: routePath, framework: "actix" }
4859
+ });
4860
+ }
4861
+ }
4862
+ return records;
4863
+ }
4864
+ extractJava(content, filePath, language) {
4865
+ const records = [];
4866
+ const lines = content.split("\n");
4867
+ const springPattern = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/;
4868
+ let basePath = "";
4869
+ for (const line of lines) {
4870
+ const baseMatch = line.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']\s*\)/);
4871
+ if (baseMatch && line.match(/class\s/) === null) {
4872
+ basePath = baseMatch[1];
4873
+ }
4874
+ }
4875
+ for (let i = 0; i < lines.length; i++) {
4876
+ const line = lines[i];
4877
+ const match = line.match(springPattern);
4878
+ if (match) {
4879
+ const annotation = match[1];
4880
+ const routePath = basePath + match[2];
4881
+ const methodMap = {
4882
+ GetMapping: "GET",
4883
+ PostMapping: "POST",
4884
+ PutMapping: "PUT",
4885
+ PatchMapping: "PATCH",
4886
+ DeleteMapping: "DELETE",
4887
+ RequestMapping: "ANY"
4888
+ };
4889
+ const method = methodMap[annotation] ?? "ANY";
4890
+ const patternKey = `${method} ${routePath}`;
4891
+ records.push({
4892
+ id: `extracted:api-paths:${hash(filePath + ":" + patternKey)}`,
4893
+ extractor: "api-paths",
4894
+ language,
4895
+ filePath,
4896
+ line: i + 1,
4897
+ nodeType: "business_process",
4898
+ name: patternKey,
4899
+ content: `${method} ${routePath}`,
4900
+ confidence: 0.9,
4901
+ metadata: { method, path: routePath, framework: "spring" }
4902
+ });
4903
+ }
4904
+ }
4905
+ return records;
4906
+ }
4907
+ };
4908
+ ALL_EXTRACTORS = [
4909
+ new TestDescriptionExtractor(),
4910
+ new EnumConstantExtractor(),
4911
+ new ValidationRuleExtractor(),
4912
+ new ApiPathExtractor()
4913
+ ];
4914
+ ImageAnalysisExtractor = class {
4915
+ provider;
4916
+ /** Maximum file size in bytes. Reserved for future file-size filtering. */
4917
+ maxFileSizeBytes;
4918
+ constructor(options) {
4919
+ this.provider = options.analysisProvider;
4920
+ this.maxFileSizeBytes = (options.maxFileSizeMB ?? 10) * 1024 * 1024;
4921
+ }
4922
+ async analyze(store, imagePaths) {
4923
+ const start = Date.now();
4924
+ let nodesAdded = 0;
4925
+ let edgesAdded = 0;
4926
+ const errors = [];
4927
+ for (const imagePath of imagePaths) {
4928
+ try {
4929
+ const response = await this.provider.analyze({
4930
+ prompt: "Analyze this image and provide a structured description of its visual contents.",
4931
+ systemPrompt: "You are an image analysis assistant. Describe the image contents, detect UI elements, extract visible text, identify design patterns, and note accessibility concerns.",
4932
+ responseSchema: {},
4933
+ // Schema handled by provider
4934
+ maxTokens: 1e3
4935
+ });
4936
+ const result = response.result;
4937
+ const pathHash = hash(imagePath);
4938
+ const annotationId = `img:${pathHash}`;
4939
+ store.addNode({
4940
+ id: annotationId,
4941
+ type: "image_annotation",
4942
+ name: result.description.slice(0, 200),
4943
+ path: imagePath,
4944
+ content: result.description,
4945
+ hash: hash(result.description),
4946
+ metadata: {
4947
+ source: "image-analysis",
4948
+ detectedElements: result.detectedElements,
4949
+ extractedText: result.extractedText,
4950
+ designPatterns: result.designPatterns,
4951
+ accessibilityNotes: result.accessibilityNotes,
4952
+ model: response.model
4953
+ }
4954
+ });
4955
+ nodesAdded++;
4956
+ const fileNodes = store.findNodes({ type: "file" });
4957
+ const fileNode = fileNodes.find((n) => n.path === imagePath);
4958
+ if (fileNode) {
4959
+ store.addEdge({ from: annotationId, to: fileNode.id, type: "annotates" });
4960
+ edgesAdded++;
4961
+ }
4962
+ for (const pattern of result.designPatterns) {
4963
+ const conceptId = `img:concept:${hash(pattern + imagePath)}`;
4964
+ store.addNode({
4965
+ id: conceptId,
4966
+ type: "business_concept",
4967
+ name: pattern,
4968
+ content: `Design pattern detected in ${imagePath}: ${pattern}`,
4969
+ hash: hash(pattern),
4970
+ metadata: {
4971
+ source: "image-analysis",
4972
+ sourceImage: imagePath,
4973
+ domain: "design"
4974
+ }
4975
+ });
4976
+ nodesAdded++;
4977
+ store.addEdge({ from: annotationId, to: conceptId, type: "contains" });
4978
+ edgesAdded++;
4979
+ }
4980
+ } catch (err) {
4981
+ errors.push(
4982
+ `Image analysis failed for ${imagePath}: ${err instanceof Error ? err.message : String(err)}`
4983
+ );
4984
+ }
4985
+ }
4986
+ return {
4987
+ nodesAdded,
4988
+ nodesUpdated: 0,
4989
+ edgesAdded,
4990
+ edgesUpdated: 0,
4991
+ errors,
4992
+ durationMs: Date.now() - start
4993
+ };
4994
+ }
4995
+ };
4996
+ KNOWLEDGE_NODE_TYPES = [
4997
+ "business_fact",
4998
+ "business_rule",
4999
+ "business_process",
5000
+ "business_term",
5001
+ "business_concept",
5002
+ "business_metric",
5003
+ "design_token",
5004
+ "design_constraint",
5005
+ "aesthetic_intent",
5006
+ "image_annotation"
5007
+ ];
5008
+ SIMILARITY_THRESHOLD = 0.8;
5009
+ ContradictionDetector = class {
5010
+ detect(store) {
5011
+ const nodes = KNOWLEDGE_NODE_TYPES.flatMap((t) => store.findNodes({ type: t }));
5012
+ const byName = /* @__PURE__ */ new Map();
5013
+ for (const node of nodes) {
5014
+ const key = node.name.toLowerCase().trim();
5015
+ const group = byName.get(key) ?? [];
5016
+ group.push(node);
5017
+ byName.set(key, group);
5018
+ }
5019
+ const contradictions = [];
5020
+ const sourcePairCounts = {};
5021
+ const seen = /* @__PURE__ */ new Set();
5022
+ const addContradiction = (a, b, similarity) => {
5023
+ const sourceA = a.metadata.source ?? "unknown";
5024
+ const sourceB = b.metadata.source ?? "unknown";
5025
+ if (sourceA === sourceB) return;
5026
+ const hashA = a.hash ?? a.id;
5027
+ const hashB = b.hash ?? b.id;
5028
+ if (hashA === hashB) return;
5029
+ const pairId = [a.id, b.id].sort().join(":");
5030
+ if (seen.has(pairId)) return;
5031
+ seen.add(pairId);
5032
+ const conflictType = classifyConflict(a, b);
5033
+ const severity = conflictType === "value_mismatch" ? "critical" : conflictType === "definition_conflict" ? "high" : "medium";
5034
+ const pairKey = [sourceA, sourceB].sort().join("\u2194");
5035
+ sourcePairCounts[pairKey] = (sourcePairCounts[pairKey] ?? 0) + 1;
5036
+ contradictions.push({
5037
+ id: `contradiction:${a.id}:${b.id}`,
5038
+ entityA: {
5039
+ nodeId: a.id,
5040
+ source: sourceA,
5041
+ name: a.name,
5042
+ content: a.content ?? "",
5043
+ lastModified: a.lastModified
5044
+ },
5045
+ entityB: {
5046
+ nodeId: b.id,
5047
+ source: sourceB,
5048
+ name: b.name,
5049
+ content: b.content ?? "",
5050
+ lastModified: b.lastModified
5051
+ },
5052
+ similarity,
5053
+ conflictType,
5054
+ severity,
5055
+ description: `"${a.name}" has conflicting definitions from ${sourceA} and ${sourceB}`
5056
+ });
5057
+ };
5058
+ for (const [, group] of byName) {
5059
+ if (group.length < 2) continue;
5060
+ for (let i = 0; i < group.length; i++) {
5061
+ for (let j = i + 1; j < group.length; j++) {
5062
+ addContradiction(group[i], group[j], 1);
5063
+ }
5064
+ }
5065
+ }
5066
+ const keys = [...byName.keys()];
5067
+ for (let i = 0; i < keys.length; i++) {
5068
+ for (let j = i + 1; j < keys.length; j++) {
5069
+ const keyA = keys[i];
5070
+ const keyB = keys[j];
5071
+ const maxLen = Math.max(keyA.length, keyB.length);
5072
+ const minLen = Math.min(keyA.length, keyB.length);
5073
+ if (minLen / maxLen < SIMILARITY_THRESHOLD) continue;
5074
+ const ratio = levenshteinRatio(keyA, keyB);
5075
+ if (ratio < SIMILARITY_THRESHOLD) continue;
5076
+ const groupA = byName.get(keyA);
5077
+ const groupB = byName.get(keyB);
5078
+ for (const a of groupA) {
5079
+ for (const b of groupB) {
5080
+ addContradiction(a, b, ratio);
5081
+ }
5082
+ }
5083
+ }
5084
+ }
5085
+ return { contradictions, sourcePairCounts, totalChecked: nodes.length };
5086
+ }
5087
+ };
5088
+ KNOWLEDGE_TYPES = KNOWLEDGE_NODE_TYPES;
5089
+ CODE_TYPES2 = [
5090
+ "file",
5091
+ "function",
5092
+ "class",
5093
+ "method",
5094
+ "interface",
5095
+ "variable"
5096
+ ];
5097
+ KNOWLEDGE_EDGE_TYPES = [
5098
+ "governs",
5099
+ "documents",
5100
+ "measures",
5101
+ "applies_to",
5102
+ "references",
5103
+ "uses_token",
5104
+ "declares_intent",
5105
+ "annotates"
5106
+ ];
5107
+ CoverageScorer = class {
5108
+ score(store) {
5109
+ const knowledgeNodes = KNOWLEDGE_TYPES.flatMap((t) => store.findNodes({ type: t }));
5110
+ const domainMap = /* @__PURE__ */ new Map();
5111
+ for (const node of knowledgeNodes) {
5112
+ const domain = node.metadata.domain ?? "unclassified";
5113
+ const group = domainMap.get(domain) ?? [];
5114
+ group.push(node);
5115
+ domainMap.set(domain, group);
5116
+ }
5117
+ const codeNodes = CODE_TYPES2.flatMap((t) => store.findNodes({ type: t }));
5118
+ const codeDomains = /* @__PURE__ */ new Map();
5119
+ for (const node of codeNodes) {
5120
+ const domain = node.metadata.domain ?? this.domainFromPath(node.path);
5121
+ const group = codeDomains.get(domain) ?? [];
5122
+ group.push(node);
5123
+ codeDomains.set(domain, group);
5124
+ }
5125
+ const allDomains = /* @__PURE__ */ new Set([...domainMap.keys(), ...codeDomains.keys()]);
5126
+ const domains = [];
5127
+ for (const domain of allDomains) {
5128
+ const knEntries = domainMap.get(domain) ?? [];
5129
+ const codeEntries = codeDomains.get(domain) ?? [];
5130
+ const linkedIds = /* @__PURE__ */ new Set();
5131
+ for (const codeNode of codeEntries) {
5132
+ for (const edgeType of KNOWLEDGE_EDGE_TYPES) {
5133
+ const edges = store.getEdges({ to: codeNode.id, type: edgeType });
5134
+ if (edges.length > 0) {
5135
+ linkedIds.add(codeNode.id);
5136
+ break;
5137
+ }
5138
+ }
5139
+ }
5140
+ const sourceBreakdown = {};
5141
+ for (const kn of knEntries) {
5142
+ const src = kn.metadata.source ?? "unknown";
5143
+ sourceBreakdown[src] = (sourceBreakdown[src] ?? 0) + 1;
5144
+ }
5145
+ const codeEntities = codeEntries.length;
5146
+ const linkedEntities = linkedIds.size;
5147
+ const knowledgeEntries = knEntries.length;
5148
+ const uniqueSources = Object.keys(sourceBreakdown).length;
5149
+ const codeCoverageComponent = codeEntities > 0 ? linkedEntities / codeEntities * 60 : 0;
5150
+ const knowledgeDepthComponent = Math.min(knowledgeEntries / 10, 1) * 20;
5151
+ const sourceDiversityComponent = Math.min(uniqueSources / 3, 1) * 20;
5152
+ const score = Math.round(
5153
+ codeCoverageComponent + knowledgeDepthComponent + sourceDiversityComponent
5154
+ );
5155
+ domains.push({
5156
+ domain,
5157
+ score,
5158
+ knowledgeEntries,
5159
+ codeEntities,
5160
+ linkedEntities,
5161
+ unlinkedEntities: codeEntities - linkedEntities,
5162
+ sourceBreakdown,
5163
+ grade: toGrade(score)
5164
+ });
5165
+ }
5166
+ const overallScore = domains.length > 0 ? Math.round(domains.reduce((sum, d) => sum + d.score, 0) / domains.length) : 0;
5167
+ return {
5168
+ domains,
5169
+ overallScore,
5170
+ overallGrade: toGrade(overallScore),
5171
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
5172
+ };
5173
+ }
5174
+ domainFromPath(filePath) {
5175
+ if (!filePath) return "unclassified";
5176
+ const parts = filePath.split("/");
5177
+ const pkgIdx = parts.indexOf("packages");
5178
+ if (pkgIdx >= 0 && parts[pkgIdx + 1]) return parts[pkgIdx + 1];
5179
+ const srcIdx = parts.indexOf("src");
5180
+ if (srcIdx >= 0 && parts[srcIdx + 1]) return parts[srcIdx + 1];
5181
+ return parts[0] ?? "unclassified";
5182
+ }
5183
+ };
5184
+ BUSINESS_NODE_TYPES = [
5185
+ "business_concept",
5186
+ "business_rule",
5187
+ "business_process",
5188
+ "business_term",
5189
+ "business_metric",
5190
+ "business_fact"
5191
+ ];
5192
+ SNAPSHOT_NODE_TYPES = [
5193
+ ...BUSINESS_NODE_TYPES,
5194
+ "design_token",
5195
+ "design_constraint",
5196
+ "aesthetic_intent",
5197
+ "image_annotation"
5198
+ ];
5199
+ KnowledgePipelineRunner = class {
5200
+ constructor(store) {
5201
+ this.store = store;
5202
+ }
5203
+ store;
5204
+ async run(options) {
5205
+ const maxIterations = options.maxIterations ?? 5;
5206
+ const remediations = [];
5207
+ const preSnapshot = this.buildSnapshot(options.domain);
5208
+ const extraction = await this.extract(options);
5209
+ const postSnapshot = this.buildSnapshot(options.domain);
5210
+ let driftResult = this.reconcile(preSnapshot, postSnapshot);
5211
+ const contradictionDetector = new ContradictionDetector();
5212
+ const contradictions = contradictionDetector.detect(this.store);
5213
+ let gapReport = await this.detect(options);
5214
+ const coverageScorer = new CoverageScorer();
5215
+ const coverage = coverageScorer.score(this.store);
5216
+ let iterations = 1;
5217
+ if (options.fix) {
5218
+ let previousFindingCount = driftResult.findings.length;
5219
+ while (iterations < maxIterations) {
5220
+ if (driftResult.findings.length === 0) break;
5221
+ this.remediate(driftResult, remediations, options);
5222
+ const rePreSnapshot = this.buildSnapshot(options.domain);
5223
+ await this.extract(options);
5224
+ const rePostSnapshot = this.buildSnapshot(options.domain);
5225
+ driftResult = this.reconcile(rePreSnapshot, rePostSnapshot);
5226
+ gapReport = await this.detect(options);
5227
+ iterations++;
5228
+ const newFindingCount = driftResult.findings.length;
5229
+ if (newFindingCount >= previousFindingCount) break;
5230
+ previousFindingCount = newFindingCount;
5231
+ }
5232
+ }
5233
+ await this.stageNewFindings(driftResult, options);
5234
+ return {
5235
+ verdict: this.computeVerdict(driftResult),
5236
+ driftScore: driftResult.driftScore,
5237
+ iterations,
5238
+ findings: driftResult.summary,
5239
+ extraction,
5240
+ gaps: gapReport,
5241
+ remediations,
5242
+ contradictions,
5243
+ coverage
5244
+ };
5245
+ }
5246
+ // ── Phase 1: EXTRACT ──────────────────────────────────────────────────────
5247
+ async extract(options) {
5248
+ const extractedDir = path10.join(options.projectDir, ".harness", "knowledge", "extracted");
5249
+ await fs9.mkdir(extractedDir, { recursive: true });
5250
+ const runner = createExtractionRunner();
5251
+ const extractionResult = await runner.run(options.projectDir, this.store, extractedDir);
5252
+ const diagramParser = new DiagramParser(this.store);
5253
+ const diagramResult = await diagramParser.ingest(options.projectDir);
5254
+ let imageCount = 0;
5255
+ const imagePaths = options.imagePaths ?? [];
5256
+ if (options.analyzeImages && options.analysisProvider && imagePaths.length > 0) {
5257
+ const imageExtractor = new ImageAnalysisExtractor({
5258
+ analysisProvider: options.analysisProvider
5259
+ });
5260
+ const imageResult = await imageExtractor.analyze(this.store, imagePaths);
5261
+ imageCount = imageResult.nodesAdded;
5262
+ }
5263
+ const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5264
+ const bkIngestor = new BusinessKnowledgeIngestor(this.store);
5265
+ let bkResult;
5266
+ try {
5267
+ bkResult = await bkIngestor.ingest(knowledgeDir);
5268
+ } catch {
5269
+ bkResult = {
5270
+ nodesAdded: 0,
5271
+ nodesUpdated: 0,
5272
+ edgesAdded: 0,
5273
+ edgesUpdated: 0,
5274
+ errors: [],
5275
+ durationMs: 0
5276
+ };
5277
+ }
5278
+ const linker = new KnowledgeLinker(this.store, extractedDir);
5279
+ const linkResult = await linker.link();
5280
+ return {
5281
+ codeSignals: extractionResult.nodesAdded,
5282
+ diagrams: diagramResult.nodesAdded,
5283
+ linkerFacts: linkResult.factsCreated,
5284
+ businessKnowledge: bkResult.nodesAdded,
5285
+ images: imageCount
5286
+ };
5287
+ }
5288
+ // ── Phase 2: RECONCILE ────────────────────────────────────────────────────
5289
+ buildSnapshot(domain) {
5290
+ let nodes = SNAPSHOT_NODE_TYPES.flatMap((type) => this.store.findNodes({ type }));
5291
+ if (domain) {
5292
+ nodes = nodes.filter((n) => n.metadata?.domain === domain);
5293
+ }
5294
+ return {
5295
+ entries: nodes.map((n) => ({
5296
+ id: n.id,
5297
+ type: n.type,
5298
+ contentHash: n.hash ?? n.id,
5299
+ source: n.metadata?.source ?? "unknown",
5300
+ name: n.name
5301
+ })),
5302
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5303
+ };
5304
+ }
5305
+ reconcile(current, fresh) {
5306
+ const detector = new StructuralDriftDetector();
5307
+ return detector.detect(current, fresh);
5308
+ }
5309
+ // ── Phase 3: DETECT ───────────────────────────────────────────────────────
5310
+ async detect(options) {
5311
+ const knowledgeDir = path10.join(options.projectDir, "docs", "knowledge");
5312
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir);
5313
+ const gapReport = await aggregator.generateGapReport(knowledgeDir);
5314
+ await aggregator.writeGapReport(gapReport);
5315
+ return gapReport;
5316
+ }
5317
+ // ── Phase 4: REMEDIATE ────────────────────────────────────────────────────
5318
+ remediate(driftResult, remediations, options) {
5319
+ for (const finding of driftResult.findings) {
5320
+ switch (finding.classification) {
5321
+ case "stale":
5322
+ this.store.removeNode(finding.entryId);
5323
+ remediations.push(`removed stale: ${finding.entryId}`);
5324
+ break;
5325
+ case "new":
5326
+ break;
5327
+ case "drifted":
5328
+ if (!options.ci) {
5329
+ remediations.push(`flagged drifted: ${finding.entryId}`);
5330
+ }
5331
+ break;
5332
+ case "contradicting":
5333
+ break;
5334
+ }
5335
+ }
5336
+ }
5337
+ async stageNewFindings(driftResult, options) {
5338
+ const newFindings = driftResult.findings.filter((f) => f.classification === "new");
5339
+ if (newFindings.length === 0) return;
5340
+ const stagedEntries = newFindings.filter((f) => f.fresh != null).map((f) => ({
5341
+ id: f.fresh.id,
5342
+ source: this.classifySource(f.fresh.source),
5343
+ nodeType: f.fresh.type,
5344
+ name: f.fresh.name,
5345
+ confidence: 0.7,
5346
+ contentHash: f.fresh.contentHash,
5347
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5348
+ }));
5349
+ if (stagedEntries.length > 0) {
5350
+ const aggregator = new KnowledgeStagingAggregator(options.projectDir);
5351
+ await aggregator.aggregate(stagedEntries, [], []);
5352
+ }
5353
+ }
5354
+ classifySource(source) {
5355
+ if (source === "linker" || source === "knowledge-linker") return "linker";
5356
+ if (source === "diagram") return "diagram";
5357
+ return "extractor";
5358
+ }
5359
+ // ── Verdict ───────────────────────────────────────────────────────────────
5360
+ computeVerdict(driftResult) {
5361
+ const { summary } = driftResult;
5362
+ const unresolved = summary.drifted + summary.stale + summary.contradicting;
5363
+ if (unresolved === 0 && summary.new === 0) return "pass";
5364
+ if (unresolved === 0) return "warn";
5365
+ return "fail";
5366
+ }
5367
+ };
5368
+ CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
5369
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
5370
+ SANITIZE_RULES = [
5371
+ // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
5372
+ {
5373
+ pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
5374
+ replacement: ""
5375
+ },
5376
+ // Strip markdown-style system prompt markers (including trailing space)
5377
+ {
5378
+ pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
5379
+ replacement: ""
5380
+ },
5381
+ // Strip common injection prefixes
5382
+ {
5383
+ pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
5384
+ replacement: "[filtered]"
5385
+ },
5386
+ // Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
5387
+ {
5388
+ pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
5389
+ replacement: "[filtered]"
5390
+ }
5391
+ ];
5392
+ SyncManager = class {
5393
+ constructor(store, graphDir) {
5394
+ this.store = store;
5395
+ this.metadataPath = path11.join(graphDir, "sync-metadata.json");
5396
+ }
5397
+ store;
5398
+ registrations = /* @__PURE__ */ new Map();
5399
+ metadataPath;
5400
+ registerConnector(connector, config) {
5401
+ this.registrations.set(connector.name, { connector, config });
5402
+ }
5403
+ async sync(connectorName) {
5404
+ const registration = this.registrations.get(connectorName);
5405
+ if (!registration) {
5406
+ return {
5407
+ nodesAdded: 0,
5408
+ nodesUpdated: 0,
5409
+ edgesAdded: 0,
5410
+ edgesUpdated: 0,
5411
+ errors: [`Connector "${connectorName}" not registered`],
5412
+ durationMs: 0
5413
+ };
5414
+ }
5415
+ const { connector, config } = registration;
5416
+ const result = await connector.ingest(this.store, config);
5417
+ const metadata = await this.loadMetadata();
5418
+ metadata.connectors[connectorName] = {
5419
+ lastSyncTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
5420
+ lastResult: result
5421
+ };
5422
+ await this.saveMetadata(metadata);
5423
+ return result;
5424
+ }
5425
+ async syncAll() {
5426
+ const combined = {
5427
+ nodesAdded: 0,
5428
+ nodesUpdated: 0,
5429
+ edgesAdded: 0,
5430
+ edgesUpdated: 0,
5431
+ errors: [],
5432
+ durationMs: 0
5433
+ };
5434
+ for (const [name] of this.registrations) {
5435
+ const result = await this.sync(name);
5436
+ combined.nodesAdded += result.nodesAdded;
5437
+ combined.nodesUpdated += result.nodesUpdated;
5438
+ combined.edgesAdded += result.edgesAdded;
5439
+ combined.edgesUpdated += result.edgesUpdated;
5440
+ combined.errors.push(...result.errors);
5441
+ combined.durationMs += result.durationMs;
5442
+ }
5443
+ try {
5444
+ const linker = new KnowledgeLinker(this.store);
5445
+ const linkResult = await linker.link();
5446
+ combined.nodesAdded += linkResult.factsCreated + linkResult.conceptsClustered;
5447
+ combined.errors.push(...linkResult.errors);
5448
+ } catch (err) {
5449
+ combined.errors.push(
5450
+ `KnowledgeLinker error: ${err instanceof Error ? err.message : String(err)}`
5451
+ );
5452
+ }
5453
+ return combined;
5454
+ }
5455
+ async getMetadata() {
5456
+ return this.loadMetadata();
5457
+ }
5458
+ async loadMetadata() {
5459
+ try {
5460
+ const raw = await fs10.readFile(this.metadataPath, "utf-8");
5461
+ return JSON.parse(raw);
5462
+ } catch {
5463
+ return { connectors: {} };
5464
+ }
5465
+ }
5466
+ async saveMetadata(metadata) {
5467
+ await fs10.mkdir(path11.dirname(this.metadataPath), { recursive: true });
5468
+ await fs10.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
5469
+ }
5470
+ };
5471
+ SUMMARIZE_PROMPT = `Summarize the following content to fit within the specified length.
5472
+ Preserve all business rules, SLAs, requirements, decisions, and regulatory references.
5473
+ Remove redundant details and boilerplate while keeping all actionable information.
5474
+
5475
+ Content to summarize:
5476
+ `;
5477
+ JiraConnector = class {
5478
+ name = "jira";
5479
+ source = "jira";
5480
+ httpClient;
5481
+ constructor(httpClient) {
5482
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
5483
+ }
5484
+ async ingest(store, config) {
5485
+ const start = Date.now();
5486
+ const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
5487
+ const apiKey = process.env[apiKeyEnv];
5488
+ if (!apiKey) {
5489
+ return buildIngestResult(
5490
+ 0,
5491
+ 0,
5492
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5493
+ start
5494
+ );
5495
+ }
5496
+ const baseUrlEnv = config.baseUrlEnv ?? "JIRA_BASE_URL";
5497
+ const baseUrl = process.env[baseUrlEnv];
5498
+ if (!baseUrl) {
5499
+ return buildIngestResult(
5500
+ 0,
5501
+ 0,
5502
+ [`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
5503
+ start
5504
+ );
2777
5505
  }
2778
5506
  const jql = buildJql(config);
2779
5507
  const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
2780
5508
  try {
2781
- const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
5509
+ const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
2782
5510
  return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
2783
5511
  } catch (err) {
2784
5512
  return buildIngestResult(
@@ -2789,7 +5517,7 @@ var init_dist = __esm({
2789
5517
  );
2790
5518
  }
2791
5519
  }
2792
- async fetchAllIssues(store, baseUrl, jql, headers) {
5520
+ async fetchAllIssues(store, baseUrl, jql, headers, config) {
2793
5521
  let nodesAdded = 0;
2794
5522
  let edgesAdded = 0;
2795
5523
  let startAt = 0;
@@ -2802,7 +5530,7 @@ var init_dist = __esm({
2802
5530
  const data = await response.json();
2803
5531
  total = data.total;
2804
5532
  for (const issue of data.issues) {
2805
- const counts = this.processIssue(store, issue);
5533
+ const counts = await this.processIssue(store, issue, baseUrl, headers, config);
2806
5534
  nodesAdded += counts.nodesAdded;
2807
5535
  edgesAdded += counts.edgesAdded;
2808
5536
  }
@@ -2810,19 +5538,47 @@ var init_dist = __esm({
2810
5538
  }
2811
5539
  return { nodesAdded, edgesAdded };
2812
5540
  }
2813
- processIssue(store, issue) {
5541
+ async processIssue(store, issue, baseUrl, headers, config) {
2814
5542
  const nodeId = `issue:jira:${issue.key}`;
5543
+ const comments = await this.fetchComments(baseUrl, issue.key, headers);
5544
+ const parts = [issue.fields.summary];
5545
+ if (issue.fields.description) {
5546
+ parts.push(issue.fields.description);
5547
+ }
5548
+ if (comments.length > 0) {
5549
+ for (const comment of comments) {
5550
+ parts.push(`${comment.author.displayName} (${comment.created}): ${comment.body}`);
5551
+ }
5552
+ }
5553
+ const rawContent = parts.join("\n");
5554
+ const acceptanceCriteria = parseAcceptanceCriteria(issue.fields.description ?? "");
5555
+ const customFields = extractCustomFields(issue.fields);
5556
+ const maxLen = config.maxContentLength ?? 4e3;
5557
+ const condensed = await condenseContent(rawContent, { maxLength: maxLen });
5558
+ const metadata = {
5559
+ key: issue.key,
5560
+ status: issue.fields.status?.name,
5561
+ priority: issue.fields.priority?.name,
5562
+ assignee: issue.fields.assignee?.displayName,
5563
+ labels: issue.fields.labels ?? [],
5564
+ commentCount: comments.length
5565
+ };
5566
+ if (acceptanceCriteria.length > 0) {
5567
+ metadata.acceptanceCriteria = acceptanceCriteria;
5568
+ }
5569
+ if (Object.keys(customFields).length > 0) {
5570
+ metadata.customFields = customFields;
5571
+ }
5572
+ if (condensed.method !== "passthrough") {
5573
+ metadata.condensed = condensed.method;
5574
+ metadata.originalLength = condensed.originalLength;
5575
+ }
2815
5576
  store.addNode({
2816
5577
  id: nodeId,
2817
5578
  type: "issue",
2818
5579
  name: sanitizeExternalText(issue.fields.summary, 500),
2819
- metadata: {
2820
- key: issue.key,
2821
- status: issue.fields.status?.name,
2822
- priority: issue.fields.priority?.name,
2823
- assignee: issue.fields.assignee?.displayName,
2824
- labels: issue.fields.labels ?? []
2825
- }
5580
+ content: condensed.content,
5581
+ metadata
2826
5582
  });
2827
5583
  const searchText = sanitizeExternalText(
2828
5584
  [issue.fields.summary, issue.fields.description ?? ""].join(" ")
@@ -2830,13 +5586,24 @@ var init_dist = __esm({
2830
5586
  const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
2831
5587
  return { nodesAdded: 1, edgesAdded };
2832
5588
  }
5589
+ async fetchComments(baseUrl, issueKey, headers) {
5590
+ try {
5591
+ const url = `${baseUrl}/rest/api/2/issue/${issueKey}/comment`;
5592
+ const response = await this.httpClient(url, { headers });
5593
+ if (!response.ok) return [];
5594
+ const data = await response.json();
5595
+ return data.comments ?? [];
5596
+ } catch {
5597
+ return [];
5598
+ }
5599
+ }
2833
5600
  };
2834
5601
  SlackConnector = class {
2835
5602
  name = "slack";
2836
5603
  source = "slack";
2837
5604
  httpClient;
2838
5605
  constructor(httpClient) {
2839
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5606
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2840
5607
  }
2841
5608
  async ingest(store, config) {
2842
5609
  const start = Date.now();
@@ -2859,7 +5626,7 @@ var init_dist = __esm({
2859
5626
  const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
2860
5627
  for (const channel of channels) {
2861
5628
  try {
2862
- const result = await this.processChannel(store, channel, apiKey, oldest);
5629
+ const result = await this.processChannel(store, channel, apiKey, oldest, config);
2863
5630
  nodesAdded += result.nodesAdded;
2864
5631
  edgesAdded += result.edgesAdded;
2865
5632
  errors.push(...result.errors);
@@ -2878,7 +5645,7 @@ var init_dist = __esm({
2878
5645
  durationMs: Date.now() - start
2879
5646
  };
2880
5647
  }
2881
- async processChannel(store, channel, apiKey, oldest) {
5648
+ async processChannel(store, channel, apiKey, oldest, config) {
2882
5649
  const errors = [];
2883
5650
  let nodesAdded = 0;
2884
5651
  let edgesAdded = 0;
@@ -2903,34 +5670,81 @@ var init_dist = __esm({
2903
5670
  if (!data.ok) {
2904
5671
  return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
2905
5672
  }
5673
+ const maxLen = config.maxContentLength ?? 2e3;
2906
5674
  for (const message of data.messages) {
2907
5675
  const nodeId = `conversation:slack:${channel}:${message.ts}`;
2908
- const sanitizedText = sanitizeExternalText(message.text);
2909
- const snippet = sanitizedText.length > 100 ? sanitizedText.slice(0, 100) : sanitizedText;
5676
+ let assembledText = message.text;
5677
+ let threadReplyCount;
5678
+ if (message.reply_count && message.reply_count > 0 && message.thread_ts) {
5679
+ const replies = await this.fetchThreadReplies(channel, message.thread_ts, apiKey);
5680
+ if (replies.length > 0) {
5681
+ const threadReplies = replies.slice(1);
5682
+ threadReplyCount = threadReplies.length;
5683
+ if (threadReplies.length > 0) {
5684
+ const replyLines = threadReplies.map((r) => `${r.user} (${r.ts}): ${r.text}`);
5685
+ assembledText = `${message.text}
5686
+ ${replyLines.join("\n")}`;
5687
+ }
5688
+ }
5689
+ }
5690
+ const condensed = await condenseContent(assembledText, { maxLength: maxLen });
5691
+ const snippet = condensed.content.length > 100 ? condensed.content.slice(0, 100) : condensed.content;
5692
+ const reactions = message.reactions ? message.reactions.reduce(
5693
+ (acc, r) => ({ ...acc, [r.name]: r.count }),
5694
+ {}
5695
+ ) : void 0;
5696
+ const metadata = {
5697
+ author: message.user,
5698
+ channel,
5699
+ timestamp: message.ts
5700
+ };
5701
+ if (threadReplyCount !== void 0) {
5702
+ metadata.threadReplyCount = threadReplyCount;
5703
+ }
5704
+ if (reactions) {
5705
+ metadata.reactions = reactions;
5706
+ }
5707
+ if (condensed.method !== "passthrough") {
5708
+ metadata.condensed = condensed.method;
5709
+ metadata.originalLength = condensed.originalLength;
5710
+ }
2910
5711
  store.addNode({
2911
5712
  id: nodeId,
2912
5713
  type: "conversation",
2913
5714
  name: snippet,
2914
- metadata: {
2915
- author: message.user,
2916
- channel,
2917
- timestamp: message.ts
2918
- }
5715
+ content: condensed.content,
5716
+ metadata
2919
5717
  });
2920
5718
  nodesAdded++;
2921
- edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
5719
+ edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
2922
5720
  checkPaths: true
2923
5721
  });
2924
5722
  }
2925
5723
  return { nodesAdded, edgesAdded, errors };
2926
5724
  }
5725
+ async fetchThreadReplies(channel, threadTs, apiKey) {
5726
+ try {
5727
+ const url = `https://slack.com/api/conversations.replies?channel=${encodeURIComponent(channel)}&ts=${threadTs}`;
5728
+ const response = await this.httpClient(url, {
5729
+ headers: {
5730
+ Authorization: `Bearer ${apiKey}`,
5731
+ "Content-Type": "application/json"
5732
+ }
5733
+ });
5734
+ if (!response.ok) return [];
5735
+ const data = await response.json();
5736
+ return data.ok ? data.messages : [];
5737
+ } catch {
5738
+ return [];
5739
+ }
5740
+ }
2927
5741
  };
2928
5742
  ConfluenceConnector = class {
2929
5743
  name = "confluence";
2930
5744
  source = "confluence";
2931
5745
  httpClient;
2932
5746
  constructor(httpClient) {
2933
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5747
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2934
5748
  }
2935
5749
  async ingest(store, config) {
2936
5750
  const start = Date.now();
@@ -2943,7 +5757,14 @@ var init_dist = __esm({
2943
5757
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
2944
5758
  const baseUrl = process.env[baseUrlEnv] ?? "";
2945
5759
  const spaceKey = config.spaceKey ?? "";
2946
- const counts = await this.fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors);
5760
+ const counts = await this.fetchAllPagesHandled(
5761
+ store,
5762
+ baseUrl,
5763
+ apiKey,
5764
+ spaceKey,
5765
+ errors,
5766
+ config
5767
+ );
2947
5768
  return {
2948
5769
  nodesAdded: counts.nodesAdded,
2949
5770
  nodesUpdated: 0,
@@ -2953,9 +5774,9 @@ var init_dist = __esm({
2953
5774
  durationMs: Date.now() - start
2954
5775
  };
2955
5776
  }
2956
- async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
5777
+ async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
2957
5778
  try {
2958
- const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
5779
+ const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
2959
5780
  errors.push(...result.errors);
2960
5781
  return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
2961
5782
  } catch (err) {
@@ -2963,7 +5784,7 @@ var init_dist = __esm({
2963
5784
  return { nodesAdded: 0, edgesAdded: 0 };
2964
5785
  }
2965
5786
  }
2966
- async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
5787
+ async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
2967
5788
  const errors = [];
2968
5789
  let nodesAdded = 0;
2969
5790
  let edgesAdded = 0;
@@ -2978,7 +5799,7 @@ var init_dist = __esm({
2978
5799
  }
2979
5800
  const data = await response.json();
2980
5801
  for (const page of data.results) {
2981
- const counts = this.processPage(store, page, spaceKey);
5802
+ const counts = await this.processPage(store, page, spaceKey, config);
2982
5803
  nodesAdded += counts.nodesAdded;
2983
5804
  edgesAdded += counts.edgesAdded;
2984
5805
  }
@@ -2986,22 +5807,43 @@ var init_dist = __esm({
2986
5807
  }
2987
5808
  return { nodesAdded, edgesAdded, errors };
2988
5809
  }
2989
- processPage(store, page, spaceKey) {
5810
+ async processPage(store, page, spaceKey, config) {
2990
5811
  const nodeId = `confluence:${page.id}`;
5812
+ let edgesAdded = 0;
5813
+ const labels = page.metadata?.labels?.results?.map((l) => l.name) ?? [];
5814
+ const parentPageId = page.ancestors && page.ancestors.length > 0 ? page.ancestors[page.ancestors.length - 1].id : void 0;
5815
+ const rawContent = `${page.title} ${page.body?.storage?.value ?? ""}`;
5816
+ const maxLen = config.maxContentLength ?? 8e3;
5817
+ const condensed = await condenseContent(rawContent, { maxLength: maxLen });
5818
+ const metadata = {
5819
+ source: "confluence",
5820
+ spaceKey,
5821
+ pageId: page.id,
5822
+ status: page.status,
5823
+ url: page._links?.webui ?? "",
5824
+ labels
5825
+ };
5826
+ if (parentPageId) {
5827
+ metadata.parentPageId = parentPageId;
5828
+ }
5829
+ if (condensed.method !== "passthrough") {
5830
+ metadata.condensed = condensed.method;
5831
+ metadata.originalLength = condensed.originalLength;
5832
+ }
2991
5833
  store.addNode({
2992
5834
  id: nodeId,
2993
5835
  type: "document",
2994
5836
  name: sanitizeExternalText(page.title, 500),
2995
- metadata: {
2996
- source: "confluence",
2997
- spaceKey,
2998
- pageId: page.id,
2999
- status: page.status,
3000
- url: page._links?.webui ?? ""
3001
- }
5837
+ content: condensed.content,
5838
+ metadata
3002
5839
  });
5840
+ if (parentPageId) {
5841
+ const parentNodeId = `confluence:${parentPageId}`;
5842
+ store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
5843
+ edgesAdded++;
5844
+ }
3003
5845
  const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
3004
- const edgesAdded = linkToCode(store, text, nodeId, "documents");
5846
+ edgesAdded += linkToCode(store, text, nodeId, "documents");
3005
5847
  return { nodesAdded: 1, edgesAdded };
3006
5848
  }
3007
5849
  };
@@ -3010,7 +5852,7 @@ var init_dist = __esm({
3010
5852
  source = "github-actions";
3011
5853
  httpClient;
3012
5854
  constructor(httpClient) {
3013
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5855
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
3014
5856
  }
3015
5857
  async ingest(store, config) {
3016
5858
  const start = Date.now();
@@ -3061,6 +5903,275 @@ var init_dist = __esm({
3061
5903
  return { nodesAdded, edgesAdded };
3062
5904
  }
3063
5905
  };
5906
+ CONSTRAINT_KEYWORDS = [
5907
+ "constraint",
5908
+ "must",
5909
+ "required",
5910
+ "minimum",
5911
+ "maximum",
5912
+ "spacing",
5913
+ "padding",
5914
+ "margin",
5915
+ "breakpoint",
5916
+ "responsive",
5917
+ "accessible",
5918
+ "a11y",
5919
+ "wcag"
5920
+ ];
5921
+ FigmaConnector = class {
5922
+ name = "figma";
5923
+ source = "figma";
5924
+ httpClient;
5925
+ constructor(httpClient) {
5926
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
5927
+ }
5928
+ async ingest(store, config) {
5929
+ const start = Date.now();
5930
+ const apiKeyEnv = config.apiKeyEnv ?? "FIGMA_API_KEY";
5931
+ const apiKey = process.env[apiKeyEnv];
5932
+ if (!apiKey) {
5933
+ return buildIngestResult2(
5934
+ 0,
5935
+ 0,
5936
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
5937
+ start
5938
+ );
5939
+ }
5940
+ const baseUrlEnv = config.baseUrlEnv ?? "FIGMA_BASE_URL";
5941
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.figma.com";
5942
+ try {
5943
+ const parsed = new URL(baseUrl);
5944
+ if (parsed.protocol !== "https:" || !parsed.hostname.endsWith("api.figma.com")) {
5945
+ return buildIngestResult2(
5946
+ 0,
5947
+ 0,
5948
+ [`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.figma.com`],
5949
+ start
5950
+ );
5951
+ }
5952
+ } catch {
5953
+ return buildIngestResult2(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
5954
+ }
5955
+ const fileIds = config.fileIds;
5956
+ if (!fileIds || fileIds.length === 0) {
5957
+ return buildIngestResult2(0, 0, ["No fileIds provided in connector config"], start);
5958
+ }
5959
+ const headers = { "X-FIGMA-TOKEN": apiKey };
5960
+ const maxLen = config.maxContentLength ?? 4e3;
5961
+ let nodesAdded = 0;
5962
+ let edgesAdded = 0;
5963
+ const errors = [];
5964
+ for (const fileId of fileIds) {
5965
+ try {
5966
+ const counts = await this.processFile(store, baseUrl, fileId, headers, maxLen);
5967
+ nodesAdded += counts.nodesAdded;
5968
+ edgesAdded += counts.edgesAdded;
5969
+ } catch (err) {
5970
+ errors.push(
5971
+ `Figma API error for file ${fileId}: ${err instanceof Error ? err.message : String(err)}`
5972
+ );
5973
+ }
5974
+ }
5975
+ return buildIngestResult2(nodesAdded, edgesAdded, errors, start);
5976
+ }
5977
+ async processFile(store, baseUrl, fileId, headers, maxLen) {
5978
+ let nodesAdded = 0;
5979
+ let edgesAdded = 0;
5980
+ const stylesUrl = `${baseUrl}/v1/files/${fileId}/styles`;
5981
+ const stylesResponse = await this.httpClient(stylesUrl, { headers });
5982
+ if (!stylesResponse.ok) throw new Error(`Styles request failed for file ${fileId}`);
5983
+ const stylesData = await stylesResponse.json();
5984
+ for (const style of stylesData.meta.styles) {
5985
+ const nodeId = `figma:token:${style.key}`;
5986
+ const condensed = await condenseContent(sanitizeExternalText(style.description || "", 2e3), {
5987
+ maxLength: maxLen
5988
+ });
5989
+ const metadata = {
5990
+ source: "figma",
5991
+ key: style.key,
5992
+ styleType: style.style_type,
5993
+ fileId
5994
+ };
5995
+ if (condensed.method !== "passthrough") {
5996
+ metadata.condensed = condensed.method;
5997
+ metadata.originalLength = condensed.originalLength;
5998
+ }
5999
+ store.addNode({
6000
+ id: nodeId,
6001
+ type: "design_token",
6002
+ name: sanitizeExternalText(style.name, 500),
6003
+ content: condensed.content,
6004
+ metadata
6005
+ });
6006
+ nodesAdded++;
6007
+ const searchText = sanitizeExternalText([style.name, style.description].join(" "));
6008
+ edgesAdded += linkToCode(store, searchText, nodeId, "references");
6009
+ }
6010
+ const componentsUrl = `${baseUrl}/v1/files/${fileId}/components`;
6011
+ const componentsResponse = await this.httpClient(componentsUrl, { headers });
6012
+ if (!componentsResponse.ok) throw new Error(`Components request failed for file ${fileId}`);
6013
+ const componentsData = await componentsResponse.json();
6014
+ for (const component of componentsData.meta.components) {
6015
+ const description = component.description || "";
6016
+ if (description) {
6017
+ const intentId = `figma:intent:${component.key}`;
6018
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6019
+ maxLength: maxLen
6020
+ });
6021
+ const metadata = {
6022
+ source: "figma",
6023
+ key: component.key,
6024
+ fileId
6025
+ };
6026
+ if (condensed.method !== "passthrough") {
6027
+ metadata.condensed = condensed.method;
6028
+ metadata.originalLength = condensed.originalLength;
6029
+ }
6030
+ store.addNode({
6031
+ id: intentId,
6032
+ type: "aesthetic_intent",
6033
+ name: sanitizeExternalText(component.name, 500),
6034
+ content: condensed.content,
6035
+ metadata
6036
+ });
6037
+ nodesAdded++;
6038
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6039
+ edgesAdded += linkToCode(store, searchText, intentId, "references");
6040
+ }
6041
+ if (description && hasConstraintKeyword(description)) {
6042
+ const constraintId = `figma:constraint:${component.key}`;
6043
+ const condensed = await condenseContent(sanitizeExternalText(description, 2e3), {
6044
+ maxLength: maxLen
6045
+ });
6046
+ const metadata = {
6047
+ source: "figma",
6048
+ key: component.key,
6049
+ fileId
6050
+ };
6051
+ if (condensed.method !== "passthrough") {
6052
+ metadata.condensed = condensed.method;
6053
+ metadata.originalLength = condensed.originalLength;
6054
+ }
6055
+ store.addNode({
6056
+ id: constraintId,
6057
+ type: "design_constraint",
6058
+ name: sanitizeExternalText(component.name, 500),
6059
+ content: condensed.content,
6060
+ metadata
6061
+ });
6062
+ nodesAdded++;
6063
+ const searchText = sanitizeExternalText([component.name, description].join(" "));
6064
+ edgesAdded += linkToCode(store, searchText, constraintId, "references");
6065
+ }
6066
+ }
6067
+ return { nodesAdded, edgesAdded };
6068
+ }
6069
+ };
6070
+ MIN_CONTENT_LENGTH = 10;
6071
+ CONCEPT_ITEM_TYPES = /* @__PURE__ */ new Set(["sticky_note", "text"]);
6072
+ MiroConnector = class {
6073
+ name = "miro";
6074
+ source = "miro";
6075
+ httpClient;
6076
+ constructor(httpClient) {
6077
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
6078
+ }
6079
+ async ingest(store, config) {
6080
+ const start = Date.now();
6081
+ const apiKeyEnv = config.apiKeyEnv ?? "MIRO_API_KEY";
6082
+ const apiKey = process.env[apiKeyEnv];
6083
+ if (!apiKey) {
6084
+ return buildIngestResult3(
6085
+ 0,
6086
+ 0,
6087
+ [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
6088
+ start
6089
+ );
6090
+ }
6091
+ const baseUrlEnv = config.baseUrlEnv ?? "MIRO_BASE_URL";
6092
+ const baseUrl = process.env[baseUrlEnv] ?? "https://api.miro.com";
6093
+ try {
6094
+ const parsed = new URL(baseUrl);
6095
+ if (parsed.protocol !== "https:" || parsed.hostname !== "api.miro.com") {
6096
+ return buildIngestResult3(
6097
+ 0,
6098
+ 0,
6099
+ [`Invalid ${baseUrlEnv}: must be an HTTPS URL on api.miro.com`],
6100
+ start
6101
+ );
6102
+ }
6103
+ } catch {
6104
+ return buildIngestResult3(0, 0, [`Invalid ${baseUrlEnv}: not a valid URL`], start);
6105
+ }
6106
+ const boardIds = config.boardIds;
6107
+ if (!boardIds || boardIds.length === 0) {
6108
+ return buildIngestResult3(0, 0, ["No boardIds provided in config"], start);
6109
+ }
6110
+ const headers = { Authorization: `Bearer ${apiKey}` };
6111
+ let totalNodesAdded = 0;
6112
+ let totalEdgesAdded = 0;
6113
+ const errors = [];
6114
+ for (const boardId of boardIds) {
6115
+ try {
6116
+ const counts = await this.processBoard(store, baseUrl, boardId, headers);
6117
+ totalNodesAdded += counts.nodesAdded;
6118
+ totalEdgesAdded += counts.edgesAdded;
6119
+ } catch (err) {
6120
+ errors.push(
6121
+ `Miro API error for board ${boardId}: ${err instanceof Error ? err.message : String(err)}`
6122
+ );
6123
+ }
6124
+ }
6125
+ return buildIngestResult3(totalNodesAdded, totalEdgesAdded, errors, start);
6126
+ }
6127
+ async processBoard(store, baseUrl, boardId, headers) {
6128
+ let nodesAdded = 0;
6129
+ let edgesAdded = 0;
6130
+ const boardResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}`, { headers });
6131
+ if (!boardResponse.ok) throw new Error(`Failed to fetch board ${boardId}`);
6132
+ const board = await boardResponse.json();
6133
+ const boardNodeId = `miro:board:${boardId}`;
6134
+ store.addNode({
6135
+ id: boardNodeId,
6136
+ type: "document",
6137
+ name: sanitizeExternalText(board.name, 500),
6138
+ content: sanitizeExternalText(board.description ?? "", 2e3),
6139
+ metadata: {
6140
+ source: "miro",
6141
+ boardId: board.id
6142
+ }
6143
+ });
6144
+ nodesAdded++;
6145
+ const itemsResponse = await this.httpClient(`${baseUrl}/v2/boards/${boardId}/items`, {
6146
+ headers
6147
+ });
6148
+ if (!itemsResponse.ok) throw new Error(`Failed to fetch items for board ${boardId}`);
6149
+ const itemsData = await itemsResponse.json();
6150
+ for (const item of itemsData.data) {
6151
+ const rawContent = item.data?.content ?? "";
6152
+ const plainContent = stripHtml(rawContent);
6153
+ if (plainContent.length < MIN_CONTENT_LENGTH) continue;
6154
+ if (!CONCEPT_ITEM_TYPES.has(item.type)) continue;
6155
+ const itemNodeId = `miro:item:${item.id}`;
6156
+ store.addNode({
6157
+ id: itemNodeId,
6158
+ type: "business_concept",
6159
+ name: sanitizeExternalText(plainContent, 500),
6160
+ content: sanitizeExternalText(plainContent, 2e3),
6161
+ metadata: {
6162
+ source: "miro",
6163
+ boardId: board.id,
6164
+ itemType: item.type
6165
+ }
6166
+ });
6167
+ nodesAdded++;
6168
+ store.addEdge({ from: boardNodeId, to: itemNodeId, type: "contains" });
6169
+ edgesAdded++;
6170
+ edgesAdded += linkToCode(store, plainContent, itemNodeId, "documents");
6171
+ }
6172
+ return { nodesAdded, edgesAdded };
6173
+ }
6174
+ };
3064
6175
  STOP_WORDS = /* @__PURE__ */ new Set([
3065
6176
  "the",
3066
6177
  "a",
@@ -4129,9 +7240,9 @@ var init_dist = __esm({
4129
7240
  extractPaths(trimmed, add) {
4130
7241
  const consumed = /* @__PURE__ */ new Set();
4131
7242
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
4132
- const path8 = match[0];
4133
- add(path8);
4134
- consumed.add(path8);
7243
+ const path13 = match[0];
7244
+ add(path13);
7245
+ consumed.add(path13);
4135
7246
  }
4136
7247
  return consumed;
4137
7248
  }
@@ -4201,8 +7312,8 @@ var init_dist = __esm({
4201
7312
  if (isPathLike && node.path.includes(raw)) {
4202
7313
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
4203
7314
  }
4204
- const basename6 = node.path.split("/").pop() ?? "";
4205
- if (basename6.includes(raw)) {
7315
+ const basename7 = node.path.split("/").pop() ?? "";
7316
+ if (basename7.includes(raw)) {
4206
7317
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
4207
7318
  }
4208
7319
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -4279,13 +7390,13 @@ var init_dist = __esm({
4279
7390
  const context = Array.isArray(d?.context) ? d.context : [];
4280
7391
  const firstEntity = entities[0];
4281
7392
  const nodeType = firstEntity?.node.type ?? "node";
4282
- const path8 = firstEntity?.node.path ?? "unknown";
7393
+ const path13 = firstEntity?.node.path ?? "unknown";
4283
7394
  let neighborCount = 0;
4284
7395
  const firstContext = context[0];
4285
7396
  if (firstContext && Array.isArray(firstContext.nodes)) {
4286
7397
  neighborCount = firstContext.nodes.length;
4287
7398
  }
4288
- return `**${entityName}** is a ${nodeType} at \`${path8}\`. Connected to ${neighborCount} nodes.`;
7399
+ return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
4289
7400
  }
4290
7401
  formatAnomaly(data) {
4291
7402
  const d = data;
@@ -4816,8 +7927,8 @@ var init_dist = __esm({
4816
7927
  const { edges } = this.computeDependencyGraph();
4817
7928
  const violations = [];
4818
7929
  for (const edge of edges) {
4819
- const fromRelative = relative3(rootDir, edge.from).replaceAll("\\", "/");
4820
- const toRelative = relative3(rootDir, edge.to).replaceAll("\\", "/");
7930
+ const fromRelative = relative5(rootDir, edge.from).replaceAll("\\", "/");
7931
+ const toRelative = relative5(rootDir, edge.to).replaceAll("\\", "/");
4821
7932
  const fromLayer = this.resolveLayer(fromRelative, layers);
4822
7933
  const toLayer = this.resolveLayer(toRelative, layers);
4823
7934
  if (!fromLayer || !toLayer) continue;
@@ -4910,8 +8021,8 @@ var init_dist = __esm({
4910
8021
  async ingestAll(designDir) {
4911
8022
  const start = Date.now();
4912
8023
  const [tokensResult, intentResult] = await Promise.all([
4913
- this.ingestTokens(path7.join(designDir, "tokens.json")),
4914
- this.ingestDesignIntent(path7.join(designDir, "DESIGN.md"))
8024
+ this.ingestTokens(path12.join(designDir, "tokens.json")),
8025
+ this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
4915
8026
  ]);
4916
8027
  const merged = mergeResults(tokensResult, intentResult);
4917
8028
  return { ...merged, durationMs: Date.now() - start };
@@ -5141,10 +8252,10 @@ var init_dist = __esm({
5141
8252
  includeTypes: ["file"]
5142
8253
  });
5143
8254
  for (const n of queryResult.nodes) {
5144
- const path8 = n.path ?? n.id.replace(/^file:/, "");
5145
- if (!fileSet.has(path8)) {
5146
- if (!result.has(path8)) {
5147
- result.set(path8, file);
8255
+ const path13 = n.path ?? n.id.replace(/^file:/, "");
8256
+ if (!fileSet.has(path13)) {
8257
+ if (!result.has(path13)) {
8258
+ result.set(path13, file);
5148
8259
  }
5149
8260
  }
5150
8261
  }
@@ -5545,12 +8656,32 @@ export {
5545
8656
  KnowledgeIngestor,
5546
8657
  BusinessKnowledgeIngestor,
5547
8658
  RequirementIngestor,
8659
+ MermaidParser,
8660
+ D2Parser,
8661
+ PlantUmlParser,
8662
+ DiagramParser,
8663
+ StructuralDriftDetector,
8664
+ KnowledgeStagingAggregator,
8665
+ detectLanguage,
8666
+ ExtractionRunner,
8667
+ TestDescriptionExtractor,
8668
+ EnumConstantExtractor,
8669
+ ValidationRuleExtractor,
8670
+ ApiPathExtractor,
8671
+ ALL_EXTRACTORS,
8672
+ createExtractionRunner,
8673
+ ImageAnalysisExtractor,
8674
+ ContradictionDetector,
8675
+ CoverageScorer,
8676
+ KnowledgePipelineRunner,
5548
8677
  linkToCode,
5549
8678
  SyncManager,
5550
8679
  JiraConnector,
5551
8680
  SlackConnector,
5552
8681
  ConfluenceConnector,
5553
8682
  CIConnector,
8683
+ FigmaConnector,
8684
+ MiroConnector,
5554
8685
  FusionLayer,
5555
8686
  GraphEntropyAdapter,
5556
8687
  GraphComplexityAdapter,