role-os 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +58 -14
- package/bin/roleos.mjs +20 -0
- package/package.json +2 -2
- package/src/artifacts.mjs +79 -1
- package/src/audit-cmd.mjs +401 -0
- package/src/brainstorm-roles.mjs +44 -1
- package/src/composite.mjs +41 -0
- package/src/dispatch.mjs +9 -83
- package/src/hooks.mjs +5 -5
- package/src/knowledge/analyze-artifact-evidence.mjs +420 -0
- package/src/knowledge/attach-bundle-to-evidence.mjs +62 -0
- package/src/knowledge/attach-bundle-to-packet.mjs +42 -0
- package/src/knowledge/fallback-policy.mjs +79 -0
- package/src/knowledge/index.mjs +14 -0
- package/src/knowledge/render-knowledge-block.mjs +215 -0
- package/src/knowledge/resolve-overlay.mjs +66 -0
- package/src/knowledge/retrieve-for-dispatch.mjs +150 -0
- package/src/mission-run.mjs +119 -2
- package/src/mission.mjs +130 -0
- package/src/packs.mjs +37 -0
- package/src/route.mjs +51 -0
- package/src/run-cmd.mjs +4 -1
- package/src/run.mjs +51 -3
- package/src/state-machine.mjs +70 -0
- package/src/swarm/build-gate.mjs +127 -0
- package/src/swarm/domain-detect.mjs +230 -0
- package/src/swarm/persist-bridge.mjs +174 -0
- package/src/swarm-cmd.mjs +424 -0
- package/src/tool-profiles.mjs +91 -0
- package/src/trial.mjs +1 -1
package/src/hooks.mjs
CHANGED
|
@@ -322,7 +322,7 @@ function generateSessionStartScript() {
|
|
|
322
322
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
323
323
|
import { join } from "node:path";
|
|
324
324
|
|
|
325
|
-
const input = JSON.parse(readFileSync(
|
|
325
|
+
const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
|
|
326
326
|
const cwd = input.cwd || process.cwd();
|
|
327
327
|
const stateDir = join(cwd, ".claude", "hooks");
|
|
328
328
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -356,7 +356,7 @@ function generatePromptSubmitScript() {
|
|
|
356
356
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
357
357
|
import { join } from "node:path";
|
|
358
358
|
|
|
359
|
-
const input = JSON.parse(readFileSync(
|
|
359
|
+
const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
|
|
360
360
|
const cwd = input.cwd || process.cwd();
|
|
361
361
|
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
362
362
|
|
|
@@ -389,7 +389,7 @@ function generatePreToolUseScript() {
|
|
|
389
389
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
390
390
|
import { join } from "node:path";
|
|
391
391
|
|
|
392
|
-
const input = JSON.parse(readFileSync(
|
|
392
|
+
const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
|
|
393
393
|
const cwd = input.cwd || process.cwd();
|
|
394
394
|
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
395
395
|
|
|
@@ -421,7 +421,7 @@ function generateSubagentStartScript() {
|
|
|
421
421
|
import { readFileSync, existsSync } from "node:fs";
|
|
422
422
|
import { join } from "node:path";
|
|
423
423
|
|
|
424
|
-
const input = JSON.parse(readFileSync(
|
|
424
|
+
const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
|
|
425
425
|
const cwd = input.cwd || process.cwd();
|
|
426
426
|
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
427
427
|
|
|
@@ -444,7 +444,7 @@ function generateStopScript() {
|
|
|
444
444
|
import { readFileSync, existsSync } from "node:fs";
|
|
445
445
|
import { join } from "node:path";
|
|
446
446
|
|
|
447
|
-
const input = JSON.parse(readFileSync(
|
|
447
|
+
const input = JSON.parse(readFileSync(0, "utf-8").toString() || "{}");
|
|
448
448
|
const cwd = input.cwd || process.cwd();
|
|
449
449
|
const statePath = join(cwd, ".claude", "hooks", "session-state.json");
|
|
450
450
|
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact Evidence Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Connects: retrieval bundle → prompt knowledge block → artifact output
|
|
5
|
+
*
|
|
6
|
+
* Deterministic chain-of-custody checks:
|
|
7
|
+
* - Which retrieved references appear in the artifact?
|
|
8
|
+
* - Which artifact claims are backed by retrieved evidence?
|
|
9
|
+
* - Did the artifact cite material when it should?
|
|
10
|
+
* - Did it invent unsupported references?
|
|
11
|
+
* - Did posture status affect artifact behavior?
|
|
12
|
+
*
|
|
13
|
+
* No LLM judge vibes. Just structural truth checks.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── ArtifactEvidenceReport ──────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} ArtifactEvidenceReport
|
|
20
|
+
* @property {string} role_id
|
|
21
|
+
* @property {string} task_id
|
|
22
|
+
* @property {string} knowledge_status - strong | weak | stale | conflicted | none
|
|
23
|
+
*
|
|
24
|
+
* @property {RetrievedReferenceCheck[]} reference_checks
|
|
25
|
+
* @property {number} references_available - total retrieved references
|
|
26
|
+
* @property {number} references_used - references found in artifact
|
|
27
|
+
* @property {number} references_missed - available but not used
|
|
28
|
+
* @property {number} unsupported_references - artifact cited something not in bundle
|
|
29
|
+
* @property {number} reference_use_ratio - references_used / references_available
|
|
30
|
+
*
|
|
31
|
+
* @property {PostureComplianceCheck} posture_compliance
|
|
32
|
+
* @property {DistinctivenessSignal[]} distinctiveness_signals
|
|
33
|
+
* @property {DriftViolation[]} drift_violations
|
|
34
|
+
*
|
|
35
|
+
* @property {string} verdict - pass | warn | fail
|
|
36
|
+
* @property {string[]} reasons
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} RetrievedReferenceCheck
|
|
41
|
+
* @property {string} reference - citation reference from bundle
|
|
42
|
+
* @property {string} chunk_id
|
|
43
|
+
* @property {boolean} found_in_artifact
|
|
44
|
+
* @property {string[]} match_locations - where in artifact it was found
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} PostureComplianceCheck
|
|
49
|
+
* @property {string} status - strong | weak | stale | conflicted | none
|
|
50
|
+
* @property {boolean} compliant
|
|
51
|
+
* @property {string[]} expected_signals - what should appear given posture
|
|
52
|
+
* @property {string[]} found_signals - what actually appeared
|
|
53
|
+
* @property {string[]} missing_signals - expected but absent
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} DistinctivenessSignal
|
|
58
|
+
* @property {string} signal - what was checked
|
|
59
|
+
* @property {boolean} present
|
|
60
|
+
* @property {string} [evidence] - where in artifact
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} DriftViolation
|
|
65
|
+
* @property {string} violation - what was found
|
|
66
|
+
* @property {string} evidence - the offending text
|
|
67
|
+
* @property {string} rule - which anti-goal was violated
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
// ── Posture Signal Expectations ─────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const POSTURE_SIGNALS = {
|
|
73
|
+
strong: {
|
|
74
|
+
// Strong posture: artifact should reference sources and make definitive claims
|
|
75
|
+
expected: ["per ", "according to", "documented in", "as identified", "findings", "recommendation"],
|
|
76
|
+
banned: ["insufficient evidence", "unable to assess", "lack sufficient data"],
|
|
77
|
+
},
|
|
78
|
+
weak: {
|
|
79
|
+
expected: ["limited evidence", "partial", "insufficient", "uncertain", "preliminary"],
|
|
80
|
+
banned: [],
|
|
81
|
+
},
|
|
82
|
+
stale: {
|
|
83
|
+
expected: ["outdated", "stale", "may no longer", "as of", "dated", "older"],
|
|
84
|
+
banned: [],
|
|
85
|
+
},
|
|
86
|
+
conflicted: {
|
|
87
|
+
expected: ["disagree", "conflict", "contradict", "contested", "divergent"],
|
|
88
|
+
banned: ["all sources agree", "clear answer"],
|
|
89
|
+
// Note: "consensus" alone is too broad — "false consensus" is actually correct behavior.
|
|
90
|
+
// Only ban unqualified consensus claims.
|
|
91
|
+
},
|
|
92
|
+
none: {
|
|
93
|
+
expected: [],
|
|
94
|
+
banned: ["retrieved evidence", "according to retrieved", "knowledge base"],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ── Role Distinctiveness Profiles ───────────────────────────────────
|
|
99
|
+
|
|
100
|
+
const ROLE_PROFILES = {
|
|
101
|
+
"product-strategist": {
|
|
102
|
+
expected_signals: [
|
|
103
|
+
"user value", "tradeoff", "scope", "adoption", "leverage",
|
|
104
|
+
"opportunity cost", "strategic", "outcome", "positioning",
|
|
105
|
+
],
|
|
106
|
+
anti_signals: [
|
|
107
|
+
"CVE", "exploit", "injection", "threat model",
|
|
108
|
+
"handbook structure", "navigation pattern",
|
|
109
|
+
"verdict", "accept", "reject",
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
"security-reviewer": {
|
|
113
|
+
expected_signals: [
|
|
114
|
+
"vulnerability", "threat", "exploit", "attack", "risk",
|
|
115
|
+
"mitigation", "access control", "injection", "auth",
|
|
116
|
+
"security", "trust boundary",
|
|
117
|
+
],
|
|
118
|
+
anti_signals: [
|
|
119
|
+
"market share", "competitor", "pricing",
|
|
120
|
+
"user value", "adoption friction",
|
|
121
|
+
"handbook", "progressive disclosure",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
"competitive-analyst": {
|
|
125
|
+
expected_signals: [
|
|
126
|
+
"competitor", "market", "differentiation", "pricing",
|
|
127
|
+
"substitute", "switching cost", "moat", "disadvantage",
|
|
128
|
+
"landscape", "ecosystem",
|
|
129
|
+
],
|
|
130
|
+
anti_signals: [
|
|
131
|
+
"CVE", "exploit", "injection",
|
|
132
|
+
"handbook", "navigation",
|
|
133
|
+
"verdict", "reject", "quality bar",
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
"docs-architect": {
|
|
137
|
+
expected_signals: [
|
|
138
|
+
"structure", "navigation", "hierarchy", "section",
|
|
139
|
+
"handbook", "documentation", "getting started",
|
|
140
|
+
"progressive disclosure", "cross-reference",
|
|
141
|
+
],
|
|
142
|
+
anti_signals: [
|
|
143
|
+
"CVE", "exploit", "threat model",
|
|
144
|
+
"market share", "competitor", "pricing",
|
|
145
|
+
"verdict", "reject",
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
"critic-reviewer": {
|
|
149
|
+
expected_signals: [
|
|
150
|
+
"verdict", "accept", "reject", "quality bar",
|
|
151
|
+
"evidence", "contract", "completeness",
|
|
152
|
+
"precedent", "failure pattern", "drift",
|
|
153
|
+
],
|
|
154
|
+
anti_signals: [
|
|
155
|
+
"market share", "competitor",
|
|
156
|
+
"handbook structure",
|
|
157
|
+
"exploit", "CVE",
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ── Main Analyzer ───────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Analyze an artifact against its retrieval bundle for evidence use.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} options
|
|
168
|
+
* @param {string} options.role_id
|
|
169
|
+
* @param {string} options.task_id
|
|
170
|
+
* @param {string} options.artifact_text - the role's output artifact
|
|
171
|
+
* @param {Object|null} options.packet_knowledge - packet.knowledge
|
|
172
|
+
* @param {string[]} [options.known_external_refs] - references not in bundle that are still valid
|
|
173
|
+
* @returns {ArtifactEvidenceReport}
|
|
174
|
+
*/
|
|
175
|
+
export function analyzeArtifactEvidence({
|
|
176
|
+
role_id,
|
|
177
|
+
task_id,
|
|
178
|
+
artifact_text,
|
|
179
|
+
packet_knowledge,
|
|
180
|
+
known_external_refs = [],
|
|
181
|
+
}) {
|
|
182
|
+
const reasons = [];
|
|
183
|
+
const artifactLower = artifact_text.toLowerCase();
|
|
184
|
+
const status = packet_knowledge?.status ?? "none";
|
|
185
|
+
const bundle = packet_knowledge?.retrieval_bundle;
|
|
186
|
+
|
|
187
|
+
// ── Reference Checks ────────────────────────────────────────────
|
|
188
|
+
const referenceChecks = [];
|
|
189
|
+
let refsAvailable = 0;
|
|
190
|
+
let refsUsed = 0;
|
|
191
|
+
|
|
192
|
+
if (bundle?.selected) {
|
|
193
|
+
refsAvailable = bundle.selected.length;
|
|
194
|
+
|
|
195
|
+
for (const chunk of bundle.selected) {
|
|
196
|
+
const ref = chunk.citation?.reference ?? chunk.chunk_id;
|
|
197
|
+
const refLower = ref.toLowerCase();
|
|
198
|
+
|
|
199
|
+
// Check for exact reference, partial title match, or chunk content echo
|
|
200
|
+
const locations = [];
|
|
201
|
+
if (artifactLower.includes(refLower)) {
|
|
202
|
+
locations.push("exact-reference");
|
|
203
|
+
}
|
|
204
|
+
// Check for title fragment
|
|
205
|
+
if (chunk.title && artifactLower.includes(chunk.title.toLowerCase())) {
|
|
206
|
+
locations.push("title-match");
|
|
207
|
+
}
|
|
208
|
+
// Check for source ID reference
|
|
209
|
+
if (artifactLower.includes(chunk.source_id.toLowerCase())) {
|
|
210
|
+
locations.push("source-id");
|
|
211
|
+
}
|
|
212
|
+
// Check for key content phrases (first 50 chars of content)
|
|
213
|
+
const contentSnippet = chunk.content.slice(0, 50).toLowerCase();
|
|
214
|
+
if (contentSnippet.length > 20 && artifactLower.includes(contentSnippet)) {
|
|
215
|
+
locations.push("content-echo");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const found = locations.length > 0;
|
|
219
|
+
if (found) refsUsed++;
|
|
220
|
+
|
|
221
|
+
referenceChecks.push({
|
|
222
|
+
reference: ref,
|
|
223
|
+
chunk_id: chunk.chunk_id,
|
|
224
|
+
found_in_artifact: found,
|
|
225
|
+
match_locations: locations,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const refsMissed = refsAvailable - refsUsed;
|
|
231
|
+
const refUseRatio = refsAvailable > 0 ? refsUsed / refsAvailable : 0;
|
|
232
|
+
|
|
233
|
+
// ── Unsupported Reference Detection ─────────────────────────────
|
|
234
|
+
// Look for citation-like patterns that don't match any retrieved reference
|
|
235
|
+
const citationPatterns = extractCitationPatterns(artifact_text);
|
|
236
|
+
const knownRefs = new Set([
|
|
237
|
+
...(bundle?.selected?.map((c) => (c.citation?.reference ?? c.chunk_id).toLowerCase()) ?? []),
|
|
238
|
+
...(bundle?.selected?.map((c) => c.title?.toLowerCase()).filter(Boolean) ?? []),
|
|
239
|
+
...(bundle?.selected?.map((c) => c.source_id.toLowerCase()) ?? []),
|
|
240
|
+
...known_external_refs.map((r) => r.toLowerCase()),
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
const unsupportedRefs = citationPatterns.filter(
|
|
244
|
+
(p) => !knownRefs.has(p.toLowerCase()) && !isGenericPhrase(p)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ── Posture Compliance ──────────────────────────────────────────
|
|
248
|
+
const postureCompliance = checkPostureCompliance(artifactLower, status);
|
|
249
|
+
|
|
250
|
+
// ── Distinctiveness Signals ─────────────────────────────────────
|
|
251
|
+
const distinctivenessSignals = checkDistinctiveness(artifactLower, role_id);
|
|
252
|
+
|
|
253
|
+
// ── Drift Violations ────────────────────────────────────────────
|
|
254
|
+
const driftViolations = checkDrift(artifactLower, role_id);
|
|
255
|
+
|
|
256
|
+
// ── Verdict ─────────────────────────────────────────────────────
|
|
257
|
+
let verdict = "pass";
|
|
258
|
+
|
|
259
|
+
if (status !== "none" && refsAvailable > 0 && refUseRatio < 0.1) {
|
|
260
|
+
verdict = "fail";
|
|
261
|
+
reasons.push(`Retrieved evidence mostly unused (${refsUsed}/${refsAvailable} = ${(refUseRatio * 100).toFixed(0)}%)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (unsupportedRefs.length > 2) {
|
|
265
|
+
verdict = verdict === "fail" ? "fail" : "warn";
|
|
266
|
+
reasons.push(`${unsupportedRefs.length} unsupported references in artifact`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!postureCompliance.compliant) {
|
|
270
|
+
verdict = verdict === "fail" ? "fail" : "warn";
|
|
271
|
+
reasons.push(`Posture compliance failed: missing ${postureCompliance.missing_signals.join(", ")}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (driftViolations.length > 0) {
|
|
275
|
+
verdict = "fail";
|
|
276
|
+
reasons.push(`${driftViolations.length} drift violation(s): ${driftViolations.map((d) => d.violation).join(", ")}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const expectedSignalsHit = distinctivenessSignals.filter((s) => s.present).length;
|
|
280
|
+
const totalExpected = distinctivenessSignals.length;
|
|
281
|
+
if (totalExpected > 0 && expectedSignalsHit / totalExpected < 0.3) {
|
|
282
|
+
verdict = verdict === "fail" ? "fail" : "warn";
|
|
283
|
+
reasons.push(`Low distinctiveness: only ${expectedSignalsHit}/${totalExpected} expected signals found`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (verdict === "pass") {
|
|
287
|
+
reasons.push("Evidence chain intact, posture compliant, role-distinct");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
role_id,
|
|
292
|
+
task_id,
|
|
293
|
+
knowledge_status: status,
|
|
294
|
+
reference_checks: referenceChecks,
|
|
295
|
+
references_available: refsAvailable,
|
|
296
|
+
references_used: refsUsed,
|
|
297
|
+
references_missed: refsMissed,
|
|
298
|
+
unsupported_references: unsupportedRefs.length,
|
|
299
|
+
reference_use_ratio: refUseRatio,
|
|
300
|
+
posture_compliance: postureCompliance,
|
|
301
|
+
distinctiveness_signals: distinctivenessSignals,
|
|
302
|
+
drift_violations: driftViolations,
|
|
303
|
+
verdict,
|
|
304
|
+
reasons,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Posture Compliance Checker ──────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
function checkPostureCompliance(artifactLower, status) {
|
|
311
|
+
const profile = POSTURE_SIGNALS[status];
|
|
312
|
+
if (!profile) {
|
|
313
|
+
return { status, compliant: true, expected_signals: [], found_signals: [], missing_signals: [] };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const found = [];
|
|
317
|
+
const missing = [];
|
|
318
|
+
|
|
319
|
+
for (const signal of profile.expected) {
|
|
320
|
+
if (artifactLower.includes(signal.toLowerCase())) {
|
|
321
|
+
found.push(signal);
|
|
322
|
+
} else {
|
|
323
|
+
missing.push(signal);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// For posture compliance, finding at least one expected signal is enough
|
|
328
|
+
// (except "none" which has no expected signals)
|
|
329
|
+
const compliant = profile.expected.length === 0 || found.length > 0;
|
|
330
|
+
|
|
331
|
+
// Check banned phrases
|
|
332
|
+
const bannedFound = [];
|
|
333
|
+
for (const banned of profile.banned) {
|
|
334
|
+
if (artifactLower.includes(banned.toLowerCase())) {
|
|
335
|
+
bannedFound.push(banned);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
status,
|
|
341
|
+
compliant: compliant && bannedFound.length === 0,
|
|
342
|
+
expected_signals: profile.expected,
|
|
343
|
+
found_signals: found,
|
|
344
|
+
missing_signals: missing.length > 0 && found.length === 0 ? missing : [],
|
|
345
|
+
banned_violations: bannedFound,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Distinctiveness Checker ─────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
function checkDistinctiveness(artifactLower, roleId) {
|
|
352
|
+
const profile = ROLE_PROFILES[roleId];
|
|
353
|
+
if (!profile) return [];
|
|
354
|
+
|
|
355
|
+
return profile.expected_signals.map((signal) => ({
|
|
356
|
+
signal,
|
|
357
|
+
present: artifactLower.includes(signal.toLowerCase()),
|
|
358
|
+
evidence: artifactLower.includes(signal.toLowerCase())
|
|
359
|
+
? `Contains "${signal}"`
|
|
360
|
+
: undefined,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Drift Checker ───────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
function checkDrift(artifactLower, roleId) {
|
|
367
|
+
const profile = ROLE_PROFILES[roleId];
|
|
368
|
+
if (!profile) return [];
|
|
369
|
+
|
|
370
|
+
const violations = [];
|
|
371
|
+
for (const antiSignal of profile.anti_signals) {
|
|
372
|
+
if (artifactLower.includes(antiSignal.toLowerCase())) {
|
|
373
|
+
violations.push({
|
|
374
|
+
violation: `Contains "${antiSignal}" — outside role mandate`,
|
|
375
|
+
evidence: antiSignal,
|
|
376
|
+
rule: `anti_signal: ${antiSignal}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return violations;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Extract citation-like patterns from artifact text.
|
|
388
|
+
* Looks for: [brackets], "Source: ...", "Reference: ...", "per ...", "according to ..."
|
|
389
|
+
*/
|
|
390
|
+
function extractCitationPatterns(text) {
|
|
391
|
+
const patterns = [];
|
|
392
|
+
|
|
393
|
+
// Bracketed references: [Something]
|
|
394
|
+
const bracketMatches = text.match(/\[([^\]]{5,80})\]/g) ?? [];
|
|
395
|
+
for (const m of bracketMatches) {
|
|
396
|
+
patterns.push(m.slice(1, -1));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// "Source: X" or "Reference: X"
|
|
400
|
+
const sourceMatches = text.match(/(?:source|reference|per|according to):?\s+([^\n.]{5,80})/gi) ?? [];
|
|
401
|
+
for (const m of sourceMatches) {
|
|
402
|
+
const cleaned = m.replace(/^(?:source|reference|per|according to):?\s+/i, "").trim();
|
|
403
|
+
if (cleaned) patterns.push(cleaned);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return patterns;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Filter out generic phrases that look like citations but aren't.
|
|
411
|
+
*/
|
|
412
|
+
function isGenericPhrase(text) {
|
|
413
|
+
const generics = [
|
|
414
|
+
"see above", "see below", "as noted", "as mentioned", "emphasis added",
|
|
415
|
+
"bold mine", "note", "important", "warning", "example",
|
|
416
|
+
"preferred", "authoritative", "fresh", "stale", "aging",
|
|
417
|
+
];
|
|
418
|
+
const lower = text.toLowerCase();
|
|
419
|
+
return generics.some((g) => lower.includes(g)) || text.length < 8;
|
|
420
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach retrieval provenance to Role OS evidence items.
|
|
3
|
+
*
|
|
4
|
+
* When a role cites retrieved knowledge in its output, this wires
|
|
5
|
+
* the citation back to the retrieval bundle for full traceability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create an evidence provenance record from a retrieved chunk.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} chunk - A RetrievedChunk from the bundle
|
|
12
|
+
* @returns {Object} EvidenceProvenance
|
|
13
|
+
*/
|
|
14
|
+
export function provenanceFromChunk(chunk) {
|
|
15
|
+
return {
|
|
16
|
+
source_id: chunk.source_id,
|
|
17
|
+
document_id: chunk.document_id,
|
|
18
|
+
chunk_id: chunk.chunk_id,
|
|
19
|
+
trust_tier: chunk.metadata?.trust_tier ?? "general",
|
|
20
|
+
freshness_status: chunk.metadata?.freshness?.status ?? "undated",
|
|
21
|
+
citation_reference: chunk.citation?.reference ?? `chunk:${chunk.chunk_id}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Attach provenance to an existing evidence item.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} evidenceItem - Role OS evidence entry (mutable)
|
|
29
|
+
* @param {Object} chunk - The RetrievedChunk that backs this evidence
|
|
30
|
+
* @returns {Object} The mutated evidence item
|
|
31
|
+
*/
|
|
32
|
+
export function attachProvenance(evidenceItem, chunk) {
|
|
33
|
+
evidenceItem.provenance = provenanceFromChunk(chunk);
|
|
34
|
+
return evidenceItem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bulk-attach provenance to evidence items that match retrieved chunks by reference.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object[]} evidenceItems - Array of evidence entries
|
|
41
|
+
* @param {Object} bundle - RetrievalBundle
|
|
42
|
+
* @returns {Object[]} Updated evidence items (mutated in place)
|
|
43
|
+
*/
|
|
44
|
+
export function reconcileEvidenceWithBundle(evidenceItems, bundle) {
|
|
45
|
+
if (!bundle?.selected?.length) return evidenceItems;
|
|
46
|
+
|
|
47
|
+
// Build lookup by citation reference
|
|
48
|
+
const chunkByRef = new Map();
|
|
49
|
+
for (const chunk of bundle.selected) {
|
|
50
|
+
if (chunk.citation?.reference) {
|
|
51
|
+
chunkByRef.set(chunk.citation.reference, chunk);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const item of evidenceItems) {
|
|
56
|
+
if (item.reference && chunkByRef.has(item.reference)) {
|
|
57
|
+
attachProvenance(item, chunkByRef.get(item.reference));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return evidenceItems;
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach a retrieval bundle to a Role OS packet.
|
|
3
|
+
*
|
|
4
|
+
* Adds packet.knowledge with the bundle and its status.
|
|
5
|
+
* The prompt builder consumes this — retrieval never touches prompts directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attach retrieval results to a packet object.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} packet - Role OS packet (mutable)
|
|
12
|
+
* @param {Object} bundle - RetrievalBundle from retrieveForDispatch
|
|
13
|
+
* @param {string} status - Knowledge status: strong | weak | stale | conflicted | none
|
|
14
|
+
* @returns {Object} The mutated packet (for chaining)
|
|
15
|
+
*/
|
|
16
|
+
export function attachBundleToPacket(packet, bundle, status) {
|
|
17
|
+
packet.knowledge = {
|
|
18
|
+
retrieval_bundle: bundle,
|
|
19
|
+
status,
|
|
20
|
+
};
|
|
21
|
+
return packet;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if a packet has knowledge attached.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} packet
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export function hasKnowledge(packet) {
|
|
31
|
+
return packet.knowledge != null && packet.knowledge.status !== "none";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the knowledge status from a packet.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} packet
|
|
38
|
+
* @returns {string} One of: strong | weak | stale | conflicted | none
|
|
39
|
+
*/
|
|
40
|
+
export function getKnowledgeStatus(packet) {
|
|
41
|
+
return packet.knowledge?.status ?? "none";
|
|
42
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback policy for degraded retrieval scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Explicit governance — no silent garbage.
|
|
5
|
+
* Every degraded state has a named action and a message.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Evaluate the fallback state for a retrieval bundle.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} bundle - RetrievalBundle
|
|
12
|
+
* @param {Object|null} overlay - RoleOverlay or null
|
|
13
|
+
* @returns {{ state: string, action: string, message: string }}
|
|
14
|
+
*/
|
|
15
|
+
export function applyFallbackPolicy(bundle, overlay) {
|
|
16
|
+
// No overlay → shared corpus only
|
|
17
|
+
if (!overlay) {
|
|
18
|
+
return {
|
|
19
|
+
state: "no_overlay",
|
|
20
|
+
action: "continue",
|
|
21
|
+
message: `No overlay for role ${bundle.role_id} — using shared corpus only`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// No selected chunks at all
|
|
26
|
+
if (bundle.selected.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
state: "no_strong_match",
|
|
29
|
+
action: "warn",
|
|
30
|
+
message: "No chunks selected — bundle is empty",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for forbidden source hits
|
|
35
|
+
if (bundle.summary.forbidden_hits > 0) {
|
|
36
|
+
// Forbidden sources were removed, but log the diagnostic
|
|
37
|
+
return {
|
|
38
|
+
state: "forbidden_hit",
|
|
39
|
+
action: "continue",
|
|
40
|
+
message: `${bundle.summary.forbidden_hits} forbidden source(s) removed from results`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for stale-dominant results
|
|
45
|
+
const totalRelevant = bundle.summary.selected_count + bundle.summary.stale_count;
|
|
46
|
+
if (totalRelevant > 0 && bundle.summary.stale_count / totalRelevant > 0.5) {
|
|
47
|
+
return {
|
|
48
|
+
state: "stale_dominant",
|
|
49
|
+
action: "warn",
|
|
50
|
+
message: `${bundle.summary.stale_count} of ${totalRelevant} relevant candidates are stale`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for conflicting evidence
|
|
55
|
+
const conflictWarning = bundle.warnings?.find((w) => w.code === "CONFLICTING_EVIDENCE");
|
|
56
|
+
if (conflictWarning) {
|
|
57
|
+
return {
|
|
58
|
+
state: "conflicting",
|
|
59
|
+
action: "warn",
|
|
60
|
+
message: conflictWarning.message,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for weak trust posture
|
|
65
|
+
if (bundle.provenance.trust_posture === "weak") {
|
|
66
|
+
return {
|
|
67
|
+
state: "no_strong_match",
|
|
68
|
+
action: "warn",
|
|
69
|
+
message: "All selected chunks are low-trust — bundle marked weak",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Healthy
|
|
74
|
+
return {
|
|
75
|
+
state: "healthy",
|
|
76
|
+
action: "continue",
|
|
77
|
+
message: "Retrieval healthy",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role OS Knowledge Integration Layer
|
|
3
|
+
*
|
|
4
|
+
* Wires knowledge-core retrieval into Role OS dispatch, packet, and evidence systems.
|
|
5
|
+
* Phase 2: live retrieval via configureKnowledge().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { resolveOverlay, hasOverlay } from "./resolve-overlay.mjs";
|
|
9
|
+
export { retrieveForDispatch, configureKnowledge, isKnowledgeConfigured } from "./retrieve-for-dispatch.mjs";
|
|
10
|
+
export { attachBundleToPacket, hasKnowledge, getKnowledgeStatus } from "./attach-bundle-to-packet.mjs";
|
|
11
|
+
export { provenanceFromChunk, attachProvenance, reconcileEvidenceWithBundle } from "./attach-bundle-to-evidence.mjs";
|
|
12
|
+
export { applyFallbackPolicy } from "./fallback-policy.mjs";
|
|
13
|
+
export { renderKnowledgeBlock, knowledgeManifestSummary } from "./render-knowledge-block.mjs";
|
|
14
|
+
export { analyzeArtifactEvidence } from "./analyze-artifact-evidence.mjs";
|