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