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