@danielblomma/cortex-mcp 2.2.5 → 2.4.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/CHANGELOG.md +201 -0
- package/README.md +73 -3
- package/bin/cortex.mjs +339 -242
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +336 -212
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/query.ts +36 -2
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/embed.ts +211 -10
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/index.ts +1 -1
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
- package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/paths.ts +3 -5
- package/scaffold/mcp/src/patternEvidence.ts +347 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/src/search.ts +35 -9
- package/scaffold/mcp/src/searchCore.ts +254 -12
- package/scaffold/mcp/src/searchResults.ts +96 -9
- package/scaffold/mcp/src/types.ts +7 -0
- package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/paths.test.mjs +11 -3
- package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/scripts/dashboard.mjs +29 -18
- package/scaffold/scripts/doctor.sh +26 -8
- package/scaffold/scripts/ingest.mjs +37 -4
- package/scaffold/scripts/status.sh +13 -6
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { ensureDaemon, parseInput, readStdin, resolveDaemonEntry, sendHeartbeat, } from "./shared.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const raw = await readStdin();
|
|
5
|
+
const input = parseInput(raw);
|
|
6
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
8
|
+
const promptLen = typeof input.prompt === "string" ? input.prompt.length : 0;
|
|
9
|
+
const payload = {
|
|
10
|
+
cwd,
|
|
11
|
+
entry: {
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
tool: "user.prompt",
|
|
14
|
+
input: { prompt_length: promptLen },
|
|
15
|
+
event_type: "session",
|
|
16
|
+
evidence_level: "diagnostic",
|
|
17
|
+
resource_type: "user_input",
|
|
18
|
+
session_id: input.session_id,
|
|
19
|
+
metadata: { prompt_length: promptLen },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
await call("audit.log", payload, { timeoutMs: 3000 });
|
|
23
|
+
if (input.session_id) {
|
|
24
|
+
await sendHeartbeat({
|
|
25
|
+
cli: "claude",
|
|
26
|
+
hook: "UserPromptSubmit",
|
|
27
|
+
session_id: input.session_id,
|
|
28
|
+
cwd,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
function impactEntityLabel(entityId, catalog, searchEntities) {
|
|
2
|
+
const searchEntity = searchEntities.get(entityId);
|
|
3
|
+
if (searchEntity?.label) {
|
|
4
|
+
return searchEntity.label;
|
|
5
|
+
}
|
|
6
|
+
const catalogEntry = catalog.get(entityId);
|
|
7
|
+
return String(catalogEntry?.label ?? entityId);
|
|
8
|
+
}
|
|
9
|
+
export function buildImpactPath(targetId, seedId, visited, catalog, searchEntities) {
|
|
10
|
+
const entities = [targetId];
|
|
11
|
+
const edges = [];
|
|
12
|
+
let currentId = targetId;
|
|
13
|
+
while (currentId !== seedId) {
|
|
14
|
+
const metadata = visited.get(currentId);
|
|
15
|
+
if (!metadata) {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
const from = metadata.direction === "outgoing" ? metadata.via_entity : currentId;
|
|
19
|
+
const to = metadata.direction === "outgoing" ? currentId : metadata.via_entity;
|
|
20
|
+
edges.push({
|
|
21
|
+
from,
|
|
22
|
+
to,
|
|
23
|
+
relation: metadata.via_relation,
|
|
24
|
+
note: metadata.via_note ?? ""
|
|
25
|
+
});
|
|
26
|
+
entities.push(metadata.via_entity);
|
|
27
|
+
currentId = metadata.via_entity;
|
|
28
|
+
}
|
|
29
|
+
entities.reverse();
|
|
30
|
+
edges.reverse();
|
|
31
|
+
const labels = entities.map((entityId) => impactEntityLabel(entityId, catalog, searchEntities));
|
|
32
|
+
const summaryParts = [];
|
|
33
|
+
for (let index = 0; index < labels.length; index += 1) {
|
|
34
|
+
summaryParts.push(labels[index]);
|
|
35
|
+
if (index < edges.length) {
|
|
36
|
+
const edge = edges[index];
|
|
37
|
+
const note = edge.note ? `(${String(edge.note)})` : "";
|
|
38
|
+
summaryParts.push(`-[${String(edge.relation)}${note}]->`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
summary: summaryParts.join(" "),
|
|
43
|
+
entities,
|
|
44
|
+
edges
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function buildCompactImpactSummary(entities, edges, catalog, searchEntities, maxPathHopsShown) {
|
|
48
|
+
const labels = entities.map((entityId) => impactEntityLabel(entityId, catalog, searchEntities));
|
|
49
|
+
if (labels.length <= 3 || edges.length <= maxPathHopsShown) {
|
|
50
|
+
const summaryParts = [];
|
|
51
|
+
for (let index = 0; index < labels.length; index += 1) {
|
|
52
|
+
summaryParts.push(labels[index]);
|
|
53
|
+
if (index < edges.length) {
|
|
54
|
+
const edge = edges[index];
|
|
55
|
+
const note = edge.note ? `(${String(edge.note)})` : "";
|
|
56
|
+
summaryParts.push(`-[${String(edge.relation)}${note}]->`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return summaryParts.join(" ");
|
|
60
|
+
}
|
|
61
|
+
const headEdgeCount = Math.max(1, Math.ceil(maxPathHopsShown / 2));
|
|
62
|
+
const tailEdgeCount = Math.max(0, Math.floor(maxPathHopsShown / 2));
|
|
63
|
+
const hiddenHopCount = Math.max(0, edges.length - headEdgeCount - tailEdgeCount);
|
|
64
|
+
const hiddenText = hiddenHopCount === 1 ? "1 more hop" : `${hiddenHopCount} more hops`;
|
|
65
|
+
const summaryParts = [labels[0]];
|
|
66
|
+
for (let index = 0; index < headEdgeCount; index += 1) {
|
|
67
|
+
const edge = edges[index];
|
|
68
|
+
const note = edge.note ? `(${String(edge.note)})` : "";
|
|
69
|
+
summaryParts.push(`-[${String(edge.relation)}${note}]->`);
|
|
70
|
+
summaryParts.push(labels[index + 1]);
|
|
71
|
+
}
|
|
72
|
+
summaryParts.push(`... ${hiddenText} ...`);
|
|
73
|
+
const tailStart = edges.length - tailEdgeCount;
|
|
74
|
+
const tailEntityStart = labels.length - tailEdgeCount - 1;
|
|
75
|
+
for (let index = tailStart; index < edges.length; index += 1) {
|
|
76
|
+
const edge = edges[index];
|
|
77
|
+
const note = edge.note ? `(${String(edge.note)})` : "";
|
|
78
|
+
const labelIndex = tailEntityStart + (index - tailStart);
|
|
79
|
+
summaryParts.push(`-[${String(edge.relation)}${note}]->`);
|
|
80
|
+
summaryParts.push(labels[labelIndex + 1]);
|
|
81
|
+
}
|
|
82
|
+
return summaryParts.join(" ");
|
|
83
|
+
}
|
|
84
|
+
function formatImpactRelationLabel(relation) {
|
|
85
|
+
return relation.toLowerCase().replaceAll("_", " ");
|
|
86
|
+
}
|
|
87
|
+
function formatImpactRelationWithNote(edge) {
|
|
88
|
+
const relationLabel = formatImpactRelationLabel(String(edge.relation ?? ""));
|
|
89
|
+
const note = String(edge.note ?? "").trim();
|
|
90
|
+
if (!note) {
|
|
91
|
+
return relationLabel;
|
|
92
|
+
}
|
|
93
|
+
return `${relationLabel} (${note})`;
|
|
94
|
+
}
|
|
95
|
+
export function buildImpactWhy(seedId, targetId, pathEdges, hops, catalog, searchEntities) {
|
|
96
|
+
const seedLabel = impactEntityLabel(seedId, catalog, searchEntities);
|
|
97
|
+
const targetLabel = impactEntityLabel(targetId, catalog, searchEntities);
|
|
98
|
+
const relationLabels = [...new Set(pathEdges.map((edge) => formatImpactRelationWithNote(edge)))];
|
|
99
|
+
if (relationLabels.length === 0) {
|
|
100
|
+
return `${targetLabel} is reachable from ${seedLabel}.`;
|
|
101
|
+
}
|
|
102
|
+
const relationText = relationLabels.length === 1
|
|
103
|
+
? relationLabels[0]
|
|
104
|
+
: `${relationLabels.slice(0, -1).join(", ")} and ${relationLabels[relationLabels.length - 1]}`;
|
|
105
|
+
const hopText = hops === 1 ? "1 hop" : `${hops} hops`;
|
|
106
|
+
return `${targetLabel} is impacted from ${seedLabel} via ${relationText} in ${hopText}.`;
|
|
107
|
+
}
|
|
108
|
+
export function buildImpactTopReasons(params) {
|
|
109
|
+
const reasons = [];
|
|
110
|
+
reasons.push(params.hops === 1 ? "1-hop path" : `${params.hops}-hop path`);
|
|
111
|
+
if (params.impactDomains.length > 0) {
|
|
112
|
+
reasons.push(`domains: ${params.impactDomains.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
if (params.noteScore > 0) {
|
|
115
|
+
const notedEdges = params.pathEdges
|
|
116
|
+
.map((edge) => ({
|
|
117
|
+
relation: formatImpactRelationLabel(String(edge.relation ?? "")),
|
|
118
|
+
note: String(edge.note ?? "").trim()
|
|
119
|
+
}))
|
|
120
|
+
.filter((edge) => edge.note.length > 0)
|
|
121
|
+
.slice(0, 2)
|
|
122
|
+
.map((edge) => `${edge.relation} (${edge.note})`);
|
|
123
|
+
if (notedEdges.length > 0) {
|
|
124
|
+
reasons.push(`note match: ${notedEdges.join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
reasons.push("note match");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (params.profileScore > 0) {
|
|
131
|
+
reasons.push(`profile boost: ${params.profile}`);
|
|
132
|
+
}
|
|
133
|
+
if (params.semanticScore > 0.15) {
|
|
134
|
+
reasons.push("entity text matched query");
|
|
135
|
+
}
|
|
136
|
+
return reasons.slice(0, 4);
|
|
137
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const SQL_ENTITY_KINDS = new Set(["procedure", "view", "function", "table", "trigger"]);
|
|
2
|
+
const SQL_LIKE_EXTENSIONS = [".sql"];
|
|
3
|
+
const CONFIG_LIKE_EXTENSIONS = [".config"];
|
|
4
|
+
const RESOURCE_LIKE_EXTENSIONS = [".resx"];
|
|
5
|
+
const SETTINGS_LIKE_EXTENSIONS = [".settings"];
|
|
6
|
+
const IMPACT_RELATION_TYPE_LIST = [
|
|
7
|
+
"CALLS",
|
|
8
|
+
"CALLS_SQL",
|
|
9
|
+
"IMPORTS",
|
|
10
|
+
"USES_CONFIG_KEY",
|
|
11
|
+
"USES_RESOURCE_KEY",
|
|
12
|
+
"USES_SETTING_KEY",
|
|
13
|
+
"USES_CONFIG",
|
|
14
|
+
"TRANSFORMS_CONFIG",
|
|
15
|
+
"PART_OF"
|
|
16
|
+
];
|
|
17
|
+
const IMPACT_PROFILE_RELATIONS = {
|
|
18
|
+
all: IMPACT_RELATION_TYPE_LIST,
|
|
19
|
+
config_only: ["USES_CONFIG_KEY", "USES_RESOURCE_KEY", "USES_SETTING_KEY", "USES_CONFIG", "TRANSFORMS_CONFIG", "PART_OF"],
|
|
20
|
+
config_to_sql: ["USES_CONFIG_KEY", "USES_RESOURCE_KEY", "USES_SETTING_KEY", "USES_CONFIG", "TRANSFORMS_CONFIG", "CALLS_SQL", "PART_OF"],
|
|
21
|
+
code_only: ["CALLS", "IMPORTS", "PART_OF"],
|
|
22
|
+
sql_only: ["CALLS_SQL", "PART_OF"]
|
|
23
|
+
};
|
|
24
|
+
function normalizeText(value) {
|
|
25
|
+
return value.normalize("NFKC").toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
function pathHasExtension(pathValue, extensions) {
|
|
28
|
+
const normalized = normalizeText(pathValue);
|
|
29
|
+
return extensions.some((extension) => normalized.endsWith(extension));
|
|
30
|
+
}
|
|
31
|
+
export function impactBaseScore(hops, graphScore, trustScore, semantic = 0) {
|
|
32
|
+
const hopScore = 1 / (1 + Math.max(0, hops));
|
|
33
|
+
const score = hopScore * 0.55 + graphScore * 0.2 + trustScore * 0.15 + semantic * 0.1;
|
|
34
|
+
return Number(score.toFixed(4));
|
|
35
|
+
}
|
|
36
|
+
export function resolveImpactRelationTypes(parsed) {
|
|
37
|
+
if (Array.isArray(parsed.relation_types) && parsed.relation_types.length > 0) {
|
|
38
|
+
return new Set(parsed.relation_types);
|
|
39
|
+
}
|
|
40
|
+
const profile = parsed.profile ?? "all";
|
|
41
|
+
return new Set(IMPACT_PROFILE_RELATIONS[profile]);
|
|
42
|
+
}
|
|
43
|
+
export function resolveImpactResultDomains(parsed) {
|
|
44
|
+
if (!Array.isArray(parsed.result_domains) || parsed.result_domains.length === 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return new Set(parsed.result_domains.map((domain) => normalizeText(domain)));
|
|
48
|
+
}
|
|
49
|
+
export function resolveImpactResultEntityTypes(parsed) {
|
|
50
|
+
if (!Array.isArray(parsed.result_entity_types) || parsed.result_entity_types.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return new Set(parsed.result_entity_types.map((entityType) => normalizeText(entityType)));
|
|
54
|
+
}
|
|
55
|
+
export function resolveImpactPathMustInclude(parsed) {
|
|
56
|
+
if (!Array.isArray(parsed.path_must_include) || parsed.path_must_include.length === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return new Set(parsed.path_must_include.map((relation) => normalizeText(relation)));
|
|
60
|
+
}
|
|
61
|
+
export function resolveImpactPathMustExclude(parsed) {
|
|
62
|
+
if (!Array.isArray(parsed.path_must_exclude) || parsed.path_must_exclude.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return new Set(parsed.path_must_exclude.map((relation) => normalizeText(relation)));
|
|
66
|
+
}
|
|
67
|
+
export function impactResultComparator(sortBy) {
|
|
68
|
+
return (a, b) => {
|
|
69
|
+
const aHops = Number(a.hops ?? Number.POSITIVE_INFINITY);
|
|
70
|
+
const bHops = Number(b.hops ?? Number.POSITIVE_INFINITY);
|
|
71
|
+
const aImpact = Number(a.impact_score ?? 0);
|
|
72
|
+
const bImpact = Number(b.impact_score ?? 0);
|
|
73
|
+
const aSemantic = Number(a.semantic_score ?? 0);
|
|
74
|
+
const bSemantic = Number(b.semantic_score ?? 0);
|
|
75
|
+
const aGraph = Number(a.graph_score ?? 0);
|
|
76
|
+
const bGraph = Number(b.graph_score ?? 0);
|
|
77
|
+
const aTrust = Number(a.trust_score ?? 0);
|
|
78
|
+
const bTrust = Number(b.trust_score ?? 0);
|
|
79
|
+
if (sortBy === "shortest_path") {
|
|
80
|
+
return aHops - bHops || bImpact - aImpact || bSemantic - aSemantic;
|
|
81
|
+
}
|
|
82
|
+
if (sortBy === "semantic_score") {
|
|
83
|
+
return bSemantic - aSemantic || bImpact - aImpact || aHops - bHops;
|
|
84
|
+
}
|
|
85
|
+
if (sortBy === "graph_score") {
|
|
86
|
+
return bGraph - aGraph || bImpact - aImpact || aHops - bHops;
|
|
87
|
+
}
|
|
88
|
+
if (sortBy === "trust_score") {
|
|
89
|
+
return bTrust - aTrust || bImpact - aImpact || aHops - bHops;
|
|
90
|
+
}
|
|
91
|
+
return bImpact - aImpact || aHops - bHops || bSemantic - aSemantic;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function impactDomainsForEntity(entity, catalogEntry) {
|
|
95
|
+
const domains = new Set();
|
|
96
|
+
const normalizedKind = normalizeText(entity?.kind ?? "");
|
|
97
|
+
const normalizedType = normalizeText(entity?.entity_type ?? String(catalogEntry?.type ?? ""));
|
|
98
|
+
const pathValue = String(entity?.path ?? catalogEntry?.path ?? "");
|
|
99
|
+
if (SQL_ENTITY_KINDS.has(normalizedKind) || pathHasExtension(pathValue, SQL_LIKE_EXTENSIONS)) {
|
|
100
|
+
domains.add("sql");
|
|
101
|
+
}
|
|
102
|
+
if (normalizedKind === "connection_string" ||
|
|
103
|
+
normalizedKind === "database_target" ||
|
|
104
|
+
normalizedKind === "app_setting" ||
|
|
105
|
+
pathHasExtension(pathValue, CONFIG_LIKE_EXTENSIONS)) {
|
|
106
|
+
domains.add("config");
|
|
107
|
+
}
|
|
108
|
+
if (normalizedKind === "resource_entry" || pathHasExtension(pathValue, RESOURCE_LIKE_EXTENSIONS)) {
|
|
109
|
+
domains.add("resource");
|
|
110
|
+
domains.add("config");
|
|
111
|
+
}
|
|
112
|
+
if (normalizedKind === "setting_entry" || pathHasExtension(pathValue, SETTINGS_LIKE_EXTENSIONS)) {
|
|
113
|
+
domains.add("settings");
|
|
114
|
+
domains.add("config");
|
|
115
|
+
}
|
|
116
|
+
if (normalizedType === "project") {
|
|
117
|
+
domains.add("project");
|
|
118
|
+
}
|
|
119
|
+
if (!domains.has("sql") &&
|
|
120
|
+
!domains.has("config") &&
|
|
121
|
+
!domains.has("resource") &&
|
|
122
|
+
!domains.has("settings") &&
|
|
123
|
+
(normalizedType === "file" || normalizedType === "chunk" || normalizedType === "module")) {
|
|
124
|
+
domains.add("code");
|
|
125
|
+
}
|
|
126
|
+
return [...domains];
|
|
127
|
+
}
|
|
128
|
+
export function impactProfileBoost(profile, domains, pathEdges) {
|
|
129
|
+
const relationTypes = new Set(pathEdges.map((edge) => String(edge.relation ?? "")));
|
|
130
|
+
const hasSqlPath = relationTypes.has("CALLS_SQL");
|
|
131
|
+
const hasConfigKeyPath = relationTypes.has("USES_CONFIG_KEY") ||
|
|
132
|
+
relationTypes.has("USES_RESOURCE_KEY") ||
|
|
133
|
+
relationTypes.has("USES_SETTING_KEY") ||
|
|
134
|
+
relationTypes.has("USES_CONFIG");
|
|
135
|
+
let boost = 0;
|
|
136
|
+
if (profile === "config_to_sql") {
|
|
137
|
+
if (domains.includes("sql")) {
|
|
138
|
+
boost += 0.18;
|
|
139
|
+
}
|
|
140
|
+
if (domains.includes("config")) {
|
|
141
|
+
boost += 0.04;
|
|
142
|
+
}
|
|
143
|
+
if (hasSqlPath) {
|
|
144
|
+
boost += 0.08;
|
|
145
|
+
}
|
|
146
|
+
if (hasConfigKeyPath && hasSqlPath) {
|
|
147
|
+
boost += 0.08;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (profile === "config_only") {
|
|
151
|
+
if (domains.includes("config")) {
|
|
152
|
+
boost += 0.08;
|
|
153
|
+
}
|
|
154
|
+
if (!hasSqlPath && !relationTypes.has("CALLS")) {
|
|
155
|
+
boost += 0.04;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (profile === "sql_only") {
|
|
159
|
+
if (domains.includes("sql")) {
|
|
160
|
+
boost += 0.14;
|
|
161
|
+
}
|
|
162
|
+
if (hasSqlPath) {
|
|
163
|
+
boost += 0.08;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (profile === "code_only") {
|
|
167
|
+
if (domains.includes("code")) {
|
|
168
|
+
boost += 0.08;
|
|
169
|
+
}
|
|
170
|
+
if (relationTypes.has("CALLS") || relationTypes.has("IMPORTS")) {
|
|
171
|
+
boost += 0.05;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (domains.includes("sql") && hasSqlPath) {
|
|
175
|
+
boost += 0.04;
|
|
176
|
+
}
|
|
177
|
+
return Number(boost.toFixed(4));
|
|
178
|
+
}
|
|
179
|
+
export function impactNoteScore(queryTokens, queryPhrase, pathEdges, semanticScorer) {
|
|
180
|
+
if (pathEdges.length === 0 || (queryTokens.length === 0 && !queryPhrase)) {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const noteText = pathEdges
|
|
184
|
+
.map((edge) => String(edge.note ?? "").trim())
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join("\n");
|
|
187
|
+
if (!noteText) {
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
return Number(semanticScorer(queryTokens, queryPhrase, noteText).toFixed(4));
|
|
191
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function buildImpactResponseMeta(params) {
|
|
2
|
+
return {
|
|
3
|
+
entity_id: params.parsed.entity_id,
|
|
4
|
+
query: params.parsed.query,
|
|
5
|
+
depth: params.parsed.depth,
|
|
6
|
+
top_k: params.parsed.top_k,
|
|
7
|
+
response_preset: params.responsePreset,
|
|
8
|
+
include_scores: params.includeScores,
|
|
9
|
+
include_reasons: params.includeReasons,
|
|
10
|
+
verbose_paths: params.verbosePaths,
|
|
11
|
+
max_path_hops_shown: params.maxPathHopsShown,
|
|
12
|
+
profile: params.profile,
|
|
13
|
+
sort_by: params.sortBy,
|
|
14
|
+
relation_types: [...params.allowedRelationTypes],
|
|
15
|
+
path_must_include: params.parsed.path_must_include ?? [],
|
|
16
|
+
path_must_exclude: params.parsed.path_must_exclude ?? [],
|
|
17
|
+
result_domains: params.parsed.result_domains ?? [],
|
|
18
|
+
result_entity_types: params.parsed.result_entity_types ?? [],
|
|
19
|
+
context_source: params.contextSource
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function buildEmptyImpactResponse(params) {
|
|
23
|
+
return {
|
|
24
|
+
...params.meta,
|
|
25
|
+
warning: params.warning,
|
|
26
|
+
seed: null,
|
|
27
|
+
results: [],
|
|
28
|
+
edges: []
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { buildCompactImpactSummary, buildImpactPath, buildImpactTopReasons, buildImpactWhy } from "./impactPresentation.js";
|
|
2
|
+
import { impactBaseScore, impactDomainsForEntity, impactNoteScore, impactProfileBoost, impactResultComparator } from "./impactRanking.js";
|
|
3
|
+
function normalizeText(value) {
|
|
4
|
+
return value.normalize("NFKC").toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
function matchesImpactFilters(result, resultDomains, resultEntityTypes, pathMustInclude, pathMustExclude) {
|
|
7
|
+
const pathRelationTypes = new Set((result.path_relation_types ?? []).map((relation) => normalizeText(String(relation))));
|
|
8
|
+
if (pathMustInclude && pathMustInclude.size > 0) {
|
|
9
|
+
for (const requiredRelation of pathMustInclude) {
|
|
10
|
+
if (!pathRelationTypes.has(requiredRelation)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (pathMustExclude && pathMustExclude.size > 0) {
|
|
16
|
+
for (const blockedRelation of pathMustExclude) {
|
|
17
|
+
if (pathRelationTypes.has(blockedRelation)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!resultDomains || resultDomains.size === 0) {
|
|
23
|
+
return !resultEntityTypes || resultEntityTypes.has(normalizeText(String(result.entity_type ?? "")));
|
|
24
|
+
}
|
|
25
|
+
const impactDomains = Array.isArray(result.impact_domains) ? result.impact_domains : [];
|
|
26
|
+
const matchesDomain = impactDomains.some((domain) => resultDomains.has(normalizeText(String(domain))));
|
|
27
|
+
if (!matchesDomain) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return !resultEntityTypes || resultEntityTypes.has(normalizeText(String(result.entity_type ?? "")));
|
|
31
|
+
}
|
|
32
|
+
export function buildImpactResults(params) {
|
|
33
|
+
return [...params.visited.entries()]
|
|
34
|
+
.filter(([id]) => id !== params.seedId)
|
|
35
|
+
.map(([id, metadata]) => {
|
|
36
|
+
const entity = params.searchEntities.get(id);
|
|
37
|
+
const catalogEntry = params.catalog.get(id) ?? {
|
|
38
|
+
id,
|
|
39
|
+
type: "Unknown",
|
|
40
|
+
label: id,
|
|
41
|
+
status: "unknown",
|
|
42
|
+
source_of_truth: false
|
|
43
|
+
};
|
|
44
|
+
const semantic = entity && params.hasQuery ? params.semanticScorer(params.queryTokens, params.queryPhrase, entity.text) : 0;
|
|
45
|
+
const graphScore = Math.min(1, (params.degreeByEntity.get(id) ?? 0) / 4);
|
|
46
|
+
const trustScore = entity ? Math.max(0, Math.min(1, entity.trust_level / 100)) : 0.5;
|
|
47
|
+
const impactPath = buildImpactPath(id, params.seedId, params.visited, params.catalog, params.searchEntities);
|
|
48
|
+
const impactDomains = impactDomainsForEntity(entity, catalogEntry);
|
|
49
|
+
const profileScore = impactProfileBoost(params.profile, impactDomains, impactPath.edges);
|
|
50
|
+
const noteScore = impactNoteScore(params.queryTokens, params.queryPhrase, impactPath.edges, params.semanticScorer);
|
|
51
|
+
const impactScore = Number((impactBaseScore(metadata.hops, graphScore, trustScore, semantic) + profileScore + noteScore * 0.12).toFixed(4));
|
|
52
|
+
const topReasons = buildImpactTopReasons({
|
|
53
|
+
hops: metadata.hops,
|
|
54
|
+
profile: params.profile,
|
|
55
|
+
semanticScore: semantic,
|
|
56
|
+
noteScore,
|
|
57
|
+
profileScore,
|
|
58
|
+
impactDomains,
|
|
59
|
+
pathEdges: impactPath.edges
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
entity_type: entity?.entity_type ?? String(catalogEntry.type ?? "Unknown"),
|
|
64
|
+
kind: entity?.kind ?? "",
|
|
65
|
+
title: entity?.label ?? String(catalogEntry.label ?? id),
|
|
66
|
+
path: entity?.path || catalogEntry.path || undefined,
|
|
67
|
+
hops: metadata.hops,
|
|
68
|
+
via_relation: metadata.via_relation,
|
|
69
|
+
direction: metadata.direction,
|
|
70
|
+
via_entity: metadata.via_entity,
|
|
71
|
+
impact_domains: impactDomains,
|
|
72
|
+
why: buildImpactWhy(params.seedId, id, impactPath.edges, metadata.hops, params.catalog, params.searchEntities),
|
|
73
|
+
path_summary: impactPath.summary,
|
|
74
|
+
path_summary_compact: buildCompactImpactSummary(impactPath.entities, impactPath.edges, params.catalog, params.searchEntities, params.maxPathHopsShown),
|
|
75
|
+
path_relation_types: impactPath.edges.map((edge) => String(edge.relation ?? "")),
|
|
76
|
+
excerpt: entity?.snippet ?? "",
|
|
77
|
+
status: entity?.status ?? String(catalogEntry.status ?? "unknown"),
|
|
78
|
+
source_of_truth: entity?.source_of_truth ?? Boolean(catalogEntry.source_of_truth),
|
|
79
|
+
...(params.includeReasons
|
|
80
|
+
? {
|
|
81
|
+
top_reasons: topReasons
|
|
82
|
+
}
|
|
83
|
+
: {}),
|
|
84
|
+
...(params.includeScores
|
|
85
|
+
? {
|
|
86
|
+
impact_score: impactScore,
|
|
87
|
+
profile_score: profileScore,
|
|
88
|
+
note_score: noteScore,
|
|
89
|
+
semantic_score: Number(semantic.toFixed(4)),
|
|
90
|
+
graph_score: Number(graphScore.toFixed(4)),
|
|
91
|
+
trust_score: Number(trustScore.toFixed(4))
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
94
|
+
...(params.verbosePaths
|
|
95
|
+
? {
|
|
96
|
+
path_entities: impactPath.entities,
|
|
97
|
+
path_edges: impactPath.edges
|
|
98
|
+
}
|
|
99
|
+
: {})
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.filter((result) => matchesImpactFilters(result, params.resultDomains, params.resultEntityTypes, params.pathMustInclude, params.pathMustExclude))
|
|
103
|
+
.sort(impactResultComparator(params.sortBy))
|
|
104
|
+
.slice(0, params.topK);
|
|
105
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function resolveImpactSeed(parsed, runSearch) {
|
|
2
|
+
if (parsed.entity_id) {
|
|
3
|
+
return { id: parsed.entity_id };
|
|
4
|
+
}
|
|
5
|
+
if (!parsed.query) {
|
|
6
|
+
return { id: null, warning: "Either entity_id or query is required." };
|
|
7
|
+
}
|
|
8
|
+
const searchPayload = await runSearch({
|
|
9
|
+
query: parsed.query,
|
|
10
|
+
top_k: Math.max(parsed.top_k, 5),
|
|
11
|
+
include_deprecated: false,
|
|
12
|
+
include_content: false
|
|
13
|
+
});
|
|
14
|
+
const rawResults = Array.isArray(searchPayload.results) ? searchPayload.results : [];
|
|
15
|
+
const firstResult = rawResults[0];
|
|
16
|
+
return {
|
|
17
|
+
id: typeof firstResult?.id === "string" ? firstResult.id : null,
|
|
18
|
+
query_results: rawResults
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function traverseImpactGraph(params) {
|
|
2
|
+
const outgoing = new Map();
|
|
3
|
+
const incoming = new Map();
|
|
4
|
+
for (const relation of params.relations) {
|
|
5
|
+
const outList = outgoing.get(relation.from) ?? [];
|
|
6
|
+
outList.push(relation);
|
|
7
|
+
outgoing.set(relation.from, outList);
|
|
8
|
+
const inList = incoming.get(relation.to) ?? [];
|
|
9
|
+
inList.push(relation);
|
|
10
|
+
incoming.set(relation.to, inList);
|
|
11
|
+
}
|
|
12
|
+
const seen = new Set([params.seedId]);
|
|
13
|
+
const queue = [{ id: params.seedId, hop: 0 }];
|
|
14
|
+
const visited = new Map();
|
|
15
|
+
const traversedEdges = [];
|
|
16
|
+
const traversedEdgeKeys = new Set();
|
|
17
|
+
while (queue.length > 0) {
|
|
18
|
+
const current = queue.shift();
|
|
19
|
+
if (current.hop >= params.depth) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const neighbors = [
|
|
23
|
+
...(outgoing.get(current.id) ?? []).map((edge) => ({
|
|
24
|
+
edge,
|
|
25
|
+
next: edge.to,
|
|
26
|
+
direction: "outgoing"
|
|
27
|
+
})),
|
|
28
|
+
...(incoming.get(current.id) ?? []).map((edge) => ({
|
|
29
|
+
edge,
|
|
30
|
+
next: edge.from,
|
|
31
|
+
direction: "incoming"
|
|
32
|
+
}))
|
|
33
|
+
];
|
|
34
|
+
for (const neighbor of neighbors) {
|
|
35
|
+
const target = neighbor.next;
|
|
36
|
+
const nextHop = current.hop + 1;
|
|
37
|
+
if (!seen.has(target)) {
|
|
38
|
+
seen.add(target);
|
|
39
|
+
queue.push({ id: target, hop: nextHop });
|
|
40
|
+
visited.set(target, {
|
|
41
|
+
hops: nextHop,
|
|
42
|
+
via_relation: neighbor.edge.relation,
|
|
43
|
+
direction: neighbor.direction,
|
|
44
|
+
via_entity: current.id,
|
|
45
|
+
via_note: neighbor.edge.note
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const edgeKey = `${neighbor.edge.from}|${neighbor.edge.relation}|${neighbor.edge.to}|${neighbor.edge.note}`;
|
|
49
|
+
if (!traversedEdgeKeys.has(edgeKey)) {
|
|
50
|
+
traversedEdgeKeys.add(edgeKey);
|
|
51
|
+
traversedEdges.push({
|
|
52
|
+
from: neighbor.edge.from,
|
|
53
|
+
to: neighbor.edge.to,
|
|
54
|
+
relation: neighbor.edge.relation,
|
|
55
|
+
note: neighbor.edge.note
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
visited,
|
|
62
|
+
traversedEdges
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
function parseJsonlLine(line) {
|
|
4
|
+
const trimmed = line.trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(trimmed);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function* readJsonlRecords(filePath) {
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const fd = fs.openSync(filePath, "r");
|
|
20
|
+
const decoder = new StringDecoder("utf8");
|
|
21
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
22
|
+
let carry = "";
|
|
23
|
+
try {
|
|
24
|
+
while (true) {
|
|
25
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
26
|
+
if (bytesRead === 0) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
carry += decoder.write(buffer.subarray(0, bytesRead));
|
|
30
|
+
let lineStart = 0;
|
|
31
|
+
let newlineIndex = carry.indexOf("\n", lineStart);
|
|
32
|
+
while (newlineIndex !== -1) {
|
|
33
|
+
const parsed = parseJsonlLine(carry.slice(lineStart, newlineIndex));
|
|
34
|
+
if (parsed) {
|
|
35
|
+
yield parsed;
|
|
36
|
+
}
|
|
37
|
+
lineStart = newlineIndex + 1;
|
|
38
|
+
newlineIndex = carry.indexOf("\n", lineStart);
|
|
39
|
+
}
|
|
40
|
+
carry = carry.slice(lineStart);
|
|
41
|
+
}
|
|
42
|
+
carry += decoder.end();
|
|
43
|
+
const parsed = parseJsonlLine(carry);
|
|
44
|
+
if (parsed) {
|
|
45
|
+
yield parsed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
fs.closeSync(fd);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function readJsonl(filePath) {
|
|
53
|
+
return Array.from(readJsonlRecords(filePath));
|
|
54
|
+
}
|
|
55
|
+
export function writeJsonlRecords(filePath, records) {
|
|
56
|
+
const fd = fs.openSync(filePath, "w");
|
|
57
|
+
let count = 0;
|
|
58
|
+
try {
|
|
59
|
+
for (const record of records) {
|
|
60
|
+
fs.writeSync(fd, `${JSON.stringify(record)}\n`, undefined, "utf8");
|
|
61
|
+
count += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
fs.closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
export function asString(value, fallback = "") {
|
|
70
|
+
return typeof value === "string" ? value : fallback;
|
|
71
|
+
}
|
|
72
|
+
export function asNumber(value, fallback = 0) {
|
|
73
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
74
|
+
}
|
|
75
|
+
export function asBoolean(value, fallback = false) {
|
|
76
|
+
return typeof value === "boolean" ? value : fallback;
|
|
77
|
+
}
|