instar 1.3.890 → 1.3.892
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/dashboard/index.html +35 -2
- package/dashboard/preferences-learning.js +2 -2
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +100 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +12 -1
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +1 -5
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +33 -7
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +8 -2
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/AutonomousThroughputFloor.d.ts +118 -0
- package/dist/monitoring/AutonomousThroughputFloor.d.ts.map +1 -0
- package/dist/monitoring/AutonomousThroughputFloor.js +147 -0
- package/dist/monitoring/AutonomousThroughputFloor.js.map +1 -0
- package/dist/monitoring/ClaimClauseArbiter.d.ts +11 -2
- package/dist/monitoring/ClaimClauseArbiter.d.ts.map +1 -1
- package/dist/monitoring/ClaimClauseArbiter.js +55 -9
- package/dist/monitoring/ClaimClauseArbiter.js.map +1 -1
- package/dist/monitoring/ClaimObservation.d.ts +181 -0
- package/dist/monitoring/ClaimObservation.d.ts.map +1 -0
- package/dist/monitoring/ClaimObservation.js +575 -0
- package/dist/monitoring/ClaimObservation.js.map +1 -0
- package/dist/monitoring/ClaimObservationAdmissionQueue.d.ts +38 -0
- package/dist/monitoring/ClaimObservationAdmissionQueue.d.ts.map +1 -0
- package/dist/monitoring/ClaimObservationAdmissionQueue.js +90 -0
- package/dist/monitoring/ClaimObservationAdmissionQueue.js.map +1 -0
- package/dist/monitoring/CompletionClaimVerifier.d.ts +45 -4
- package/dist/monitoring/CompletionClaimVerifier.d.ts.map +1 -1
- package/dist/monitoring/CompletionClaimVerifier.js +74 -29
- package/dist/monitoring/CompletionClaimVerifier.js.map +1 -1
- package/dist/monitoring/LlmQueue.d.ts +31 -0
- package/dist/monitoring/LlmQueue.d.ts.map +1 -1
- package/dist/monitoring/LlmQueue.js +41 -0
- package/dist/monitoring/LlmQueue.js.map +1 -1
- package/dist/monitoring/TurnEvidence.d.ts.map +1 -1
- package/dist/monitoring/TurnEvidence.js +12 -4
- package/dist/monitoring/TurnEvidence.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +5 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +83 -2
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -1
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/fileRoutes.d.ts.map +1 -1
- package/dist/server/fileRoutes.js +2 -0
- package/dist/server/fileRoutes.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +93 -21
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/data/state-coherence-registry.json +18 -0
- package/src/scaffold/templates.ts +5 -1
- package/upgrades/1.3.891.md +30 -0
- package/upgrades/1.3.892.md +20 -0
- package/upgrades/side-effects/autonomous-throughput-floor.md +36 -0
- package/upgrades/side-effects/claim-verification-sentinel.md +93 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, createHmac, randomUUID } from 'node:crypto';
|
|
4
|
+
import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
|
|
5
|
+
const KINDS = new Set(['temporal', 'capacity-limit', 'completion', 'cross-agent-action', 'operator-attribution', 'state-fact', 'external-fact', 'unknown']);
|
|
6
|
+
const SUBJECTS = new Set(['session', 'commitment', 'guard', 'pull-request', 'tool-action', 'capacity-model', 'operator', 'external-entity', 'unknown']);
|
|
7
|
+
const PREDICATES = new Set(['session.elapsed-ms', 'session.state', 'commitment.state', 'guard.state', 'pull-request.merged', 'pull-request.checks-pass', 'tool-action.completed', 'capacity.limit', 'operator.attributed', 'external.fact', 'unknown']);
|
|
8
|
+
const COMPARATORS = new Set(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);
|
|
9
|
+
const CRITICALITIES = new Set(['low', 'medium', 'high', 'irreversible-precondition']);
|
|
10
|
+
const TENSES = new Set(['current', 'past', 'future', 'timeless', 'unknown']);
|
|
11
|
+
const IRREVERSIBLE = new Set(['delete', 'production-deploy', 'merge', 'publish', 'external-send', 'credential-change', 'other']);
|
|
12
|
+
const PROTECTED = {
|
|
13
|
+
capacity: /\b(?:capacity|cap(?:ped)?|limit|lane|slot|concurr\w*)\b/i,
|
|
14
|
+
completion: /\b(?:done|complete\w*|finish\w*|pass(?:ed)?|green|merge\w*|deploy\w*|ship\w*|fix(?:ed)?)\b/i,
|
|
15
|
+
attribution: /\b(?:approv\w*|authoriz\w*|credential|permission)\b/i,
|
|
16
|
+
action: /\b(?:delete|remove|publish|send|restart|transfer)\b/i,
|
|
17
|
+
state: /\b(?:running|stopped|open|closed|pending|active)\b/i,
|
|
18
|
+
injection: /(?:ignore previous|ignore above|system prompt|developer message|<system|do not extract)/i,
|
|
19
|
+
};
|
|
20
|
+
export function buildClaimCandidates(message) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const bytes = Buffer.from(message, 'utf8');
|
|
23
|
+
let start = 0;
|
|
24
|
+
for (let i = 0; i < bytes.length && out.length < 24; i++) {
|
|
25
|
+
const ch = bytes[i];
|
|
26
|
+
if (ch !== 10 && ch !== 59 && ch !== 33 && ch !== 63 && ch !== 46)
|
|
27
|
+
continue;
|
|
28
|
+
pushCandidate(bytes, start, i + 1, out);
|
|
29
|
+
start = i + 1;
|
|
30
|
+
}
|
|
31
|
+
if (start < bytes.length)
|
|
32
|
+
pushCandidate(bytes, start, bytes.length, out);
|
|
33
|
+
if (out.length === 24 && out[23].sourceEndByte < bytes.length) {
|
|
34
|
+
out[23] = { ...out[23], text: bytes.subarray(out[23].sourceStartByte).toString('utf8').trim(), sourceEndByte: bytes.length };
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function pushCandidate(bytes, start, end, out) {
|
|
39
|
+
const raw = bytes.subarray(start, end).toString('utf8');
|
|
40
|
+
const left = raw.length - raw.trimStart().length;
|
|
41
|
+
const right = raw.trimEnd().length;
|
|
42
|
+
const text = raw.trim();
|
|
43
|
+
if (!text || out.length >= 24)
|
|
44
|
+
return;
|
|
45
|
+
out.push({ clauseId: out.length, text, sourceStartByte: start + Buffer.byteLength(raw.slice(0, left)), sourceEndByte: start + Buffer.byteLength(raw.slice(0, right)) });
|
|
46
|
+
}
|
|
47
|
+
export function prepareClaimObservation(message, evidence) {
|
|
48
|
+
if (Buffer.byteLength(message, 'utf8') > 32_768)
|
|
49
|
+
return denied('deny');
|
|
50
|
+
let serial = 0;
|
|
51
|
+
const placeholder = (kind) => `[REDACTED_${kind}_${String(++serial).padStart(2, '0')}]`;
|
|
52
|
+
const redact = (value) => value
|
|
53
|
+
.replace(/(?:gh[pousr]_[A-Za-z0-9]{20,}|\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|\b(?:AKIA|ASIA)[A-Z0-9]{16}\b|\b\d{6,12}:[A-Za-z0-9_-]{30,}\b|\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{20,}\b)/gi, () => placeholder('SECRET'))
|
|
54
|
+
.replace(/https?:\/\/[^\s)\]}]+/gi, () => placeholder('URL'))
|
|
55
|
+
.replace(/(?:^|\s)(?:\/[A-Za-z0-9._-]+){2,}/g, (match) => `${match.startsWith(' ') ? ' ' : ''}${placeholder('PATH')}`)
|
|
56
|
+
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, () => placeholder('IDENTITY'));
|
|
57
|
+
const scrubbed = redact(message);
|
|
58
|
+
if (Buffer.byteLength(scrubbed, 'utf8') > 16_384)
|
|
59
|
+
return denied('deny');
|
|
60
|
+
const safeEvidence = {
|
|
61
|
+
hadToolCalls: evidence.hadToolCalls,
|
|
62
|
+
truncated: evidence.truncated,
|
|
63
|
+
unavailable: evidence.unavailable,
|
|
64
|
+
canaryOk: evidence.canaryOk,
|
|
65
|
+
...(evidence.reason ? { reason: redact(evidence.reason).slice(0, 256) } : {}),
|
|
66
|
+
toolCalls: evidence.toolCalls.slice(0, 200).map((item) => ({
|
|
67
|
+
tool: redact(item.tool).slice(0, 100), actionKind: item.actionKind, ok: item.ok,
|
|
68
|
+
...(item.targetSummary ? { targetSummary: redact(item.targetSummary).slice(0, 256) } : {}),
|
|
69
|
+
...(item.errorClass ? { errorClass: redact(item.errorClass).slice(0, 100) } : {}),
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
return { policy: 'standard-scrubbed', message: scrubbed, evidence: safeEvidence,
|
|
73
|
+
candidates: buildClaimCandidates(scrubbed), scrubVersion: 'claim-scrub-v1', privacyPolicyVersion: 'claim-content-v1' };
|
|
74
|
+
}
|
|
75
|
+
function denied(policy) {
|
|
76
|
+
return { policy, message: '', evidence: { hadToolCalls: false, toolCalls: [], truncated: false, unavailable: true, canaryOk: true, reason: 'privacy-boundary' }, candidates: [], scrubVersion: 'claim-scrub-v1', privacyPolicyVersion: 'claim-content-v1' };
|
|
77
|
+
}
|
|
78
|
+
export function parseGeneralClaimEnvelope(raw, message) {
|
|
79
|
+
try {
|
|
80
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
81
|
+
const parsed = JSON.parse(match?.[0] ?? raw);
|
|
82
|
+
const general = parsed.general && typeof parsed.general === 'object' ? parsed.general : parsed;
|
|
83
|
+
if (!onlyKeys(general, ['schemaVersion', 'claims']) || general.schemaVersion !== 1
|
|
84
|
+
|| !Array.isArray(general.claims) || general.claims.length > 4)
|
|
85
|
+
return null;
|
|
86
|
+
const claims = general.claims.map((value) => validateClaim(value, message));
|
|
87
|
+
if (claims.some((claim) => claim === null))
|
|
88
|
+
return null;
|
|
89
|
+
const unique = [];
|
|
90
|
+
const exact = new Set();
|
|
91
|
+
const spanPredicate = new Map();
|
|
92
|
+
for (const claim of claims) {
|
|
93
|
+
const tuple = `${claim.clauseId}|${claim.sourceStartByte}|${claim.sourceEndByte}|${claim.predicate}|${claim.comparator}|${JSON.stringify(claim.operand)}`;
|
|
94
|
+
if (exact.has(tuple))
|
|
95
|
+
continue;
|
|
96
|
+
const conflictKey = `${claim.clauseId}|${claim.sourceStartByte}|${claim.sourceEndByte}|${claim.predicate}`;
|
|
97
|
+
const prior = spanPredicate.get(conflictKey);
|
|
98
|
+
if (prior && prior !== tuple)
|
|
99
|
+
return null;
|
|
100
|
+
exact.add(tuple);
|
|
101
|
+
spanPredicate.set(conflictKey, tuple);
|
|
102
|
+
unique.push(claim);
|
|
103
|
+
}
|
|
104
|
+
return { schemaVersion: 1, claims: unique, saturated: general.claims.length === 4 };
|
|
105
|
+
}
|
|
106
|
+
catch { /* @silent-fallback-ok: malformed model output is non-authoritative */
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function validateClaim(value, message) {
|
|
111
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
112
|
+
return null;
|
|
113
|
+
const v = value;
|
|
114
|
+
if (!onlyKeys(v, ['clauseId', 'kind', 'subjectKind', 'predicate', 'operand', 'comparator', 'subjectSelector',
|
|
115
|
+
'consequence', 'sourceStartByte', 'sourceEndByte', 'referencedEntityHints', 'endorsed', 'negated',
|
|
116
|
+
'hedged', 'quoted', 'suggestedCriticality', 'confidence', 'tenseScope']))
|
|
117
|
+
return null;
|
|
118
|
+
const byteLength = Buffer.byteLength(message, 'utf8');
|
|
119
|
+
if (!Number.isSafeInteger(v.clauseId) || !KINDS.has(v.kind) || !SUBJECTS.has(v.subjectKind)
|
|
120
|
+
|| !PREDICATES.has(v.predicate) || !COMPARATORS.has(v.comparator)
|
|
121
|
+
|| !CRITICALITIES.has(v.suggestedCriticality) || !TENSES.has(v.tenseScope)
|
|
122
|
+
|| !Number.isInteger(v.sourceStartByte) || !Number.isInteger(v.sourceEndByte)
|
|
123
|
+
|| Number(v.sourceStartByte) < 0 || Number(v.sourceEndByte) <= Number(v.sourceStartByte) || Number(v.sourceEndByte) > byteLength
|
|
124
|
+
|| typeof v.endorsed !== 'boolean' || typeof v.negated !== 'boolean' || typeof v.hedged !== 'boolean' || typeof v.quoted !== 'boolean'
|
|
125
|
+
|| typeof v.confidence !== 'number' || v.confidence < 0 || v.confidence > 1
|
|
126
|
+
|| !Array.isArray(v.referencedEntityHints) || v.referencedEntityHints.length > 4
|
|
127
|
+
|| v.referencedEntityHints.some((x) => typeof x !== 'string' || Buffer.byteLength(x) > 200)
|
|
128
|
+
|| !validOperand(v.operand) || !validSelector(v.subjectSelector) || !validConsequence(v.consequence, byteLength))
|
|
129
|
+
return null;
|
|
130
|
+
const consequence = v.consequence;
|
|
131
|
+
const operand = v.operand;
|
|
132
|
+
if ((operand.type === 'boolean' || operand.type === 'state-enum' || operand.type === 'none')
|
|
133
|
+
&& v.comparator !== 'eq' && v.comparator !== 'ne')
|
|
134
|
+
return null;
|
|
135
|
+
if (consequence.relation === 'premise-for' && consequence.actionStartByte !== undefined && consequence.actionEndByte !== undefined
|
|
136
|
+
&& consequence.actionStartByte < Number(v.sourceEndByte) && consequence.actionEndByte > Number(v.sourceStartByte))
|
|
137
|
+
return null;
|
|
138
|
+
return v;
|
|
139
|
+
}
|
|
140
|
+
function validOperand(value) {
|
|
141
|
+
if (!value || typeof value !== 'object')
|
|
142
|
+
return false;
|
|
143
|
+
const v = value;
|
|
144
|
+
if (!onlyKeys(v, v.type === 'duration-ms' ? ['type', 'value'] : v.type === 'boolean' ? ['type', 'value']
|
|
145
|
+
: v.type === 'integer' ? ['type', 'value', 'unit'] : v.type === 'state-enum' ? ['type', 'value', 'enumVersion'] : ['type']))
|
|
146
|
+
return false;
|
|
147
|
+
if (v.type === 'none')
|
|
148
|
+
return true;
|
|
149
|
+
if (v.type === 'duration-ms')
|
|
150
|
+
return Number.isSafeInteger(v.value) && Number(v.value) >= 0;
|
|
151
|
+
if (v.type === 'boolean')
|
|
152
|
+
return typeof v.value === 'boolean';
|
|
153
|
+
if (v.type === 'integer')
|
|
154
|
+
return Number.isSafeInteger(v.value) && typeof v.unit === 'string' && v.unit.length <= 40;
|
|
155
|
+
return v.type === 'state-enum' && typeof v.value === 'string' && typeof v.enumVersion === 'string';
|
|
156
|
+
}
|
|
157
|
+
function validSelector(value) {
|
|
158
|
+
if (!value || typeof value !== 'object')
|
|
159
|
+
return false;
|
|
160
|
+
const v = value;
|
|
161
|
+
if (!onlyKeys(v, v.type === 'explicit-id' ? ['type', 'entityKind', 'id'] : v.type === 'pull-request' ? ['type', 'repository', 'number']
|
|
162
|
+
: v.type === 'same-turn-action' ? ['type', 'actionIndex'] : ['type']))
|
|
163
|
+
return false;
|
|
164
|
+
if (v.type === 'current-session' || v.type === 'unresolved')
|
|
165
|
+
return true;
|
|
166
|
+
if (v.type === 'same-turn-action')
|
|
167
|
+
return Number.isInteger(v.actionIndex) && Number(v.actionIndex) >= 0 && Number(v.actionIndex) < 200;
|
|
168
|
+
if (v.type === 'explicit-id')
|
|
169
|
+
return (v.entityKind === 'commitment' || v.entityKind === 'guard') && typeof v.id === 'string' && /^[A-Za-z0-9._:-]{1,128}$/.test(v.id);
|
|
170
|
+
return v.type === 'pull-request' && typeof v.repository === 'string' && /^[\w.-]+\/[\w.-]+$/.test(v.repository) && Number.isSafeInteger(v.number) && Number(v.number) > 0;
|
|
171
|
+
}
|
|
172
|
+
function validConsequence(value, messageBytes) {
|
|
173
|
+
if (!value || typeof value !== 'object')
|
|
174
|
+
return false;
|
|
175
|
+
const v = value;
|
|
176
|
+
if (!onlyKeys(v, v.relation === 'premise-for' ? ['relation', 'actionClass', 'actionStartByte', 'actionEndByte'] : ['relation', 'actionClass']))
|
|
177
|
+
return false;
|
|
178
|
+
const relations = new Set(['premise-for', 'none']);
|
|
179
|
+
const actions = new Set(['delete', 'production-deploy', 'merge', 'publish', 'external-send', 'credential-change', 'other', 'none']);
|
|
180
|
+
if (!relations.has(String(v.relation)) || !actions.has(String(v.actionClass)))
|
|
181
|
+
return false;
|
|
182
|
+
if (v.relation === 'none')
|
|
183
|
+
return v.actionClass === 'none';
|
|
184
|
+
return Number.isInteger(v.actionStartByte) && Number.isInteger(v.actionEndByte)
|
|
185
|
+
&& Number(v.actionStartByte) >= 0 && Number(v.actionEndByte) > Number(v.actionStartByte) && Number(v.actionEndByte) <= messageBytes;
|
|
186
|
+
}
|
|
187
|
+
function onlyKeys(value, allowed) {
|
|
188
|
+
const set = new Set(allowed);
|
|
189
|
+
return Object.keys(value).every((key) => set.has(key));
|
|
190
|
+
}
|
|
191
|
+
export function applyClaimCriticalityFloor(claim) {
|
|
192
|
+
if (claim.consequence.relation === 'premise-for' && IRREVERSIBLE.has(claim.consequence.actionClass))
|
|
193
|
+
return 'irreversible-precondition';
|
|
194
|
+
const floor = ['capacity.limit', 'tool-action.completed', 'operator.attributed'].includes(claim.predicate) ? 'high'
|
|
195
|
+
: ['session.elapsed-ms', 'session.state', 'commitment.state', 'guard.state', 'pull-request.merged', 'pull-request.checks-pass'].includes(claim.predicate) ? 'medium'
|
|
196
|
+
: claim.kind === 'unknown' || claim.predicate === 'unknown' ? 'medium' : 'low';
|
|
197
|
+
const rank = ['low', 'medium', 'high', 'irreversible-precondition'];
|
|
198
|
+
return rank[Math.max(rank.indexOf(floor), rank.indexOf(claim.suggestedCriticality))];
|
|
199
|
+
}
|
|
200
|
+
export function assessClaim(claim, ctx) {
|
|
201
|
+
const started = performance.now();
|
|
202
|
+
const observedAt = (ctx.now ?? new Date()).toISOString();
|
|
203
|
+
const base = { claimId: createHash('sha256').update(JSON.stringify(claim)).digest('hex'), observedAt, latencyMs: 0 };
|
|
204
|
+
if (claim.predicate === 'capacity.limit' || claim.predicate === 'pull-request.merged' || claim.predicate === 'pull-request.checks-pass') {
|
|
205
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'no-canonical-oracle', sourceKind: 'none', latencyMs: performance.now() - started };
|
|
206
|
+
}
|
|
207
|
+
if (claim.predicate === 'tool-action.completed') {
|
|
208
|
+
if ((claim.tenseScope !== 'past' && claim.tenseScope !== 'current') || claim.subjectSelector.type !== 'same-turn-action') {
|
|
209
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'unsupported-tense-or-selector', sourceKind: 'turn-evidence', latencyMs: performance.now() - started };
|
|
210
|
+
}
|
|
211
|
+
const item = ctx.evidence.toolCalls[claim.subjectSelector.actionIndex];
|
|
212
|
+
if (!item || ctx.evidence.unavailable || ctx.evidence.truncated)
|
|
213
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'evidence-incomplete', sourceKind: 'turn-evidence', latencyMs: performance.now() - started };
|
|
214
|
+
const expected = claim.operand.type === 'boolean' ? claim.operand.value : true;
|
|
215
|
+
const actual = item.ok;
|
|
216
|
+
const matches = claim.comparator === 'ne' ? actual !== expected : actual === expected;
|
|
217
|
+
return { ...base, verdict: matches ? 'supported' : 'refuted', reasonCode: 'same-turn-evidence', sourceKind: 'turn-evidence', sourceRevision: 'turn-evidence-v1', latencyMs: performance.now() - started };
|
|
218
|
+
}
|
|
219
|
+
if (claim.predicate === 'session.elapsed-ms' || claim.predicate === 'session.state') {
|
|
220
|
+
if (claim.tenseScope !== 'current' || claim.subjectSelector.type !== 'current-session') {
|
|
221
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'unsupported-tense-or-selector', sourceKind: 'session-registry', latencyMs: performance.now() - started };
|
|
222
|
+
}
|
|
223
|
+
const snapshot = ctx.sessionSnapshot;
|
|
224
|
+
if (!snapshot || Math.abs(Date.parse(observedAt) - Date.parse(snapshot.observedAt)) > 2_000) {
|
|
225
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'missing-or-stale-snapshot', sourceKind: 'session-registry', latencyMs: performance.now() - started };
|
|
226
|
+
}
|
|
227
|
+
const actual = claim.predicate === 'session.elapsed-ms' ? snapshot.elapsedMs : snapshot.state;
|
|
228
|
+
const expected = claim.operand.type === 'duration-ms' || claim.operand.type === 'state-enum' ? claim.operand.value : undefined;
|
|
229
|
+
if (expected === undefined)
|
|
230
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'operand-type-mismatch', sourceKind: 'session-registry', latencyMs: performance.now() - started };
|
|
231
|
+
const matches = claim.predicate === 'session.elapsed-ms' && typeof actual === 'number' && typeof expected === 'number'
|
|
232
|
+
&& (claim.comparator === 'eq' || claim.comparator === 'ne')
|
|
233
|
+
? (claim.comparator === 'eq') === (Math.abs(actual - expected) <= Math.max(15 * 60_000, actual * 0.2))
|
|
234
|
+
: compare(actual, expected, claim.comparator);
|
|
235
|
+
return { ...base, verdict: matches ? 'supported' : 'refuted', reasonCode: 'fresh-exact-session-snapshot',
|
|
236
|
+
sourceKind: 'session-registry', sourceRevision: snapshot.revision, latencyMs: performance.now() - started };
|
|
237
|
+
}
|
|
238
|
+
if (claim.predicate === 'commitment.state' || claim.predicate === 'guard.state') {
|
|
239
|
+
if (claim.tenseScope !== 'current' || claim.subjectSelector.type !== 'explicit-id'
|
|
240
|
+
|| (claim.predicate === 'commitment.state' && claim.subjectSelector.entityKind !== 'commitment')
|
|
241
|
+
|| (claim.predicate === 'guard.state' && claim.subjectSelector.entityKind !== 'guard')) {
|
|
242
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'unsupported-tense-or-selector', sourceKind: 'registry', latencyMs: performance.now() - started };
|
|
243
|
+
}
|
|
244
|
+
const snapshot = claim.predicate === 'commitment.state' ? ctx.commitmentSnapshots?.[claim.subjectSelector.id]
|
|
245
|
+
: ctx.guardSnapshots?.[claim.subjectSelector.id];
|
|
246
|
+
if (!snapshot || Math.abs(Date.parse(observedAt) - Date.parse(snapshot.observedAt)) > 2_000) {
|
|
247
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'missing-or-stale-snapshot', sourceKind: 'registry', latencyMs: performance.now() - started };
|
|
248
|
+
}
|
|
249
|
+
if (claim.operand.type !== 'state-enum')
|
|
250
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'operand-type-mismatch', sourceKind: 'registry', latencyMs: performance.now() - started };
|
|
251
|
+
const matches = compare(snapshot.state, claim.operand.value, claim.comparator);
|
|
252
|
+
return { ...base, verdict: matches ? 'supported' : 'refuted', reasonCode: 'fresh-exact-registry-snapshot',
|
|
253
|
+
sourceKind: claim.predicate === 'commitment.state' ? 'commitment-registry' : 'guard-registry',
|
|
254
|
+
sourceRevision: snapshot.revision, latencyMs: performance.now() - started };
|
|
255
|
+
}
|
|
256
|
+
return { ...base, verdict: 'unverifiable', reasonCode: 'no-supported-verifier', sourceKind: 'none', latencyMs: performance.now() - started };
|
|
257
|
+
}
|
|
258
|
+
function compare(actual, expected, comparator) {
|
|
259
|
+
if (typeof actual !== typeof expected)
|
|
260
|
+
return false;
|
|
261
|
+
switch (comparator) {
|
|
262
|
+
case 'eq': return actual === expected;
|
|
263
|
+
case 'ne': return actual !== expected;
|
|
264
|
+
case 'gt': return actual > expected;
|
|
265
|
+
case 'gte': return actual >= expected;
|
|
266
|
+
case 'lt': return actual < expected;
|
|
267
|
+
case 'lte': return actual <= expected;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export function protectedCueGaps(message, claims) {
|
|
271
|
+
const ranges = claims.filter((claim) => claim.endorsed && !claim.quoted && !claim.hedged)
|
|
272
|
+
.map((claim) => [claim.sourceStartByte, claim.sourceEndByte]);
|
|
273
|
+
const candidates = buildClaimCandidates(message);
|
|
274
|
+
const gaps = [];
|
|
275
|
+
for (const [name, pattern] of Object.entries(PROTECTED)) {
|
|
276
|
+
for (const candidate of candidates) {
|
|
277
|
+
if (!pattern.test(candidate.text))
|
|
278
|
+
continue;
|
|
279
|
+
if (!ranges.some(([start, end]) => start < candidate.sourceEndByte && end > candidate.sourceStartByte))
|
|
280
|
+
gaps.push(name);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return [...new Set(gaps)];
|
|
285
|
+
}
|
|
286
|
+
export function newMessageAttemptId() { return randomUUID(); }
|
|
287
|
+
/** Bounded metadata-only local projection. It is never a verifier or routing authority. */
|
|
288
|
+
export class ClaimObservationRecorder {
|
|
289
|
+
opts;
|
|
290
|
+
auditPath;
|
|
291
|
+
corpusPath;
|
|
292
|
+
keyId;
|
|
293
|
+
corpusRows = 0;
|
|
294
|
+
lastCorpusCompactionDay = '';
|
|
295
|
+
constructor(opts) {
|
|
296
|
+
this.opts = opts;
|
|
297
|
+
const root = path.join(opts.stateDir, 'state', 'claim-verification');
|
|
298
|
+
this.auditPath = path.join(root, 'claim-observation-audit-v2.jsonl');
|
|
299
|
+
this.corpusPath = path.join(root, 'claim-benchmark-v1.jsonl');
|
|
300
|
+
this.keyId = this.hmac('instar.claim-pseudonym-key-id.v1').slice(0, 32);
|
|
301
|
+
this.corpusRows = this.compactCorpus();
|
|
302
|
+
}
|
|
303
|
+
record(input) {
|
|
304
|
+
const consequenceClass = input.claim.consequence.actionClass;
|
|
305
|
+
const shape = `shape-v1|${input.claim.predicate}|${input.claim.subjectKind}|${input.claim.operand.type}|${input.claim.tenseScope}|${consequenceClass}`;
|
|
306
|
+
const claimId = this.hmac(`instar.claim-id.v1|${input.messageAttemptId}|${input.claim.clauseId}|${input.claim.sourceStartByte}|${input.claim.sourceEndByte}|${input.claim.predicate}|${JSON.stringify(input.claim.operand)}`);
|
|
307
|
+
const topicPseudonym = input.topicId === undefined ? undefined : this.hmac(`instar.claim-topic.v1|${input.topicId}`);
|
|
308
|
+
const messagePseudonym = this.hmac(`instar.claim-message.v1|${input.messageAttemptId}`);
|
|
309
|
+
const row = {
|
|
310
|
+
schemaVersion: 1, eventUuid: randomUUID(), messageAttemptId: input.messageAttemptId,
|
|
311
|
+
claimId, messagePseudonym, originMachinePseudonym: this.hmac('instar.claim-origin-machine.v1'),
|
|
312
|
+
pseudonymKeyId: this.keyId, ...(topicPseudonym ? { topicPseudonym } : {}),
|
|
313
|
+
policyVersion: 'claim-policy-v1', scrubVersion: 'claim-scrub-v1', privacyPolicyVersion: 'claim-content-v1',
|
|
314
|
+
contentClass: 'standard-scrubbed', kind: input.claim.kind, predicate: input.claim.predicate,
|
|
315
|
+
finalCriticality: input.finalCriticality, confidenceBucket: bucket(input.claim.confidence),
|
|
316
|
+
verifierId: input.assessment.sourceKind, verifierVersion: 'claim-verifier-v1', verdict: input.assessment.verdict,
|
|
317
|
+
reasonCode: input.assessment.reasonCode, sourceKind: input.assessment.sourceKind,
|
|
318
|
+
...(input.assessment.sourceRevision ? { sourceRevision: input.assessment.sourceRevision } : {}),
|
|
319
|
+
observedAt: input.assessment.observedAt, latencyBucket: latencyBucket(input.assessment.latencyMs),
|
|
320
|
+
actualLatencyMs: Math.max(0, Math.round(input.assessment.latencyMs)),
|
|
321
|
+
detectorOutcome: input.claim.endorsed ? 'endorsed-claim' : 'non-endorsed', coverageReason: input.assessment.reasonCode,
|
|
322
|
+
inputTokens: input.inputTokens ?? null, outputTokens: input.outputTokens ?? null,
|
|
323
|
+
monetaryBucket: input.inputTokens === undefined ? 'unknown' : 'provider-usage-no-price-envelope',
|
|
324
|
+
modelDoor: input.modelDoor ?? 'claude-code', modelId: input.modelId ?? 'fast',
|
|
325
|
+
claimObservationBootId: input.bootId, dryRun: input.dryRun,
|
|
326
|
+
counterfactualAdvisory: input.assessment.verdict === 'refuted' ? 'CLAIM_CONTRADICTION'
|
|
327
|
+
: input.assessment.verdict === 'unverifiable' && (input.finalCriticality === 'high' || input.finalCriticality === 'irreversible-precondition') ? 'CLAIM_UNVERIFIABLE' : 'none',
|
|
328
|
+
disposition: 'unchanged', labelTrustClass: 'none',
|
|
329
|
+
};
|
|
330
|
+
if (!this.appendBounded(this.auditPath, row, this.opts.maxAuditBytes ?? 50 * 1024 * 1024, true))
|
|
331
|
+
return false;
|
|
332
|
+
const corpus = {
|
|
333
|
+
schemaVersion: 1, claimId, claimShapeId: this.hmac(shape), ...(topicPseudonym ? { topicPseudonym } : {}), pseudonymKeyId: this.keyId,
|
|
334
|
+
claimKind: input.claim.kind, predicate: input.claim.predicate, criticality: input.finalCriticality,
|
|
335
|
+
modelDoor: row.modelDoor, modelId: row.modelId, verifierVersion: row.verifierVersion,
|
|
336
|
+
verdict: input.assessment.verdict, costBucket: 'unknown', latencyBucket: row.latencyBucket,
|
|
337
|
+
evidenceClass: input.assessment.sourceKind, sourceRevision: input.assessment.sourceRevision ?? null,
|
|
338
|
+
canonicalEntityPseudonym: this.hmac(`instar.claim-entity.v1|${JSON.stringify(input.claim.subjectSelector)}`),
|
|
339
|
+
observedAt: input.assessment.observedAt,
|
|
340
|
+
labelTrustClass: row.labelTrustClass, settlementState: 'pending', groundTruthVerdict: null,
|
|
341
|
+
correctness: null, outcomeRevision: null, automationEligible: false,
|
|
342
|
+
};
|
|
343
|
+
this.corpusRows = this.compactCorpus();
|
|
344
|
+
if (this.corpusRows >= 500_000)
|
|
345
|
+
return false;
|
|
346
|
+
const appended = this.appendBounded(this.corpusPath, corpus, this.opts.maxCorpusBytes ?? 500 * 1024 * 1024, false);
|
|
347
|
+
if (appended)
|
|
348
|
+
this.corpusRows++;
|
|
349
|
+
return appended;
|
|
350
|
+
}
|
|
351
|
+
readAudit(limit = 100) { return readJsonl(this.auditPath, limit); }
|
|
352
|
+
/** In-process settlement seam. Only a canonical adapter holding the original
|
|
353
|
+
* claim id and exact source revision can issue this later outcome receipt. */
|
|
354
|
+
recordAuthoritativeOutcome(receipt) {
|
|
355
|
+
try {
|
|
356
|
+
const rows = readJsonl(this.corpusPath, 500_000);
|
|
357
|
+
const target = rows.find((row) => row.settlementState === 'pending' && row.claimId === receipt.claimId
|
|
358
|
+
&& row.predicate === receipt.predicate && row.sourceRevision === receipt.sourceRevision);
|
|
359
|
+
if (!target)
|
|
360
|
+
return false;
|
|
361
|
+
target.groundTruthVerdict = receipt.verdict;
|
|
362
|
+
target.correctness = target.verdict === receipt.verdict;
|
|
363
|
+
target.settledAt = receipt.observedAt;
|
|
364
|
+
target.labelTrustClass = 'T0';
|
|
365
|
+
target.settlementState = 'settled';
|
|
366
|
+
target.outcomeRevision = receipt.sourceRevision;
|
|
367
|
+
const labelId = this.hmac(`instar.claim-t0-label.v1|${receipt.claimId}|${receipt.sourceRevision}|${receipt.verdict}`);
|
|
368
|
+
fs.writeFileSync(this.corpusPath, rows.map((row) => JSON.stringify(row)).join('\n') + '\n', { mode: 0o600 });
|
|
369
|
+
return this.appendBounded(this.auditPath, { schemaVersion: 1, eventUuid: randomUUID(), event: 't0-settlement',
|
|
370
|
+
claimId: receipt.claimId, eventualGroundTruthLabelId: labelId, sourceRevision: receipt.sourceRevision,
|
|
371
|
+
observedAt: receipt.observedAt, disposition: 'unchanged' }, this.opts.maxAuditBytes ?? 50 * 1024 * 1024, true);
|
|
372
|
+
}
|
|
373
|
+
catch { /* @silent-fallback-ok: failed settlement remains pending and reports false */
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
recordEvent(input) {
|
|
378
|
+
const allowed = { schemaVersion: 1, eventUuid: randomUUID() };
|
|
379
|
+
for (const key of ['ts', 'evaluated', 'flagged', 'dryRun', 'event', 'verdict', 'actionKind', 'hadToolCalls', 'reason']) {
|
|
380
|
+
const value = input[key];
|
|
381
|
+
if (typeof value === 'string')
|
|
382
|
+
allowed[key] = value.slice(0, 128);
|
|
383
|
+
else if (typeof value === 'boolean')
|
|
384
|
+
allowed[key] = value;
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(input.gapKinds))
|
|
387
|
+
allowed.gapKinds = input.gapKinds.filter((v) => typeof v === 'string').slice(0, 8);
|
|
388
|
+
return this.appendBounded(this.auditPath, allowed, this.opts.maxAuditBytes ?? 50 * 1024 * 1024, true);
|
|
389
|
+
}
|
|
390
|
+
readPoolAggregates(limit = 100) {
|
|
391
|
+
const rows = readJsonl(this.corpusPath, 500_000);
|
|
392
|
+
const groups = new Map();
|
|
393
|
+
for (const row of rows) {
|
|
394
|
+
if (row.pseudonymKeyId !== this.keyId || typeof row.observedAt !== 'string'
|
|
395
|
+
|| typeof row.claimShapeId !== 'string' || typeof row.topicPseudonym !== 'string')
|
|
396
|
+
continue;
|
|
397
|
+
const day = row.observedAt.slice(0, 10);
|
|
398
|
+
const modelDoor = typeof row.modelDoor === 'string' ? row.modelDoor : 'unknown';
|
|
399
|
+
const verifierVersion = typeof row.verifierVersion === 'string' ? row.verifierVersion : 'unknown';
|
|
400
|
+
const key = `${day}|${row.claimShapeId}|${modelDoor}|${verifierVersion}`;
|
|
401
|
+
const group = groups.get(key) ?? { day, claimShapeId: row.claimShapeId, modelDoor, verifierVersion, count: 0, topics: new Set(), verdicts: {} };
|
|
402
|
+
group.count++;
|
|
403
|
+
group.topics.add(row.topicPseudonym);
|
|
404
|
+
const verdict = typeof row.verdict === 'string' ? row.verdict : 'unknown';
|
|
405
|
+
group.verdicts[verdict] = (group.verdicts[verdict] ?? 0) + 1;
|
|
406
|
+
groups.set(key, group);
|
|
407
|
+
}
|
|
408
|
+
return [...groups.values()].filter((group) => group.count >= 20 && group.topics.size >= 5)
|
|
409
|
+
.sort((a, b) => `${a.day}|${a.claimShapeId}|${a.modelDoor}|${a.verifierVersion}`
|
|
410
|
+
.localeCompare(`${b.day}|${b.claimShapeId}|${b.modelDoor}|${b.verifierVersion}`))
|
|
411
|
+
.slice(0, Math.max(1, Math.min(limit, 500_000))).map(({ topics: _topics, ...group }) => ({ ...group, automationEligible: false }));
|
|
412
|
+
}
|
|
413
|
+
readPoolPage(limit = 100, cursor) {
|
|
414
|
+
const all = this.readPoolAggregates(500_000);
|
|
415
|
+
let afterKey = '';
|
|
416
|
+
if (cursor) {
|
|
417
|
+
try {
|
|
418
|
+
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
419
|
+
const separator = decoded.lastIndexOf('.');
|
|
420
|
+
const rawKey = decoded.slice(0, separator);
|
|
421
|
+
const signature = decoded.slice(separator + 1);
|
|
422
|
+
if (!rawKey || signature !== this.hmac(`instar.claim-pool-cursor.v1|${this.keyId}|${rawKey}`))
|
|
423
|
+
return { records: [] };
|
|
424
|
+
afterKey = rawKey;
|
|
425
|
+
}
|
|
426
|
+
catch { /* @silent-fallback-ok: invalid signed cursor yields no pool records */
|
|
427
|
+
return { records: [] };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const size = Math.max(1, Math.min(limit, 200));
|
|
431
|
+
const eligible = afterKey ? all.filter((row) => `${row.day}|${row.claimShapeId}|${row.modelDoor}|${row.verifierVersion}` > afterKey) : all;
|
|
432
|
+
const records = eligible.slice(0, size);
|
|
433
|
+
const last = records.at(-1);
|
|
434
|
+
const lastKey = last ? `${last.day}|${last.claimShapeId}|${last.modelDoor}|${last.verifierVersion}` : '';
|
|
435
|
+
return { records, ...(eligible.length > records.length && lastKey ? { nextCursor: Buffer.from(`${lastKey}.${this.hmac(`instar.claim-pool-cursor.v1|${this.keyId}|${lastKey}`)}`).toString('base64url') } : {}) };
|
|
436
|
+
}
|
|
437
|
+
hmac(value) { return createHmac('sha256', this.opts.pseudonymKey).update(value).digest('hex'); }
|
|
438
|
+
compactCorpus() {
|
|
439
|
+
try {
|
|
440
|
+
this.lastCorpusCompactionDay = new Date().toISOString().slice(0, 10);
|
|
441
|
+
if (!fs.existsSync(this.corpusPath))
|
|
442
|
+
return 0;
|
|
443
|
+
const pendingCutoff = Date.now() - 90 * 86_400_000;
|
|
444
|
+
const settledCutoff = Date.now() - 365 * 86_400_000;
|
|
445
|
+
const rows = fs.readFileSync(this.corpusPath, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
|
|
446
|
+
try {
|
|
447
|
+
const row = JSON.parse(line);
|
|
448
|
+
const cutoff = row.labelTrustClass === 'T0' ? settledCutoff : pendingCutoff;
|
|
449
|
+
return typeof row.observedAt === 'string' && Date.parse(row.observedAt) >= cutoff ? [row] : [];
|
|
450
|
+
}
|
|
451
|
+
catch { /* @silent-fallback-ok: malformed bounded corpus rows are discarded */
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
fs.writeFileSync(this.corpusPath, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), { mode: 0o600 });
|
|
456
|
+
fs.chmodSync(this.corpusPath, 0o600);
|
|
457
|
+
return rows.length;
|
|
458
|
+
}
|
|
459
|
+
catch { /* @silent-fallback-ok: compaction failure fails closed at the row cap */
|
|
460
|
+
return 500_000;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
appendBounded(file, row, maxBytes, rotate) {
|
|
464
|
+
try {
|
|
465
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
466
|
+
const line = `${JSON.stringify(row)}\n`;
|
|
467
|
+
const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
468
|
+
if (size + Buffer.byteLength(line) > maxBytes) {
|
|
469
|
+
if (!rotate)
|
|
470
|
+
return false;
|
|
471
|
+
if (fs.existsSync(`${file}.2`))
|
|
472
|
+
SafeFsExecutor.safeRmSync(`${file}.2`, { force: true, operation: 'claim-audit-rotate' });
|
|
473
|
+
if (fs.existsSync(`${file}.1`))
|
|
474
|
+
fs.renameSync(`${file}.1`, `${file}.2`);
|
|
475
|
+
if (fs.existsSync(file))
|
|
476
|
+
fs.renameSync(file, `${file}.1`);
|
|
477
|
+
}
|
|
478
|
+
fs.appendFileSync(file, line, { mode: 0o600 });
|
|
479
|
+
fs.chmodSync(file, 0o600);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
catch { /* @silent-fallback-ok: audit persistence failure is surfaced as false */
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
export class ClaimObservationHousekeeper {
|
|
488
|
+
opts;
|
|
489
|
+
constructor(opts) {
|
|
490
|
+
this.opts = opts;
|
|
491
|
+
}
|
|
492
|
+
sweep() {
|
|
493
|
+
const now = this.opts.now?.() ?? Date.now();
|
|
494
|
+
const logs = path.join(this.opts.stateDir, 'logs');
|
|
495
|
+
const root = path.join(this.opts.stateDir, 'state', 'claim-verification');
|
|
496
|
+
const candidates = ['completion-claim-audit.jsonl', 'completion-claim-audit.jsonl.1'];
|
|
497
|
+
let deleted = 0;
|
|
498
|
+
let skipped = 0;
|
|
499
|
+
let failures = 0;
|
|
500
|
+
for (const name of candidates) {
|
|
501
|
+
if (deleted >= 4)
|
|
502
|
+
break;
|
|
503
|
+
const target = path.join(logs, name);
|
|
504
|
+
try {
|
|
505
|
+
const stat = fs.lstatSync(target);
|
|
506
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
507
|
+
skipped++;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
fs.chmodSync(target, 0o600);
|
|
511
|
+
if (now - stat.mtimeMs < 7 * 86_400_000) {
|
|
512
|
+
skipped++;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
SafeFsExecutor.safeUnlinkSync(target, { operation: 'claim-legacy-retention' });
|
|
516
|
+
deleted++;
|
|
517
|
+
this.appendReceipt(root, { schemaVersion: 1, pathClass: 'legacy-completion-claim-audit',
|
|
518
|
+
deletedAt: new Date(now).toISOString(), outcome: 'deleted' }, now);
|
|
519
|
+
}
|
|
520
|
+
catch (error) { /* @silent-fallback-ok: absence is normal; other failures increment the receipt */
|
|
521
|
+
if (error.code !== 'ENOENT') {
|
|
522
|
+
skipped++;
|
|
523
|
+
failures++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { deleted, skipped, failures };
|
|
528
|
+
}
|
|
529
|
+
appendReceipt(root, row, now) {
|
|
530
|
+
fs.mkdirSync(root, { recursive: true });
|
|
531
|
+
const file = path.join(root, 'retention-receipts-v1.jsonl');
|
|
532
|
+
const line = `${JSON.stringify(row)}\n`;
|
|
533
|
+
if (fs.existsSync(file) && fs.statSync(file).size + Buffer.byteLength(line) > 1024 * 1024) {
|
|
534
|
+
const rotated = `${file}.1`;
|
|
535
|
+
if (fs.existsSync(rotated))
|
|
536
|
+
SafeFsExecutor.safeUnlinkSync(rotated, { operation: 'claim-receipt-rotate' });
|
|
537
|
+
fs.renameSync(file, rotated);
|
|
538
|
+
}
|
|
539
|
+
fs.appendFileSync(file, line, { mode: 0o600 });
|
|
540
|
+
fs.chmodSync(file, 0o600);
|
|
541
|
+
for (const candidate of [file, `${file}.1`]) {
|
|
542
|
+
if (!fs.existsSync(candidate))
|
|
543
|
+
continue;
|
|
544
|
+
const rows = fs.readFileSync(candidate, 'utf8').split('\n').filter(Boolean).filter((entry) => {
|
|
545
|
+
try {
|
|
546
|
+
const parsed = JSON.parse(entry);
|
|
547
|
+
return typeof parsed.deletedAt === 'string'
|
|
548
|
+
&& Date.parse(parsed.deletedAt) >= now - 30 * 86_400_000;
|
|
549
|
+
}
|
|
550
|
+
catch { /* @silent-fallback-ok: malformed retention receipts are dropped */
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
fs.writeFileSync(candidate, rows.join('\n') + (rows.length ? '\n' : ''), { mode: 0o600 });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function bucket(value) { return value >= .9 ? '0.9-1.0' : value >= .7 ? '0.7-0.9' : value >= .5 ? '0.5-0.7' : '0-0.5'; }
|
|
559
|
+
function latencyBucket(value) { return value <= 10 ? 'le10ms' : value <= 100 ? 'le100ms' : value <= 500 ? 'le500ms' : 'gt500ms'; }
|
|
560
|
+
function readJsonl(file, limit) {
|
|
561
|
+
try {
|
|
562
|
+
return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-Math.max(1, Math.min(limit, 500_000))).flatMap((line) => {
|
|
563
|
+
try {
|
|
564
|
+
return [JSON.parse(line)];
|
|
565
|
+
}
|
|
566
|
+
catch { /* @silent-fallback-ok: malformed bounded rows are ignored */
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch { /* @silent-fallback-ok: absent or unreadable optional audit returns empty */
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
//# sourceMappingURL=ClaimObservation.js.map
|