@harness-engineering/cli 1.25.7 → 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-QPO3H5HX.js → agents-md-OPOZA5L7.js} +3 -3
  11. package/dist/{architecture-IJLXRHZA.js → architecture-PMVMO6SF.js} +4 -4
  12. package/dist/{assess-project-QU6XIVJK.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-TNY64AOS.js → check-phase-gate-NTLAU3LO.js} +4 -4
  16. package/dist/{chunk-TQRCODRJ.js → chunk-4KSKV5MK.js} +1 -1
  17. package/dist/{chunk-IT2KORSK.js → chunk-6SCA4NEZ.js} +1 -1
  18. package/dist/{chunk-ZIYLIYN4.js → chunk-77EZGPSR.js} +1 -1
  19. package/dist/{chunk-D3XL2FLX.js → chunk-7JNZXJKY.js} +5 -5
  20. package/dist/{chunk-JJQ3TW2E.js → chunk-CAS3TS4Y.js} +101 -89
  21. package/dist/{chunk-K35D7HF3.js → chunk-EXTWTJRF.js} +1 -1
  22. package/dist/{chunk-AS5DQT4S.js → chunk-FI3D6D7U.js} +9 -9
  23. package/dist/{chunk-2DF6OZLG.js → chunk-G6SYTGOH.js} +2 -2
  24. package/dist/{chunk-KVJZQ2MV.js → chunk-HAOBHL3W.js} +1 -1
  25. package/dist/{chunk-MLOGYWLY.js → chunk-IFHAUFJX.js} +2 -2
  26. package/dist/{chunk-IQO7IINR.js → chunk-J3O64D2A.js} +1 -1
  27. package/dist/{chunk-LTCEV7VF.js → chunk-JBJ4AVE4.js} +3 -3
  28. package/dist/{chunk-FRDS4XGD.js → chunk-JWSVLC4I.js} +1 -1
  29. package/dist/{chunk-K533WNZG.js → chunk-JZ6GORAK.js} +1 -1
  30. package/dist/{chunk-YRBNAHH4.js → chunk-KFE2X7LG.js} +4 -4
  31. package/dist/{chunk-MCDCZMKN.js → chunk-KSFUJT5B.js} +1 -1
  32. package/dist/{chunk-LXPRO2FH.js → chunk-KXIAHHHP.js} +2 -2
  33. package/dist/{chunk-E5Z6NTTQ.js → chunk-NIOB6FTG.js} +1 -1
  34. package/dist/{chunk-PCOU2UXU.js → chunk-NVGQO4VM.js} +8 -8
  35. package/dist/{chunk-G5SNXUIK.js → chunk-QYSWNPXZ.js} +1 -1
  36. package/dist/{chunk-XVJRFP7W.js → chunk-RNAGPTKF.js} +6 -6
  37. package/dist/{chunk-GVNT2YC7.js → chunk-UBRD5FH4.js} +9 -9
  38. package/dist/{chunk-NKQEMWGT.js → chunk-VV2PJT4K.js} +3321 -207
  39. package/dist/{chunk-JPZY7ZQX.js → chunk-Z2JORLRT.js} +1 -1
  40. package/dist/{chunk-SZ3Q4XEO.js → chunk-ZBQ3ZY75.js} +433 -283
  41. package/dist/{ci-workflow-FJYRFZYQ.js → ci-workflow-H52EMCL6.js} +3 -3
  42. package/dist/{dist-HPWNWCT6.js → dist-PGQ32ZI7.js} +41 -1
  43. package/dist/{dist-ZV6GO6FA.js → dist-TG63OKA5.js} +2 -2
  44. package/dist/{docs-7DW3DI4Z.js → docs-GP4VUSIB.js} +4 -4
  45. package/dist/{engine-P7EE2IWO.js → engine-KQG5RVXX.js} +3 -3
  46. package/dist/{entropy-HCXRTDFU.js → entropy-RXESIYKC.js} +3 -3
  47. package/dist/{feedback-Y4FYN3NR.js → feedback-HS7ZYXOU.js} +1 -1
  48. package/dist/{generate-agent-definitions-3EHPIWBO.js → generate-agent-definitions-I4B7CVOB.js} +4 -4
  49. package/dist/{graph-loader-KXVS3YK3.js → graph-loader-TIVI6NQ7.js} +1 -1
  50. package/dist/index.d.ts +8 -8
  51. package/dist/index.js +25 -25
  52. package/dist/{loader-SKHF3JVV.js → loader-CWAH6B4H.js} +3 -3
  53. package/dist/{mcp-MWYIPDQU.js → mcp-KHK5KYER.js} +15 -15
  54. package/dist/{performance-ADWH5NJY.js → performance-WDW5WS3J.js} +4 -4
  55. package/dist/{review-pipeline-TLKE5OQD.js → review-pipeline-QOZGWCTY.js} +3 -3
  56. package/dist/{runtime-7LYAW7GU.js → runtime-SFEFRPLX.js} +3 -3
  57. package/dist/{scan-BGJ7TLLV.js → scan-IJ7EYTF7.js} +1 -1
  58. package/dist/{security-5XWINYVB.js → security-2K7UUK6T.js} +1 -1
  59. package/dist/{validate-BFFHYHY7.js → validate-HXM6DXRZ.js} +4 -4
  60. package/dist/{validate-cross-check-GQX7OVCR.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
  ];
@@ -2671,189 +2865,2745 @@ var init_dist = __esm({
2671
2865
  return count;
2672
2866
  }
2673
2867
  };
2674
- CODE_NODE_TYPES4 = ["file", "function", "class", "method", "interface", "variable"];
2675
- SANITIZE_RULES = [
2676
- // Strip XML/HTML-like instruction tags that could be interpreted as system prompts
2677
- {
2678
- pattern: /<\/?(?:system|instruction|prompt|role|context|tool_call|function_call|assistant|human|user)[^>]*>/gi,
2679
- replacement: ""
2680
- },
2681
- // Strip markdown-style system prompt markers (including trailing space)
2682
- {
2683
- pattern: /^#{1,3}\s*(?:system|instruction|prompt)\s*[::]\s*/gim,
2684
- replacement: ""
2685
- },
2686
- // Strip common injection prefixes
2687
- {
2688
- pattern: /(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|context)/gi,
2689
- replacement: "[filtered]"
2690
- },
2691
- // Strip "you are now" re-roling attempts (only when followed by AI/agent role words)
2692
- {
2693
- pattern: /you\s+are\s+now\s+(?:a\s+)?(?:helpful\s+)?(?:an?\s+)?(?:assistant|system|ai|bot|agent|tool)\b/gi,
2694
- replacement: "[filtered]"
2695
- }
2696
- ];
2697
- SyncManager = class {
2698
- constructor(store, graphDir) {
2699
- this.store = store;
2700
- this.metadataPath = path6.join(graphDir, "sync-metadata.json");
2868
+ MermaidParser = class {
2869
+ canParse(_content, ext) {
2870
+ return ext === ".mmd" || ext === ".mermaid";
2701
2871
  }
2702
- store;
2703
- registrations = /* @__PURE__ */ new Map();
2704
- metadataPath;
2705
- registerConnector(connector, config) {
2706
- this.registrations.set(connector.name, { connector, config });
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
+ }
2707
2886
  }
2708
- async sync(connectorName) {
2709
- const registration = this.registrations.get(connectorName);
2710
- if (!registration) {
2711
- return {
2712
- nodesAdded: 0,
2713
- nodesUpdated: 0,
2714
- edgesAdded: 0,
2715
- edgesUpdated: 0,
2716
- errors: [`Connector "${connectorName}" not registered`],
2717
- durationMs: 0
2718
- };
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;
2719
2897
  }
2720
- const { connector, config } = registration;
2721
- const result = await connector.ingest(this.store, config);
2722
- const metadata = await this.loadMetadata();
2723
- metadata.connectors[connectorName] = {
2724
- lastSyncTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
2725
- lastResult: result
2726
- };
2727
- await this.saveMetadata(metadata);
2728
- return result;
2898
+ return "unknown";
2729
2899
  }
2730
- async syncAll() {
2731
- const combined = {
2732
- nodesAdded: 0,
2733
- nodesUpdated: 0,
2734
- edgesAdded: 0,
2735
- edgesUpdated: 0,
2736
- errors: [],
2737
- durationMs: 0
2738
- };
2739
- for (const [name] of this.registrations) {
2740
- const result = await this.sync(name);
2741
- combined.nodesAdded += result.nodesAdded;
2742
- combined.nodesUpdated += result.nodesUpdated;
2743
- combined.edgesAdded += result.edgesAdded;
2744
- combined.edgesUpdated += result.edgesUpdated;
2745
- combined.errors.push(...result.errors);
2746
- combined.durationMs += result.durationMs;
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);
2747
2952
  }
2748
- return combined;
2953
+ return {
2954
+ entities: Array.from(entities.values()),
2955
+ relationships,
2956
+ metadata: { format: "mermaid", diagramType }
2957
+ };
2749
2958
  }
2750
- async getMetadata() {
2751
- return this.loadMetadata();
2959
+ isDecisionNode(content, nodeId) {
2960
+ const decisionRegex = new RegExp(`${nodeId}\\s*\\{`);
2961
+ return decisionRegex.test(content);
2752
2962
  }
2753
- async loadMetadata() {
2754
- try {
2755
- const raw = await fs5.readFile(this.metadataPath, "utf-8");
2756
- return JSON.parse(raw);
2757
- } catch {
2758
- return { connectors: {} };
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);
2759
3000
  }
2760
- }
2761
- async saveMetadata(metadata) {
2762
- await fs5.mkdir(path6.dirname(this.metadataPath), { recursive: true });
2763
- await fs5.writeFile(this.metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
3001
+ return {
3002
+ entities,
3003
+ relationships,
3004
+ metadata: { format: "mermaid", diagramType }
3005
+ };
2764
3006
  }
2765
3007
  };
2766
- JiraConnector = class {
2767
- name = "jira";
2768
- source = "jira";
2769
- httpClient;
2770
- constructor(httpClient) {
2771
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
3008
+ D2Parser = class {
3009
+ canParse(_content, ext) {
3010
+ return ext === ".d2";
2772
3011
  }
2773
- async ingest(store, config) {
2774
- const start = Date.now();
2775
- const apiKeyEnv = config.apiKeyEnv ?? "JIRA_API_KEY";
2776
- const apiKey = process.env[apiKeyEnv];
2777
- if (!apiKey) {
2778
- return buildIngestResult(
2779
- 0,
2780
- 0,
2781
- [`Missing API key: environment variable "${apiKeyEnv}" is not set`],
2782
- start
2783
- );
2784
- }
2785
- const baseUrlEnv = config.baseUrlEnv ?? "JIRA_BASE_URL";
2786
- const baseUrl = process.env[baseUrlEnv];
2787
- if (!baseUrl) {
2788
- return buildIngestResult(
2789
- 0,
2790
- 0,
2791
- [`Missing base URL: environment variable "${baseUrlEnv}" is not set`],
2792
- start
2793
- );
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
+ };
2794
3020
  }
2795
- const jql = buildJql(config);
2796
- const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
2797
- try {
2798
- const counts = await this.fetchAllIssues(store, baseUrl, jql, headers);
2799
- return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
2800
- } catch (err) {
2801
- return buildIngestResult(
2802
- 0,
2803
- 0,
2804
- [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
2805
- start
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*(.+?))?$/
2806
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
+ }
2807
3066
  }
3067
+ return {
3068
+ entities: Array.from(entities.values()),
3069
+ relationships,
3070
+ metadata: { format: "d2", diagramType: "architecture" }
3071
+ };
2808
3072
  }
2809
- async fetchAllIssues(store, baseUrl, jql, headers) {
2810
- let nodesAdded = 0;
2811
- let edgesAdded = 0;
2812
- let startAt = 0;
2813
- const maxResults = 50;
2814
- let total = Infinity;
2815
- while (startAt < total) {
2816
- const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
2817
- const response = await this.httpClient(url, { headers });
2818
- if (!response.ok) throw new Error("Jira API request failed");
2819
- const data = await response.json();
2820
- total = data.total;
2821
- for (const issue of data.issues) {
2822
- const counts = this.processIssue(store, issue);
2823
- nodesAdded += counts.nodesAdded;
2824
- edgesAdded += counts.edgesAdded;
3073
+ };
3074
+ PlantUmlParser = class {
3075
+ canParse(_content, ext) {
3076
+ return ext === ".puml" || ext === ".plantuml";
3077
+ }
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
+ }
2825
3110
  }
2826
- startAt += maxResults;
3111
+ match = componentRegex1.exec(body);
2827
3112
  }
2828
- return { nodesAdded, edgesAdded };
2829
- }
2830
- processIssue(store, issue) {
2831
- const nodeId = `issue:jira:${issue.key}`;
2832
- store.addNode({
2833
- id: nodeId,
2834
- type: "issue",
2835
- name: sanitizeExternalText(issue.fields.summary, 500),
2836
- metadata: {
2837
- key: issue.key,
2838
- status: issue.fields.status?.name,
2839
- priority: issue.fields.priority?.name,
2840
- assignee: issue.fields.assignee?.displayName,
2841
- labels: issue.fields.labels ?? []
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 } : {} });
2842
3121
  }
2843
- });
2844
- const searchText = sanitizeExternalText(
2845
- [issue.fields.summary, issue.fields.description ?? ""].join(" ")
2846
- );
2847
- const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
2848
- return { nodesAdded: 1, edgesAdded };
3122
+ match = relRegex.exec(body);
3123
+ }
3124
+ return {
3125
+ entities: Array.from(entities.values()),
3126
+ relationships,
3127
+ metadata: { format: "plantuml", diagramType }
3128
+ };
3129
+ }
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";
2849
3135
  }
2850
3136
  };
2851
- SlackConnector = class {
2852
- name = "slack";
2853
- source = "slack";
2854
- httpClient;
3137
+ DIAGRAM_EXTENSIONS = /* @__PURE__ */ new Set([".mmd", ".mermaid", ".d2", ".puml", ".plantuml"]);
3138
+ DiagramParser = class {
3139
+ constructor(store) {
3140
+ this.store = store;
3141
+ }
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
+ }
3154
+ }
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
+ );
5505
+ }
5506
+ const jql = buildJql(config);
5507
+ const headers = { Authorization: `Basic ${apiKey}`, "Content-Type": "application/json" };
5508
+ try {
5509
+ const counts = await this.fetchAllIssues(store, baseUrl, jql, headers, config);
5510
+ return buildIngestResult(counts.nodesAdded, counts.edgesAdded, [], start);
5511
+ } catch (err) {
5512
+ return buildIngestResult(
5513
+ 0,
5514
+ 0,
5515
+ [`Jira API error: ${err instanceof Error ? err.message : String(err)}`],
5516
+ start
5517
+ );
5518
+ }
5519
+ }
5520
+ async fetchAllIssues(store, baseUrl, jql, headers, config) {
5521
+ let nodesAdded = 0;
5522
+ let edgesAdded = 0;
5523
+ let startAt = 0;
5524
+ const maxResults = 50;
5525
+ let total = Infinity;
5526
+ while (startAt < total) {
5527
+ const url = `${baseUrl}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}`;
5528
+ const response = await this.httpClient(url, { headers });
5529
+ if (!response.ok) throw new Error("Jira API request failed");
5530
+ const data = await response.json();
5531
+ total = data.total;
5532
+ for (const issue of data.issues) {
5533
+ const counts = await this.processIssue(store, issue, baseUrl, headers, config);
5534
+ nodesAdded += counts.nodesAdded;
5535
+ edgesAdded += counts.edgesAdded;
5536
+ }
5537
+ startAt += maxResults;
5538
+ }
5539
+ return { nodesAdded, edgesAdded };
5540
+ }
5541
+ async processIssue(store, issue, baseUrl, headers, config) {
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
+ }
5576
+ store.addNode({
5577
+ id: nodeId,
5578
+ type: "issue",
5579
+ name: sanitizeExternalText(issue.fields.summary, 500),
5580
+ content: condensed.content,
5581
+ metadata
5582
+ });
5583
+ const searchText = sanitizeExternalText(
5584
+ [issue.fields.summary, issue.fields.description ?? ""].join(" ")
5585
+ );
5586
+ const edgesAdded = linkToCode(store, searchText, nodeId, "applies_to");
5587
+ return { nodesAdded: 1, edgesAdded };
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
+ }
5600
+ };
5601
+ SlackConnector = class {
5602
+ name = "slack";
5603
+ source = "slack";
5604
+ httpClient;
2855
5605
  constructor(httpClient) {
2856
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5606
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2857
5607
  }
2858
5608
  async ingest(store, config) {
2859
5609
  const start = Date.now();
@@ -2876,7 +5626,7 @@ var init_dist = __esm({
2876
5626
  const oldest = config.lookbackDays ? String(Math.floor((Date.now() - Number(config.lookbackDays) * 864e5) / 1e3)) : void 0;
2877
5627
  for (const channel of channels) {
2878
5628
  try {
2879
- const result = await this.processChannel(store, channel, apiKey, oldest);
5629
+ const result = await this.processChannel(store, channel, apiKey, oldest, config);
2880
5630
  nodesAdded += result.nodesAdded;
2881
5631
  edgesAdded += result.edgesAdded;
2882
5632
  errors.push(...result.errors);
@@ -2895,7 +5645,7 @@ var init_dist = __esm({
2895
5645
  durationMs: Date.now() - start
2896
5646
  };
2897
5647
  }
2898
- async processChannel(store, channel, apiKey, oldest) {
5648
+ async processChannel(store, channel, apiKey, oldest, config) {
2899
5649
  const errors = [];
2900
5650
  let nodesAdded = 0;
2901
5651
  let edgesAdded = 0;
@@ -2920,34 +5670,81 @@ var init_dist = __esm({
2920
5670
  if (!data.ok) {
2921
5671
  return { nodesAdded: 0, edgesAdded: 0, errors: [`Slack API error for channel ${channel}`] };
2922
5672
  }
5673
+ const maxLen = config.maxContentLength ?? 2e3;
2923
5674
  for (const message of data.messages) {
2924
5675
  const nodeId = `conversation:slack:${channel}:${message.ts}`;
2925
- const sanitizedText = sanitizeExternalText(message.text);
2926
- 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
+ }
2927
5711
  store.addNode({
2928
5712
  id: nodeId,
2929
5713
  type: "conversation",
2930
5714
  name: snippet,
2931
- metadata: {
2932
- author: message.user,
2933
- channel,
2934
- timestamp: message.ts
2935
- }
5715
+ content: condensed.content,
5716
+ metadata
2936
5717
  });
2937
5718
  nodesAdded++;
2938
- edgesAdded += linkToCode(store, sanitizedText, nodeId, "references", {
5719
+ edgesAdded += linkToCode(store, condensed.content, nodeId, "references", {
2939
5720
  checkPaths: true
2940
5721
  });
2941
5722
  }
2942
5723
  return { nodesAdded, edgesAdded, errors };
2943
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
+ }
2944
5741
  };
2945
5742
  ConfluenceConnector = class {
2946
5743
  name = "confluence";
2947
5744
  source = "confluence";
2948
5745
  httpClient;
2949
5746
  constructor(httpClient) {
2950
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5747
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
2951
5748
  }
2952
5749
  async ingest(store, config) {
2953
5750
  const start = Date.now();
@@ -2960,7 +5757,14 @@ var init_dist = __esm({
2960
5757
  const baseUrlEnv = config.baseUrlEnv ?? "CONFLUENCE_BASE_URL";
2961
5758
  const baseUrl = process.env[baseUrlEnv] ?? "";
2962
5759
  const spaceKey = config.spaceKey ?? "";
2963
- 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
+ );
2964
5768
  return {
2965
5769
  nodesAdded: counts.nodesAdded,
2966
5770
  nodesUpdated: 0,
@@ -2970,9 +5774,9 @@ var init_dist = __esm({
2970
5774
  durationMs: Date.now() - start
2971
5775
  };
2972
5776
  }
2973
- async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors) {
5777
+ async fetchAllPagesHandled(store, baseUrl, apiKey, spaceKey, errors, config) {
2974
5778
  try {
2975
- const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey);
5779
+ const result = await this.fetchAllPages(store, baseUrl, apiKey, spaceKey, config);
2976
5780
  errors.push(...result.errors);
2977
5781
  return { nodesAdded: result.nodesAdded, edgesAdded: result.edgesAdded };
2978
5782
  } catch (err) {
@@ -2980,7 +5784,7 @@ var init_dist = __esm({
2980
5784
  return { nodesAdded: 0, edgesAdded: 0 };
2981
5785
  }
2982
5786
  }
2983
- async fetchAllPages(store, baseUrl, apiKey, spaceKey) {
5787
+ async fetchAllPages(store, baseUrl, apiKey, spaceKey, config) {
2984
5788
  const errors = [];
2985
5789
  let nodesAdded = 0;
2986
5790
  let edgesAdded = 0;
@@ -2995,7 +5799,7 @@ var init_dist = __esm({
2995
5799
  }
2996
5800
  const data = await response.json();
2997
5801
  for (const page of data.results) {
2998
- const counts = this.processPage(store, page, spaceKey);
5802
+ const counts = await this.processPage(store, page, spaceKey, config);
2999
5803
  nodesAdded += counts.nodesAdded;
3000
5804
  edgesAdded += counts.edgesAdded;
3001
5805
  }
@@ -3003,22 +5807,43 @@ var init_dist = __esm({
3003
5807
  }
3004
5808
  return { nodesAdded, edgesAdded, errors };
3005
5809
  }
3006
- processPage(store, page, spaceKey) {
5810
+ async processPage(store, page, spaceKey, config) {
3007
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
+ }
3008
5833
  store.addNode({
3009
5834
  id: nodeId,
3010
5835
  type: "document",
3011
5836
  name: sanitizeExternalText(page.title, 500),
3012
- metadata: {
3013
- source: "confluence",
3014
- spaceKey,
3015
- pageId: page.id,
3016
- status: page.status,
3017
- url: page._links?.webui ?? ""
3018
- }
5837
+ content: condensed.content,
5838
+ metadata
3019
5839
  });
5840
+ if (parentPageId) {
5841
+ const parentNodeId = `confluence:${parentPageId}`;
5842
+ store.addEdge({ from: parentNodeId, to: nodeId, type: "contains" });
5843
+ edgesAdded++;
5844
+ }
3020
5845
  const text = sanitizeExternalText(`${page.title} ${page.body?.storage?.value ?? ""}`);
3021
- const edgesAdded = linkToCode(store, text, nodeId, "documents");
5846
+ edgesAdded += linkToCode(store, text, nodeId, "documents");
3022
5847
  return { nodesAdded: 1, edgesAdded };
3023
5848
  }
3024
5849
  };
@@ -3027,7 +5852,7 @@ var init_dist = __esm({
3027
5852
  source = "github-actions";
3028
5853
  httpClient;
3029
5854
  constructor(httpClient) {
3030
- this.httpClient = httpClient ?? ((url, options) => fetch(url, options));
5855
+ this.httpClient = httpClient ?? withRetry((url, options) => fetch(url, options));
3031
5856
  }
3032
5857
  async ingest(store, config) {
3033
5858
  const start = Date.now();
@@ -3078,6 +5903,275 @@ var init_dist = __esm({
3078
5903
  return { nodesAdded, edgesAdded };
3079
5904
  }
3080
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
+ };
3081
6175
  STOP_WORDS = /* @__PURE__ */ new Set([
3082
6176
  "the",
3083
6177
  "a",
@@ -4146,9 +7240,9 @@ var init_dist = __esm({
4146
7240
  extractPaths(trimmed, add) {
4147
7241
  const consumed = /* @__PURE__ */ new Set();
4148
7242
  for (const match of trimmed.matchAll(FILE_PATH_RE)) {
4149
- const path8 = match[0];
4150
- add(path8);
4151
- consumed.add(path8);
7243
+ const path13 = match[0];
7244
+ add(path13);
7245
+ consumed.add(path13);
4152
7246
  }
4153
7247
  return consumed;
4154
7248
  }
@@ -4218,8 +7312,8 @@ var init_dist = __esm({
4218
7312
  if (isPathLike && node.path.includes(raw)) {
4219
7313
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
4220
7314
  }
4221
- const basename6 = node.path.split("/").pop() ?? "";
4222
- if (basename6.includes(raw)) {
7315
+ const basename7 = node.path.split("/").pop() ?? "";
7316
+ if (basename7.includes(raw)) {
4223
7317
  return { raw, nodeId: node.id, node, confidence: 0.6, method: "path" };
4224
7318
  }
4225
7319
  if (raw.length >= 4 && node.path.includes(raw)) {
@@ -4296,13 +7390,13 @@ var init_dist = __esm({
4296
7390
  const context = Array.isArray(d?.context) ? d.context : [];
4297
7391
  const firstEntity = entities[0];
4298
7392
  const nodeType = firstEntity?.node.type ?? "node";
4299
- const path8 = firstEntity?.node.path ?? "unknown";
7393
+ const path13 = firstEntity?.node.path ?? "unknown";
4300
7394
  let neighborCount = 0;
4301
7395
  const firstContext = context[0];
4302
7396
  if (firstContext && Array.isArray(firstContext.nodes)) {
4303
7397
  neighborCount = firstContext.nodes.length;
4304
7398
  }
4305
- return `**${entityName}** is a ${nodeType} at \`${path8}\`. Connected to ${neighborCount} nodes.`;
7399
+ return `**${entityName}** is a ${nodeType} at \`${path13}\`. Connected to ${neighborCount} nodes.`;
4306
7400
  }
4307
7401
  formatAnomaly(data) {
4308
7402
  const d = data;
@@ -4833,8 +7927,8 @@ var init_dist = __esm({
4833
7927
  const { edges } = this.computeDependencyGraph();
4834
7928
  const violations = [];
4835
7929
  for (const edge of edges) {
4836
- const fromRelative = relative3(rootDir, edge.from).replaceAll("\\", "/");
4837
- const toRelative = relative3(rootDir, edge.to).replaceAll("\\", "/");
7930
+ const fromRelative = relative5(rootDir, edge.from).replaceAll("\\", "/");
7931
+ const toRelative = relative5(rootDir, edge.to).replaceAll("\\", "/");
4838
7932
  const fromLayer = this.resolveLayer(fromRelative, layers);
4839
7933
  const toLayer = this.resolveLayer(toRelative, layers);
4840
7934
  if (!fromLayer || !toLayer) continue;
@@ -4927,8 +8021,8 @@ var init_dist = __esm({
4927
8021
  async ingestAll(designDir) {
4928
8022
  const start = Date.now();
4929
8023
  const [tokensResult, intentResult] = await Promise.all([
4930
- this.ingestTokens(path7.join(designDir, "tokens.json")),
4931
- this.ingestDesignIntent(path7.join(designDir, "DESIGN.md"))
8024
+ this.ingestTokens(path12.join(designDir, "tokens.json")),
8025
+ this.ingestDesignIntent(path12.join(designDir, "DESIGN.md"))
4932
8026
  ]);
4933
8027
  const merged = mergeResults(tokensResult, intentResult);
4934
8028
  return { ...merged, durationMs: Date.now() - start };
@@ -5158,10 +8252,10 @@ var init_dist = __esm({
5158
8252
  includeTypes: ["file"]
5159
8253
  });
5160
8254
  for (const n of queryResult.nodes) {
5161
- const path8 = n.path ?? n.id.replace(/^file:/, "");
5162
- if (!fileSet.has(path8)) {
5163
- if (!result.has(path8)) {
5164
- result.set(path8, file);
8255
+ const path13 = n.path ?? n.id.replace(/^file:/, "");
8256
+ if (!fileSet.has(path13)) {
8257
+ if (!result.has(path13)) {
8258
+ result.set(path13, file);
5165
8259
  }
5166
8260
  }
5167
8261
  }
@@ -5562,12 +8656,32 @@ export {
5562
8656
  KnowledgeIngestor,
5563
8657
  BusinessKnowledgeIngestor,
5564
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,
5565
8677
  linkToCode,
5566
8678
  SyncManager,
5567
8679
  JiraConnector,
5568
8680
  SlackConnector,
5569
8681
  ConfluenceConnector,
5570
8682
  CIConnector,
8683
+ FigmaConnector,
8684
+ MiroConnector,
5571
8685
  FusionLayer,
5572
8686
  GraphEntropyAdapter,
5573
8687
  GraphComplexityAdapter,