sentinelayer-cli 0.1.2 → 0.4.4
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/README.md +998 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +63 -54
- package/src/agents/jules/config/definition.js +209 -209
- package/src/agents/jules/config/system-prompt.js +175 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +377 -377
- package/src/agents/jules/loop.js +367 -367
- package/src/agents/jules/pulse.js +327 -319
- package/src/agents/jules/stream.js +186 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +308 -308
- package/src/agents/jules/tools/auth-audit.js +557 -222
- package/src/agents/jules/tools/dispatch.js +327 -327
- package/src/agents/jules/tools/file-edit.js +180 -180
- package/src/agents/jules/tools/file-read.js +100 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +168 -168
- package/src/agents/jules/tools/grep.js +228 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +161 -161
- package/src/agents/jules/tools/runtime-audit.js +503 -493
- package/src/agents/jules/tools/shell.js +383 -383
- package/src/agents/jules/tools/url-policy.js +100 -0
- package/src/ai/aidenid.js +972 -945
- package/src/ai/client.js +508 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +45 -11
- package/src/auth/http.js +270 -113
- package/src/auth/service.js +891 -848
- package/src/auth/session-store.js +359 -345
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1337
- package/src/commands/ai/provision-governance.js +1272 -1246
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1166 -1166
- package/src/commands/auth.js +375 -366
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -404
- package/src/commands/omargate.js +15 -15
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +866 -788
- package/src/commands/spec.js +716 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +510 -510
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +770 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +626 -626
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/watchdog.js +971 -971
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/legacy-cli.js +2592 -2435
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +118 -106
- package/src/review/ai-review.js +669 -669
- package/src/review/local-review.js +1295 -1284
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -0
- package/src/scaffold/templates.js +150 -0
- package/src/scan/generator.js +418 -351
- package/src/scan/gh-secrets.js +107 -0
- package/src/spec/generator.js +519 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/sync.js +107 -61
- package/src/ui/markdown.js +220 -220
package/src/memory/blackboard.js
CHANGED
|
@@ -1,301 +1,301 @@
|
|
|
1
|
-
import fsp from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
function normalizeString(value) {
|
|
5
|
-
return String(value || "").trim();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function normalizeSeverity(value) {
|
|
9
|
-
const severity = normalizeString(value).toUpperCase();
|
|
10
|
-
if (severity === "P0" || severity === "P1" || severity === "P2" || severity === "P3") {
|
|
11
|
-
return severity;
|
|
12
|
-
}
|
|
13
|
-
return "P3";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function tokenize(value) {
|
|
17
|
-
return Array.from(
|
|
18
|
-
new Set(
|
|
19
|
-
normalizeString(value)
|
|
20
|
-
.toLowerCase()
|
|
21
|
-
.split(/[^a-z0-9_]+/g)
|
|
22
|
-
.map((token) => token.trim())
|
|
23
|
-
.filter((token) => token.length >= 2)
|
|
24
|
-
)
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const AGENT_HINT_TOKENS = Object.freeze({
|
|
29
|
-
security: ["security", "token", "secret", "credential", "auth", "injection", "xss"],
|
|
30
|
-
architecture: ["architecture", "design", "dependency", "boundary", "module", "coupling"],
|
|
31
|
-
testing: ["testing", "test", "coverage", "assertion", "regression", "e2e", "unit"],
|
|
32
|
-
performance: ["performance", "latency", "throughput", "runtime", "query", "n+1", "cache"],
|
|
33
|
-
compliance: ["compliance", "soc2", "hipaa", "gdpr", "privacy", "pii", "control"],
|
|
34
|
-
documentation: ["documentation", "docs", "spec", "guide", "readme", "contract"],
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const SEVERITY_SCORE = Object.freeze({
|
|
38
|
-
P0: 1,
|
|
39
|
-
P1: 0.9,
|
|
40
|
-
P2: 0.65,
|
|
41
|
-
P3: 0.4,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
function normalizeNumber(value, fallback = 0) {
|
|
45
|
-
const parsed = Number(value);
|
|
46
|
-
if (!Number.isFinite(parsed)) {
|
|
47
|
-
return fallback;
|
|
48
|
-
}
|
|
49
|
-
return parsed;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function toPosixPath(value) {
|
|
53
|
-
return normalizeString(value).replace(/\\/g, "/");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildEntryText(entry = {}) {
|
|
57
|
-
return [
|
|
58
|
-
entry.agentId,
|
|
59
|
-
entry.source,
|
|
60
|
-
entry.severity,
|
|
61
|
-
entry.file,
|
|
62
|
-
entry.layer,
|
|
63
|
-
entry.ruleId,
|
|
64
|
-
entry.message,
|
|
65
|
-
entry.note,
|
|
66
|
-
entry.needleId,
|
|
67
|
-
]
|
|
68
|
-
.filter(Boolean)
|
|
69
|
-
.join(" ");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function buildEntryScore({ entry, queryTokens, hintTokens, newestSequence }) {
|
|
73
|
-
const tokens = Array.isArray(entry.tokens) ? entry.tokens : [];
|
|
74
|
-
const tokenSet = new Set(tokens);
|
|
75
|
-
const queryHitCount = queryTokens.filter((token) => tokenSet.has(token)).length;
|
|
76
|
-
const hintHitCount = hintTokens.filter((token) => tokenSet.has(token)).length;
|
|
77
|
-
const querySignal = queryTokens.length > 0 ? queryHitCount / queryTokens.length : 0;
|
|
78
|
-
const hintSignal = hintTokens.length > 0 ? hintHitCount / hintTokens.length : 0;
|
|
79
|
-
const severitySignal = SEVERITY_SCORE[normalizeSeverity(entry.severity)] || 0.35;
|
|
80
|
-
const recencySignal =
|
|
81
|
-
newestSequence > 0 ? Math.max(0, Math.min(1, normalizeNumber(entry.sequence, 0) / newestSequence)) : 0;
|
|
82
|
-
return querySignal * 0.55 + hintSignal * 0.15 + severitySignal * 0.2 + recencySignal * 0.1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function nextEntryId(blackboard) {
|
|
86
|
-
const nextSequence = normalizeNumber(blackboard.nextSequence, 1);
|
|
87
|
-
blackboard.nextSequence = nextSequence + 1;
|
|
88
|
-
return {
|
|
89
|
-
entryId: `bb-${String(nextSequence).padStart(6, "0")}`,
|
|
90
|
-
sequence: nextSequence,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function asNeedleId(value) {
|
|
95
|
-
return normalizeString(value).toLowerCase();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function createBlackboard({ runId = "", scope = "audit-orchestrator" } = {}) {
|
|
99
|
-
return {
|
|
100
|
-
schemaVersion: "1.0.0",
|
|
101
|
-
runId: normalizeString(runId),
|
|
102
|
-
scope: normalizeString(scope) || "audit-orchestrator",
|
|
103
|
-
createdAt: new Date().toISOString(),
|
|
104
|
-
updatedAt: new Date().toISOString(),
|
|
105
|
-
nextSequence: 1,
|
|
106
|
-
entries: [],
|
|
107
|
-
queryEvents: [],
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function appendBlackboardEntry(blackboard, entry = {}) {
|
|
112
|
-
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
113
|
-
throw new Error("Invalid blackboard state.");
|
|
114
|
-
}
|
|
115
|
-
const { entryId, sequence } = nextEntryId(blackboard);
|
|
116
|
-
const normalizedEntry = {
|
|
117
|
-
entryId,
|
|
118
|
-
sequence,
|
|
119
|
-
timestamp: new Date().toISOString(),
|
|
120
|
-
agentId: normalizeString(entry.agentId),
|
|
121
|
-
source: normalizeString(entry.source) || "agent",
|
|
122
|
-
severity: normalizeSeverity(entry.severity),
|
|
123
|
-
file: toPosixPath(entry.file),
|
|
124
|
-
line: Math.max(0, Math.floor(normalizeNumber(entry.line, 0))),
|
|
125
|
-
layer: normalizeString(entry.layer),
|
|
126
|
-
ruleId: normalizeString(entry.ruleId),
|
|
127
|
-
message: normalizeString(entry.message),
|
|
128
|
-
note: normalizeString(entry.note),
|
|
129
|
-
confidence: Math.max(0, Math.min(1, normalizeNumber(entry.confidence, 0.7))),
|
|
130
|
-
needleId: asNeedleId(entry.needleId),
|
|
131
|
-
};
|
|
132
|
-
normalizedEntry.tokens = tokenize(buildEntryText(normalizedEntry));
|
|
133
|
-
blackboard.entries.push(normalizedEntry);
|
|
134
|
-
blackboard.updatedAt = normalizedEntry.timestamp;
|
|
135
|
-
return normalizedEntry;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function appendBlackboardFindings(
|
|
139
|
-
blackboard,
|
|
140
|
-
{ agentId = "", findings = [], source = "agent", note = "", confidence = 0.7 } = {}
|
|
141
|
-
) {
|
|
142
|
-
const writtenEntries = [];
|
|
143
|
-
for (const finding of findings || []) {
|
|
144
|
-
writtenEntries.push(
|
|
145
|
-
appendBlackboardEntry(blackboard, {
|
|
146
|
-
agentId,
|
|
147
|
-
source,
|
|
148
|
-
severity: finding.severity,
|
|
149
|
-
file: finding.file,
|
|
150
|
-
line: finding.line,
|
|
151
|
-
layer: finding.layer,
|
|
152
|
-
ruleId: finding.ruleId || finding.code || "",
|
|
153
|
-
message: finding.message || finding.title || "",
|
|
154
|
-
note,
|
|
155
|
-
confidence: normalizeNumber(finding.confidence, confidence),
|
|
156
|
-
needleId: finding.needleId || "",
|
|
157
|
-
})
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
return writtenEntries;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function queryBlackboard(
|
|
164
|
-
blackboard,
|
|
165
|
-
{ query = "", agentId = "", limit = 20, minScore = 0 } = {}
|
|
166
|
-
) {
|
|
167
|
-
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
168
|
-
throw new Error("Invalid blackboard state.");
|
|
169
|
-
}
|
|
170
|
-
const normalizedAgentId = normalizeString(agentId).toLowerCase();
|
|
171
|
-
const queryTokens = tokenize(query);
|
|
172
|
-
const hintTokens = AGENT_HINT_TOKENS[normalizedAgentId] || [];
|
|
173
|
-
const normalizedLimit = Math.max(1, Math.floor(normalizeNumber(limit, 20)));
|
|
174
|
-
const normalizedMinScore = Math.max(0, Math.min(1, normalizeNumber(minScore, 0)));
|
|
175
|
-
const newestSequence = blackboard.entries.reduce(
|
|
176
|
-
(max, entry) => Math.max(max, normalizeNumber(entry.sequence, 0)),
|
|
177
|
-
0
|
|
178
|
-
);
|
|
179
|
-
const scored = blackboard.entries
|
|
180
|
-
.map((entry) => ({
|
|
181
|
-
entry,
|
|
182
|
-
score: buildEntryScore({
|
|
183
|
-
entry,
|
|
184
|
-
queryTokens,
|
|
185
|
-
hintTokens,
|
|
186
|
-
newestSequence,
|
|
187
|
-
}),
|
|
188
|
-
}))
|
|
189
|
-
.filter((item) => item.score >= normalizedMinScore)
|
|
190
|
-
.sort((left, right) => {
|
|
191
|
-
if (right.score !== left.score) {
|
|
192
|
-
return right.score - left.score;
|
|
193
|
-
}
|
|
194
|
-
return left.entry.sequence - right.entry.sequence;
|
|
195
|
-
})
|
|
196
|
-
.slice(0, normalizedLimit)
|
|
197
|
-
.map((item) => ({
|
|
198
|
-
...item.entry,
|
|
199
|
-
score: Number(item.score.toFixed(6)),
|
|
200
|
-
}));
|
|
201
|
-
|
|
202
|
-
blackboard.queryEvents.push({
|
|
203
|
-
timestamp: new Date().toISOString(),
|
|
204
|
-
query: normalizeString(query),
|
|
205
|
-
agentId: normalizedAgentId,
|
|
206
|
-
limit: normalizedLimit,
|
|
207
|
-
minScore: normalizedMinScore,
|
|
208
|
-
queryTokenCount: queryTokens.length,
|
|
209
|
-
hintTokenCount: hintTokens.length,
|
|
210
|
-
returnedCount: scored.length,
|
|
211
|
-
});
|
|
212
|
-
blackboard.updatedAt = new Date().toISOString();
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
query: normalizeString(query),
|
|
216
|
-
agentId: normalizedAgentId,
|
|
217
|
-
limit: normalizedLimit,
|
|
218
|
-
queryTokens,
|
|
219
|
-
hintTokens,
|
|
220
|
-
entries: scored,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function benchmarkBlackboardNeedleRecall(
|
|
225
|
-
blackboard,
|
|
226
|
-
{ query = "", agentId = "", needleIds = [], limit = 20 } = {}
|
|
227
|
-
) {
|
|
228
|
-
const expectedNeedles = Array.from(
|
|
229
|
-
new Set(
|
|
230
|
-
(needleIds || [])
|
|
231
|
-
.map((needleId) => asNeedleId(needleId))
|
|
232
|
-
.filter(Boolean)
|
|
233
|
-
)
|
|
234
|
-
);
|
|
235
|
-
const result = queryBlackboard(blackboard, {
|
|
236
|
-
query,
|
|
237
|
-
agentId,
|
|
238
|
-
limit: Math.max(limit, expectedNeedles.length, 1),
|
|
239
|
-
});
|
|
240
|
-
const matched = new Set();
|
|
241
|
-
for (const entry of result.entries) {
|
|
242
|
-
const needleId = asNeedleId(entry.needleId);
|
|
243
|
-
if (needleId && expectedNeedles.includes(needleId)) {
|
|
244
|
-
matched.add(needleId);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const recall = expectedNeedles.length > 0 ? matched.size / expectedNeedles.length : 1;
|
|
248
|
-
return {
|
|
249
|
-
expectedCount: expectedNeedles.length,
|
|
250
|
-
matchedCount: matched.size,
|
|
251
|
-
recall: Number(recall.toFixed(6)),
|
|
252
|
-
pass: recall >= 0.95,
|
|
253
|
-
missingNeedles: expectedNeedles.filter((needleId) => !matched.has(needleId)),
|
|
254
|
-
retrievedEntryIds: result.entries.map((entry) => entry.entryId),
|
|
255
|
-
query: result.query,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function summarizeBlackboard(blackboard) {
|
|
260
|
-
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
261
|
-
throw new Error("Invalid blackboard state.");
|
|
262
|
-
}
|
|
263
|
-
const severity = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
264
|
-
for (const entry of blackboard.entries) {
|
|
265
|
-
const normalizedSeverity = normalizeSeverity(entry.severity);
|
|
266
|
-
severity[normalizedSeverity] += 1;
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
entryCount: blackboard.entries.length,
|
|
270
|
-
queryCount: Array.isArray(blackboard.queryEvents) ? blackboard.queryEvents.length : 0,
|
|
271
|
-
severity,
|
|
272
|
-
createdAt: blackboard.createdAt,
|
|
273
|
-
updatedAt: blackboard.updatedAt,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export async function writeBlackboardArtifact(blackboard, { outputRoot = "" } = {}) {
|
|
278
|
-
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
279
|
-
throw new Error("Invalid blackboard state.");
|
|
280
|
-
}
|
|
281
|
-
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
282
|
-
const memoryDirectory = path.join(resolvedOutputRoot, "memory");
|
|
283
|
-
await fsp.mkdir(memoryDirectory, { recursive: true });
|
|
284
|
-
const runId = normalizeString(blackboard.runId) || "run";
|
|
285
|
-
const artifactPath = path.join(memoryDirectory, `blackboard-${runId}.json`);
|
|
286
|
-
const summary = summarizeBlackboard(blackboard);
|
|
287
|
-
const payload = {
|
|
288
|
-
schemaVersion: "1.0.0",
|
|
289
|
-
runId,
|
|
290
|
-
scope: blackboard.scope,
|
|
291
|
-
generatedAt: new Date().toISOString(),
|
|
292
|
-
summary,
|
|
293
|
-
entries: blackboard.entries,
|
|
294
|
-
queryEvents: blackboard.queryEvents,
|
|
295
|
-
};
|
|
296
|
-
await fsp.writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
297
|
-
return {
|
|
298
|
-
artifactPath,
|
|
299
|
-
summary,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function normalizeString(value) {
|
|
5
|
+
return String(value || "").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeSeverity(value) {
|
|
9
|
+
const severity = normalizeString(value).toUpperCase();
|
|
10
|
+
if (severity === "P0" || severity === "P1" || severity === "P2" || severity === "P3") {
|
|
11
|
+
return severity;
|
|
12
|
+
}
|
|
13
|
+
return "P3";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function tokenize(value) {
|
|
17
|
+
return Array.from(
|
|
18
|
+
new Set(
|
|
19
|
+
normalizeString(value)
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.split(/[^a-z0-9_]+/g)
|
|
22
|
+
.map((token) => token.trim())
|
|
23
|
+
.filter((token) => token.length >= 2)
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const AGENT_HINT_TOKENS = Object.freeze({
|
|
29
|
+
security: ["security", "token", "secret", "credential", "auth", "injection", "xss"],
|
|
30
|
+
architecture: ["architecture", "design", "dependency", "boundary", "module", "coupling"],
|
|
31
|
+
testing: ["testing", "test", "coverage", "assertion", "regression", "e2e", "unit"],
|
|
32
|
+
performance: ["performance", "latency", "throughput", "runtime", "query", "n+1", "cache"],
|
|
33
|
+
compliance: ["compliance", "soc2", "hipaa", "gdpr", "privacy", "pii", "control"],
|
|
34
|
+
documentation: ["documentation", "docs", "spec", "guide", "readme", "contract"],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const SEVERITY_SCORE = Object.freeze({
|
|
38
|
+
P0: 1,
|
|
39
|
+
P1: 0.9,
|
|
40
|
+
P2: 0.65,
|
|
41
|
+
P3: 0.4,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function normalizeNumber(value, fallback = 0) {
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
if (!Number.isFinite(parsed)) {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toPosixPath(value) {
|
|
53
|
+
return normalizeString(value).replace(/\\/g, "/");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildEntryText(entry = {}) {
|
|
57
|
+
return [
|
|
58
|
+
entry.agentId,
|
|
59
|
+
entry.source,
|
|
60
|
+
entry.severity,
|
|
61
|
+
entry.file,
|
|
62
|
+
entry.layer,
|
|
63
|
+
entry.ruleId,
|
|
64
|
+
entry.message,
|
|
65
|
+
entry.note,
|
|
66
|
+
entry.needleId,
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(" ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildEntryScore({ entry, queryTokens, hintTokens, newestSequence }) {
|
|
73
|
+
const tokens = Array.isArray(entry.tokens) ? entry.tokens : [];
|
|
74
|
+
const tokenSet = new Set(tokens);
|
|
75
|
+
const queryHitCount = queryTokens.filter((token) => tokenSet.has(token)).length;
|
|
76
|
+
const hintHitCount = hintTokens.filter((token) => tokenSet.has(token)).length;
|
|
77
|
+
const querySignal = queryTokens.length > 0 ? queryHitCount / queryTokens.length : 0;
|
|
78
|
+
const hintSignal = hintTokens.length > 0 ? hintHitCount / hintTokens.length : 0;
|
|
79
|
+
const severitySignal = SEVERITY_SCORE[normalizeSeverity(entry.severity)] || 0.35;
|
|
80
|
+
const recencySignal =
|
|
81
|
+
newestSequence > 0 ? Math.max(0, Math.min(1, normalizeNumber(entry.sequence, 0) / newestSequence)) : 0;
|
|
82
|
+
return querySignal * 0.55 + hintSignal * 0.15 + severitySignal * 0.2 + recencySignal * 0.1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function nextEntryId(blackboard) {
|
|
86
|
+
const nextSequence = normalizeNumber(blackboard.nextSequence, 1);
|
|
87
|
+
blackboard.nextSequence = nextSequence + 1;
|
|
88
|
+
return {
|
|
89
|
+
entryId: `bb-${String(nextSequence).padStart(6, "0")}`,
|
|
90
|
+
sequence: nextSequence,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function asNeedleId(value) {
|
|
95
|
+
return normalizeString(value).toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createBlackboard({ runId = "", scope = "audit-orchestrator" } = {}) {
|
|
99
|
+
return {
|
|
100
|
+
schemaVersion: "1.0.0",
|
|
101
|
+
runId: normalizeString(runId),
|
|
102
|
+
scope: normalizeString(scope) || "audit-orchestrator",
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
updatedAt: new Date().toISOString(),
|
|
105
|
+
nextSequence: 1,
|
|
106
|
+
entries: [],
|
|
107
|
+
queryEvents: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function appendBlackboardEntry(blackboard, entry = {}) {
|
|
112
|
+
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
113
|
+
throw new Error("Invalid blackboard state.");
|
|
114
|
+
}
|
|
115
|
+
const { entryId, sequence } = nextEntryId(blackboard);
|
|
116
|
+
const normalizedEntry = {
|
|
117
|
+
entryId,
|
|
118
|
+
sequence,
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
agentId: normalizeString(entry.agentId),
|
|
121
|
+
source: normalizeString(entry.source) || "agent",
|
|
122
|
+
severity: normalizeSeverity(entry.severity),
|
|
123
|
+
file: toPosixPath(entry.file),
|
|
124
|
+
line: Math.max(0, Math.floor(normalizeNumber(entry.line, 0))),
|
|
125
|
+
layer: normalizeString(entry.layer),
|
|
126
|
+
ruleId: normalizeString(entry.ruleId),
|
|
127
|
+
message: normalizeString(entry.message),
|
|
128
|
+
note: normalizeString(entry.note),
|
|
129
|
+
confidence: Math.max(0, Math.min(1, normalizeNumber(entry.confidence, 0.7))),
|
|
130
|
+
needleId: asNeedleId(entry.needleId),
|
|
131
|
+
};
|
|
132
|
+
normalizedEntry.tokens = tokenize(buildEntryText(normalizedEntry));
|
|
133
|
+
blackboard.entries.push(normalizedEntry);
|
|
134
|
+
blackboard.updatedAt = normalizedEntry.timestamp;
|
|
135
|
+
return normalizedEntry;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function appendBlackboardFindings(
|
|
139
|
+
blackboard,
|
|
140
|
+
{ agentId = "", findings = [], source = "agent", note = "", confidence = 0.7 } = {}
|
|
141
|
+
) {
|
|
142
|
+
const writtenEntries = [];
|
|
143
|
+
for (const finding of findings || []) {
|
|
144
|
+
writtenEntries.push(
|
|
145
|
+
appendBlackboardEntry(blackboard, {
|
|
146
|
+
agentId,
|
|
147
|
+
source,
|
|
148
|
+
severity: finding.severity,
|
|
149
|
+
file: finding.file,
|
|
150
|
+
line: finding.line,
|
|
151
|
+
layer: finding.layer,
|
|
152
|
+
ruleId: finding.ruleId || finding.code || "",
|
|
153
|
+
message: finding.message || finding.title || "",
|
|
154
|
+
note,
|
|
155
|
+
confidence: normalizeNumber(finding.confidence, confidence),
|
|
156
|
+
needleId: finding.needleId || "",
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return writtenEntries;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function queryBlackboard(
|
|
164
|
+
blackboard,
|
|
165
|
+
{ query = "", agentId = "", limit = 20, minScore = 0 } = {}
|
|
166
|
+
) {
|
|
167
|
+
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
168
|
+
throw new Error("Invalid blackboard state.");
|
|
169
|
+
}
|
|
170
|
+
const normalizedAgentId = normalizeString(agentId).toLowerCase();
|
|
171
|
+
const queryTokens = tokenize(query);
|
|
172
|
+
const hintTokens = AGENT_HINT_TOKENS[normalizedAgentId] || [];
|
|
173
|
+
const normalizedLimit = Math.max(1, Math.floor(normalizeNumber(limit, 20)));
|
|
174
|
+
const normalizedMinScore = Math.max(0, Math.min(1, normalizeNumber(minScore, 0)));
|
|
175
|
+
const newestSequence = blackboard.entries.reduce(
|
|
176
|
+
(max, entry) => Math.max(max, normalizeNumber(entry.sequence, 0)),
|
|
177
|
+
0
|
|
178
|
+
);
|
|
179
|
+
const scored = blackboard.entries
|
|
180
|
+
.map((entry) => ({
|
|
181
|
+
entry,
|
|
182
|
+
score: buildEntryScore({
|
|
183
|
+
entry,
|
|
184
|
+
queryTokens,
|
|
185
|
+
hintTokens,
|
|
186
|
+
newestSequence,
|
|
187
|
+
}),
|
|
188
|
+
}))
|
|
189
|
+
.filter((item) => item.score >= normalizedMinScore)
|
|
190
|
+
.sort((left, right) => {
|
|
191
|
+
if (right.score !== left.score) {
|
|
192
|
+
return right.score - left.score;
|
|
193
|
+
}
|
|
194
|
+
return left.entry.sequence - right.entry.sequence;
|
|
195
|
+
})
|
|
196
|
+
.slice(0, normalizedLimit)
|
|
197
|
+
.map((item) => ({
|
|
198
|
+
...item.entry,
|
|
199
|
+
score: Number(item.score.toFixed(6)),
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
blackboard.queryEvents.push({
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
query: normalizeString(query),
|
|
205
|
+
agentId: normalizedAgentId,
|
|
206
|
+
limit: normalizedLimit,
|
|
207
|
+
minScore: normalizedMinScore,
|
|
208
|
+
queryTokenCount: queryTokens.length,
|
|
209
|
+
hintTokenCount: hintTokens.length,
|
|
210
|
+
returnedCount: scored.length,
|
|
211
|
+
});
|
|
212
|
+
blackboard.updatedAt = new Date().toISOString();
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
query: normalizeString(query),
|
|
216
|
+
agentId: normalizedAgentId,
|
|
217
|
+
limit: normalizedLimit,
|
|
218
|
+
queryTokens,
|
|
219
|
+
hintTokens,
|
|
220
|
+
entries: scored,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function benchmarkBlackboardNeedleRecall(
|
|
225
|
+
blackboard,
|
|
226
|
+
{ query = "", agentId = "", needleIds = [], limit = 20 } = {}
|
|
227
|
+
) {
|
|
228
|
+
const expectedNeedles = Array.from(
|
|
229
|
+
new Set(
|
|
230
|
+
(needleIds || [])
|
|
231
|
+
.map((needleId) => asNeedleId(needleId))
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
const result = queryBlackboard(blackboard, {
|
|
236
|
+
query,
|
|
237
|
+
agentId,
|
|
238
|
+
limit: Math.max(limit, expectedNeedles.length, 1),
|
|
239
|
+
});
|
|
240
|
+
const matched = new Set();
|
|
241
|
+
for (const entry of result.entries) {
|
|
242
|
+
const needleId = asNeedleId(entry.needleId);
|
|
243
|
+
if (needleId && expectedNeedles.includes(needleId)) {
|
|
244
|
+
matched.add(needleId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const recall = expectedNeedles.length > 0 ? matched.size / expectedNeedles.length : 1;
|
|
248
|
+
return {
|
|
249
|
+
expectedCount: expectedNeedles.length,
|
|
250
|
+
matchedCount: matched.size,
|
|
251
|
+
recall: Number(recall.toFixed(6)),
|
|
252
|
+
pass: recall >= 0.95,
|
|
253
|
+
missingNeedles: expectedNeedles.filter((needleId) => !matched.has(needleId)),
|
|
254
|
+
retrievedEntryIds: result.entries.map((entry) => entry.entryId),
|
|
255
|
+
query: result.query,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function summarizeBlackboard(blackboard) {
|
|
260
|
+
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
261
|
+
throw new Error("Invalid blackboard state.");
|
|
262
|
+
}
|
|
263
|
+
const severity = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
264
|
+
for (const entry of blackboard.entries) {
|
|
265
|
+
const normalizedSeverity = normalizeSeverity(entry.severity);
|
|
266
|
+
severity[normalizedSeverity] += 1;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
entryCount: blackboard.entries.length,
|
|
270
|
+
queryCount: Array.isArray(blackboard.queryEvents) ? blackboard.queryEvents.length : 0,
|
|
271
|
+
severity,
|
|
272
|
+
createdAt: blackboard.createdAt,
|
|
273
|
+
updatedAt: blackboard.updatedAt,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function writeBlackboardArtifact(blackboard, { outputRoot = "" } = {}) {
|
|
278
|
+
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
279
|
+
throw new Error("Invalid blackboard state.");
|
|
280
|
+
}
|
|
281
|
+
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
282
|
+
const memoryDirectory = path.join(resolvedOutputRoot, "memory");
|
|
283
|
+
await fsp.mkdir(memoryDirectory, { recursive: true });
|
|
284
|
+
const runId = normalizeString(blackboard.runId) || "run";
|
|
285
|
+
const artifactPath = path.join(memoryDirectory, `blackboard-${runId}.json`);
|
|
286
|
+
const summary = summarizeBlackboard(blackboard);
|
|
287
|
+
const payload = {
|
|
288
|
+
schemaVersion: "1.0.0",
|
|
289
|
+
runId,
|
|
290
|
+
scope: blackboard.scope,
|
|
291
|
+
generatedAt: new Date().toISOString(),
|
|
292
|
+
summary,
|
|
293
|
+
entries: blackboard.entries,
|
|
294
|
+
queryEvents: blackboard.queryEvents,
|
|
295
|
+
};
|
|
296
|
+
await fsp.writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
297
|
+
return {
|
|
298
|
+
artifactPath,
|
|
299
|
+
summary,
|
|
300
|
+
};
|
|
301
|
+
}
|