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