@xaccefy/pi-casefile 0.10.0 → 0.11.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/README.md +6 -6
- package/package.json +1 -2
- package/src/confirmation.ts +239 -323
- package/src/evidence.ts +101 -135
- package/src/harness-verify.ts +54 -247
- package/src/index.ts +188 -745
- package/src/ledger-internal.ts +118 -4
- package/src/ledger.ts +459 -175
- package/src/poc-runner.ts +43 -14
- package/src/scratchpad.ts +88 -143
- package/src/workflow.ts +6 -2
- package/src/oob-oracle.ts +0 -279
package/src/ledger-internal.ts
CHANGED
|
@@ -124,6 +124,10 @@ export function buildRecord(input: NormalizedCaseInput, existing?: CaseRecord):
|
|
|
124
124
|
confirmerVerdict: input.confirmerVerdict ?? existing?.confirmerVerdict,
|
|
125
125
|
reportedAt: input.reportedAt ?? existing?.reportedAt,
|
|
126
126
|
reportPath: input.reportPath ?? existing?.reportPath,
|
|
127
|
+
retryPolicy:
|
|
128
|
+
input.retryPolicy !== undefined
|
|
129
|
+
? normalizeRetryPolicy(input.retryPolicy)
|
|
130
|
+
: existing?.retryPolicy,
|
|
127
131
|
evidenceItems: existing?.evidenceItems ?? [],
|
|
128
132
|
coverageItems: existing?.coverageItems ?? [],
|
|
129
133
|
linkedCases: existing?.linkedCases ?? [],
|
|
@@ -132,8 +136,78 @@ export function buildRecord(input: NormalizedCaseInput, existing?: CaseRecord):
|
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
/** Normalize a retry policy: attempts 1–10, at most 8 distinct fallback models. */
|
|
140
|
+
export function normalizeRetryPolicy(policy: unknown): CaseRecord["retryPolicy"] {
|
|
141
|
+
if (policy === null || policy === undefined) return undefined;
|
|
142
|
+
if (typeof policy !== "object" || Array.isArray(policy)) {
|
|
143
|
+
throw new Error("retry_policy must be an object: { max_attempts, fallback_models? }");
|
|
144
|
+
}
|
|
145
|
+
const p = policy as { max_attempts?: unknown; fallback_models?: unknown };
|
|
146
|
+
if (
|
|
147
|
+
typeof p.max_attempts !== "number" ||
|
|
148
|
+
!Number.isInteger(p.max_attempts) ||
|
|
149
|
+
p.max_attempts < 1 ||
|
|
150
|
+
p.max_attempts > 10
|
|
151
|
+
) {
|
|
152
|
+
throw new Error("retry_policy.max_attempts must be an integer between 1 and 10");
|
|
153
|
+
}
|
|
154
|
+
if (p.fallback_models !== undefined && p.fallback_models !== null) {
|
|
155
|
+
if (!Array.isArray(p.fallback_models) || p.fallback_models.length > 8) {
|
|
156
|
+
throw new Error("retry_policy.fallback_models must be an array of at most 8 model names");
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
!p.fallback_models.every(
|
|
160
|
+
(m) => typeof m === "string" && m.trim().length > 0 && m.trim() === m,
|
|
161
|
+
)
|
|
162
|
+
) {
|
|
163
|
+
throw new Error("retry_policy.fallback_models entries must be non-empty trimmed strings");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const fallbackModels = Array.isArray(p.fallback_models)
|
|
167
|
+
? Array.from(new Set(p.fallback_models.map((m) => m as string)))
|
|
168
|
+
: undefined;
|
|
169
|
+
return fallbackModels?.length
|
|
170
|
+
? { max_attempts: p.max_attempts, fallback_models: fallbackModels }
|
|
171
|
+
: { max_attempts: p.max_attempts };
|
|
172
|
+
}
|
|
173
|
+
|
|
135
174
|
export function validateCase(record: CaseRecord): void {
|
|
136
175
|
if (!record.title.trim()) throw new Error("Case title cannot be empty");
|
|
176
|
+
// Enum membership on the public API path: the tool layer schema-gates these,
|
|
177
|
+
// but direct callers (tests, other integrations) must not be able to persist
|
|
178
|
+
// "bogus" statuses that every later gate and reader would mis-handle.
|
|
179
|
+
const STATUS = [
|
|
180
|
+
"hypothesis",
|
|
181
|
+
"investigating",
|
|
182
|
+
"confirmed",
|
|
183
|
+
"blocked",
|
|
184
|
+
"killed",
|
|
185
|
+
"reported",
|
|
186
|
+
] as const;
|
|
187
|
+
const CONFIDENCE = ["low", "medium", "high"] as const;
|
|
188
|
+
const SEVERITY = ["info", "low", "medium", "high", "critical"] as const;
|
|
189
|
+
const PRIORITY = ["P0", "P1", "P2", "P3", "P4"] as const;
|
|
190
|
+
if (!(STATUS as readonly string[]).includes(record.status)) {
|
|
191
|
+
throw new Error(`Invalid case status: ${record.status}. Statuses: ${STATUS.join(", ")}`);
|
|
192
|
+
}
|
|
193
|
+
if (!(CONFIDENCE as readonly string[]).includes(record.confidence)) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Invalid case confidence: ${record.confidence}. Confidence levels: ${CONFIDENCE.join(", ")}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
// Null check (not just undefined): DB reads surface absent columns as null.
|
|
199
|
+
if (record.severity != null && !(SEVERITY as readonly string[]).includes(record.severity)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Invalid case severity: ${record.severity}. Severities: ${SEVERITY.join(", ")}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (record.priority != null && !(PRIORITY as readonly string[]).includes(record.priority)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Invalid case priority: ${record.priority}. Priorities: ${PRIORITY.join(", ")}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
// Retry policy shape is machine-read by retry tooling — re-check on write.
|
|
210
|
+
if (record.retryPolicy !== undefined) normalizeRetryPolicy(record.retryPolicy);
|
|
137
211
|
// Falsification conditions are load-bearing: they are required at creation
|
|
138
212
|
// and must not be erasable later (CaseUpdate({ disproveIf: [] }) would wipe
|
|
139
213
|
// the hypothesis's falsifiability). Re-check on every write.
|
|
@@ -226,14 +300,14 @@ export function upsertCase(db: DatabaseSync, record: CaseRecord) {
|
|
|
226
300
|
references_json, blockers_json, tags_json, assumptions_json, poc_verified_json,
|
|
227
301
|
disconfirmation, disconfirmation_verified_json, disprove_if_json, control_verified_json,
|
|
228
302
|
pending_confirmation_json, confirmer_verdict_json,
|
|
229
|
-
reported_at, report_path, invariant, created_at, updated_at
|
|
303
|
+
reported_at, report_path, retry_policy_json, invariant, created_at, updated_at
|
|
230
304
|
) VALUES (
|
|
231
305
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
232
306
|
?, ?, ?, ?, ?, ?,
|
|
233
307
|
?, ?, ?, ?, ?,
|
|
234
308
|
?, ?, ?, ?,
|
|
235
309
|
?, ?,
|
|
236
|
-
?, ?, ?, ?, ?
|
|
310
|
+
?, ?, ?, ?, ?, ?
|
|
237
311
|
)
|
|
238
312
|
ON CONFLICT(id) DO UPDATE SET
|
|
239
313
|
title = excluded.title,
|
|
@@ -265,6 +339,7 @@ export function upsertCase(db: DatabaseSync, record: CaseRecord) {
|
|
|
265
339
|
invariant = excluded.invariant,
|
|
266
340
|
reported_at = excluded.reported_at,
|
|
267
341
|
report_path = excluded.report_path,
|
|
342
|
+
retry_policy_json = excluded.retry_policy_json,
|
|
268
343
|
created_at = excluded.created_at,
|
|
269
344
|
updated_at = excluded.updated_at
|
|
270
345
|
`);
|
|
@@ -299,6 +374,7 @@ export function upsertCase(db: DatabaseSync, record: CaseRecord) {
|
|
|
299
374
|
record.confirmerVerdict ? JSON.stringify(record.confirmerVerdict) : null,
|
|
300
375
|
record.reportedAt || null,
|
|
301
376
|
record.reportPath || null,
|
|
377
|
+
record.retryPolicy ? JSON.stringify(record.retryPolicy) : null,
|
|
302
378
|
record.invariant || null,
|
|
303
379
|
record.createdAt,
|
|
304
380
|
record.updatedAt,
|
|
@@ -307,8 +383,8 @@ export function upsertCase(db: DatabaseSync, record: CaseRecord) {
|
|
|
307
383
|
|
|
308
384
|
export function insertEvidenceItem(db: DatabaseSync, item: EvidenceItem): void {
|
|
309
385
|
db.prepare(
|
|
310
|
-
`INSERT INTO evidence_items (id, case_id, role, artifact_path, sha256, summary, created_at)
|
|
311
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
386
|
+
`INSERT INTO evidence_items (id, case_id, role, artifact_path, sha256, summary, created_at, contains_secret, secret_findings_json)
|
|
387
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
312
388
|
).run(
|
|
313
389
|
item.id,
|
|
314
390
|
item.caseId,
|
|
@@ -317,5 +393,43 @@ export function insertEvidenceItem(db: DatabaseSync, item: EvidenceItem): void {
|
|
|
317
393
|
item.sha256 ?? null,
|
|
318
394
|
item.summary,
|
|
319
395
|
item.createdAt,
|
|
396
|
+
item.containsSecret === true ? 1 : 0,
|
|
397
|
+
item.secretFindings?.length ? JSON.stringify(item.secretFindings) : null,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Event journal ────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
export type CaseEvent = {
|
|
404
|
+
caseId: string;
|
|
405
|
+
seq: number;
|
|
406
|
+
timestamp: string;
|
|
407
|
+
eventType: string;
|
|
408
|
+
actor: string;
|
|
409
|
+
payload?: Record<string, unknown>;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Append one journal event for a case. Append-only: seq is allocated as
|
|
414
|
+
* max(seq)+1 under the caller's transaction, so events land in commit order.
|
|
415
|
+
* Payloads must stay small and secret-free (field NAMES and ids, not values).
|
|
416
|
+
*/
|
|
417
|
+
export function appendCaseEvent(
|
|
418
|
+
db: DatabaseSync,
|
|
419
|
+
event: { caseId: string; eventType: string; actor?: string; payload?: Record<string, unknown> },
|
|
420
|
+
): void {
|
|
421
|
+
const row = db
|
|
422
|
+
.prepare("SELECT COALESCE(MAX(seq), 0) AS max_seq FROM case_events WHERE case_id = ?")
|
|
423
|
+
.get(event.caseId) as { max_seq: number } | undefined;
|
|
424
|
+
const seq = (row?.max_seq ?? 0) + 1;
|
|
425
|
+
db.prepare(
|
|
426
|
+
"INSERT INTO case_events (case_id, seq, timestamp, event_type, actor, payload_json) VALUES (?, ?, ?, ?, ?, ?)",
|
|
427
|
+
).run(
|
|
428
|
+
event.caseId,
|
|
429
|
+
seq,
|
|
430
|
+
new Date().toISOString(),
|
|
431
|
+
event.eventType,
|
|
432
|
+
event.actor ?? "agent",
|
|
433
|
+
event.payload ? JSON.stringify(event.payload) : null,
|
|
320
434
|
);
|
|
321
435
|
}
|