@akagilnc/pi-workflow-roles 0.1.3565 → 0.1.3621
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 +4 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +18477 -8099
- package/dist/analyst-gate-cycles-read.js +387 -0
- package/dist/archivist-record-entry.js +22 -1
- package/dist/audit-escalation.js +6 -0
- package/dist/auditor-soul.js +74 -0
- package/dist/collector-config.js +86 -0
- package/dist/collector-evidence.js +316 -0
- package/dist/collector-github.js +527 -0
- package/dist/collector-ledger.js +853 -0
- package/dist/collector-tool-schemas.js +33 -0
- package/dist/compliance-transport.js +104 -52
- package/dist/diarist-contracts.js +72 -0
- package/dist/diarist-mechanical.js +411 -0
- package/dist/diarist-ticket-resolution.js +177 -0
- package/dist/diarist.js +311 -0
- package/dist/doctor-auditor.js +25 -0
- package/dist/doctor-contracts.js +2 -0
- package/dist/doctor-evidence.js +142 -0
- package/dist/gatekeeper-role.js +104 -81
- package/dist/host-transition-prior-native.js +78 -0
- package/dist/institutional-resolution.js +1 -95
- package/dist/judge-auditor.js +26 -0
- package/dist/ledger-session-read.js +227 -0
- package/dist/merger-git-state.js +78 -0
- package/dist/navigator-attendance.js +15 -7
- package/dist/navigator-public-session.js +170 -0
- package/dist/navigator-session-contracts.js +36 -40
- package/dist/notary-source-run.js +122 -0
- package/dist/package-contracts/auditor-output.js +66 -0
- package/dist/package-contracts/evidence-child-output.js +34 -0
- package/dist/package-contracts/judge-output.js +2 -0
- package/dist/package-contracts/terminating-tools.js +44 -3
- package/dist/package-resources/method-skill.js +254 -0
- package/dist/packaged-role-registry.js +56 -0
- package/dist/pi/durable-principal.js +61 -0
- package/dist/pi/in-process-session.js +25 -6
- package/dist/pi/known-failure.js +52 -0
- package/dist/pi/role-turn-host.js +430 -0
- package/dist/public-cli/auto-resume.js +414 -0
- package/dist/public-cli/cli-errors.js +8 -0
- package/dist/public-cli/cli-io.js +1 -0
- package/dist/public-cli/command-renderer.js +5 -0
- package/dist/public-cli/doctor-run.js +84 -0
- package/dist/public-cli/inspector-run.js +136 -0
- package/dist/public-cli/instruction-seat-run.js +256 -0
- package/dist/public-cli/invocation.js +2225 -0
- package/dist/public-cli/judge-run.js +118 -0
- package/dist/public-cli/load-production-acp-host.js +40 -0
- package/dist/public-cli/main.js +1511 -1124
- package/dist/public-cli/notary-run.js +161 -0
- package/dist/public-cli/option-definitions.js +1192 -0
- package/dist/public-cli/post-admission.js +667 -0
- package/dist/public-cli/public-run-credentials.js +49 -0
- package/dist/public-cli/registry.js +8 -2
- package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
- package/dist/public-cli/run-lifecycle.js +1344 -0
- package/dist/public-cli/seat-ticket-binding.js +113 -0
- package/dist/public-cli/settlement.js +3441 -0
- package/dist/public-cli/terminal.js +135 -0
- package/dist/public-cli/turn-request.js +25 -0
- package/dist/public-role-summons.js +301 -0
- package/dist/receipt-delivery-policy.js +10 -0
- package/dist/reviewer-child-executor.js +80 -8
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/run-terminal-artifacts.js +195 -0
- package/dist/run-ticket-number.js +40 -0
- package/dist/session-assistant-usage.js +107 -0
- package/dist/shape-unreadable-failure.js +34 -0
- package/dist/submission-errors.js +3 -0
- package/dist/submission-ledger.js +388 -0
- package/dist/ticket-provenance-contracts.js +115 -0
- package/dist/ticket-provenance.js +340 -0
- package/extensions/role-runtime.ts +8 -0
- package/package.json +1 -1
- package/resources/diarist-collect.md +6 -10
- package/scripts/build-package.mjs +2 -1
- package/souls/diarist.md +9 -0
- package/souls/doctor-auditor.md +1 -0
- package/souls/evidence-child.md +1 -0
- package/src/acp-host/production-host.ts +4 -0
- package/src/acp-host/role-envelope.ts +4 -3
- package/src/acp-host/role-turn-host.ts +9 -1
- package/src/analyst-gate-cycles-read.ts +37 -2
- package/src/archivist-record-entry.ts +45 -1
- package/src/audit-escalation.ts +17 -0
- package/src/auditor-role.ts +22 -0
- package/src/auditor-soul.ts +42 -0
- package/src/compliance-transport.ts +177 -66
- package/src/diarist-contracts.ts +88 -0
- package/src/diarist-role.ts +60 -0
- package/src/diarist.ts +253 -179
- package/src/doctor-auditor.ts +10 -14
- package/src/doctor-contracts.ts +1 -0
- package/src/doctor-role.ts +2 -2
- package/src/evidence-child-role.ts +22 -0
- package/src/gatekeeper-pass-envelope.ts +109 -0
- package/src/gatekeeper-role.ts +145 -104
- package/src/host-contracts.ts +15 -2
- package/src/institutional-resolution.ts +8 -163
- package/src/judge-auditor.ts +10 -15
- package/src/judge-role.ts +8 -0
- package/src/navigator-attendance.ts +25 -9
- package/src/navigator-public-session.ts +217 -0
- package/src/navigator-session-contracts.ts +61 -54
- package/src/notary-source-run.ts +3 -1
- package/src/package-contracts/auditor-output.ts +82 -0
- package/src/package-contracts/evidence-child-output.ts +51 -0
- package/src/package-contracts/judge-output.ts +1 -0
- package/src/package-contracts/reviewer-output.ts +2 -2
- package/src/package-contracts/terminating-tools.ts +53 -3
- package/src/packaged-role-registry.ts +63 -0
- package/src/pi/adapter.ts +2 -1
- package/src/pi/in-process-session.ts +38 -12
- package/src/pi/role-turn-host.ts +42 -0
- package/src/public-cli/cli.ts +43 -6
- package/src/public-cli/countersign-run.ts +9 -129
- package/src/public-cli/diarist-run.ts +312 -0
- package/src/public-cli/instruction-seat-run.ts +289 -50
- package/src/public-cli/invocation.ts +87 -25
- package/src/public-cli/option-definitions.ts +79 -0
- package/src/public-cli/post-admission.ts +9 -3
- package/src/public-cli/registry.ts +7 -1
- package/src/public-cli/run-lifecycle.ts +53 -6
- package/src/public-cli/settlement.ts +170 -7
- package/src/public-cli/terminal.ts +4 -1
- package/src/public-role-summons.ts +431 -0
- package/src/receipt-delivery-policy.ts +10 -0
- package/src/reviewer-agent.ts +1 -1
- package/src/reviewer-child-executor.ts +114 -12
- package/src/reviewer-execution-ledger.ts +3 -2
- package/src/role-runtime.ts +275 -23
- package/src/session-assistant-usage.ts +119 -0
- package/src/session-opening-materials.ts +1 -1
- package/src/shape-unreadable-failure.ts +49 -0
- package/src/submission-errors.ts +3 -0
- package/src/ticket-provenance-contracts.ts +1 -0
- package/src/ticket-provenance.ts +0 -23
- package/dist/evidence-child-executor.js +0 -829
- package/src/diarist-llm-collector.ts +0 -324
- package/src/evidence-child-executor.ts +0 -1140
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 起居录 volume helpers — ADR 0075 / #582.
|
|
3
|
+
* Write/read via sitian facade only; no parallel destination logic.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { resolveSitianRecordPath, sitianReport, readSitianRecords, } from "./sitian-facade.js";
|
|
9
|
+
import { TICKET_PROVENANCE_HUMAN_VIEW, TICKET_PROVENANCE_KIND, TICKET_PROVENANCE_OFFERED_WATERMARK, TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC, projectTicketProvenanceDiagnostic, projectTicketProvenanceEntry, } from "./ticket-provenance-contracts.js";
|
|
10
|
+
/** Subject string for ticket-keyed volumes — history follows the ticket. */
|
|
11
|
+
export function ticketProvenanceSubject(ticketNumber) {
|
|
12
|
+
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
|
|
13
|
+
throw new Error(`ticket-provenance subject requires a positive ticket number, got ${String(ticketNumber)}`);
|
|
14
|
+
}
|
|
15
|
+
return String(ticketNumber);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic entry identity for sitian entry-level idempotency.
|
|
19
|
+
* Same ticket + source pointer + transcript → same identity → no re-append.
|
|
20
|
+
*/
|
|
21
|
+
export function ticketProvenanceEntryIdentity(input) {
|
|
22
|
+
const ref = input.sourceRef;
|
|
23
|
+
const refKey = [
|
|
24
|
+
ref.sessionFile ?? "",
|
|
25
|
+
ref.entryId === undefined ? "" : String(ref.entryId),
|
|
26
|
+
ref.path ?? "",
|
|
27
|
+
ref.url ?? "",
|
|
28
|
+
].join("\u0001");
|
|
29
|
+
const material = [
|
|
30
|
+
String(input.ticketNumber),
|
|
31
|
+
input.sourceKind,
|
|
32
|
+
refKey,
|
|
33
|
+
input.transcript,
|
|
34
|
+
].join("\u0000");
|
|
35
|
+
return createHash("sha256").update(material, "utf8").digest("hex");
|
|
36
|
+
}
|
|
37
|
+
/** Append one transcribed block; returns existing pointer on identity hit. */
|
|
38
|
+
export function appendTicketProvenanceEntry(input) {
|
|
39
|
+
const subject = ticketProvenanceSubject(input.ticketNumber);
|
|
40
|
+
const identity = ticketProvenanceEntryIdentity({
|
|
41
|
+
ticketNumber: input.ticketNumber,
|
|
42
|
+
sourceKind: input.entry.sourceKind,
|
|
43
|
+
sourceRef: input.entry.sourceRef,
|
|
44
|
+
transcript: input.entry.transcript,
|
|
45
|
+
});
|
|
46
|
+
return sitianReport({
|
|
47
|
+
level: "event",
|
|
48
|
+
kind: TICKET_PROVENANCE_KIND,
|
|
49
|
+
identity,
|
|
50
|
+
subject,
|
|
51
|
+
cwd: input.cwd,
|
|
52
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
53
|
+
host: input.host ?? "diarist",
|
|
54
|
+
source: input.source ?? "diarist",
|
|
55
|
+
payload: input.entry,
|
|
56
|
+
raw: input.entry.sourceRef.sessionFile !== undefined &&
|
|
57
|
+
input.entry.sourceRef.entryId !== undefined
|
|
58
|
+
? {
|
|
59
|
+
sessionFile: input.entry.sourceRef.sessionFile,
|
|
60
|
+
entryId: input.entry.sourceRef.entryId,
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Resolve volume paths for a ticket without writing. */
|
|
66
|
+
export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
|
|
67
|
+
const path = resolveSitianRecordPath({
|
|
68
|
+
level: "event",
|
|
69
|
+
kind: TICKET_PROVENANCE_KIND,
|
|
70
|
+
subject: ticketProvenanceSubject(ticketNumber),
|
|
71
|
+
cwd,
|
|
72
|
+
...(home === undefined ? {} : { home }),
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
recordFile: path.recordFile,
|
|
76
|
+
volumeDir: path.sessionDir,
|
|
77
|
+
humanViewFile: join(path.sessionDir, TICKET_PROVENANCE_HUMAN_VIEW),
|
|
78
|
+
offeredWatermarkFile: join(path.sessionDir, TICKET_PROVENANCE_OFFERED_WATERMARK),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Honest failure when the offered-identity watermark cannot be read as written. */
|
|
82
|
+
export class TicketProvenanceWatermarkError extends Error {
|
|
83
|
+
code = "ticket-provenance-watermark";
|
|
84
|
+
reason;
|
|
85
|
+
constructor(reason, message, options) {
|
|
86
|
+
super(message, options);
|
|
87
|
+
this.name = "TicketProvenanceWatermarkError";
|
|
88
|
+
this.reason = reason;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read identities already offered to the collector (append-only watermark).
|
|
93
|
+
* Absent file → empty set. Any non-empty line that is not a JSON object with a
|
|
94
|
+
* non-empty string `identity` throws TicketProvenanceWatermarkError (失败诚实:
|
|
95
|
+
* never interpret corruption as "unseen" and silently re-offer).
|
|
96
|
+
*/
|
|
97
|
+
export function readOfferedIdentities(ticketNumber, cwd, home) {
|
|
98
|
+
const { offeredWatermarkFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
|
|
99
|
+
if (!existsSync(offeredWatermarkFile))
|
|
100
|
+
return new Set();
|
|
101
|
+
let text;
|
|
102
|
+
try {
|
|
103
|
+
text = readFileSync(offeredWatermarkFile, "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new TicketProvenanceWatermarkError("unreadable", `ticket-provenance offered watermark unreadable (${offeredWatermarkFile})`, { cause: error });
|
|
107
|
+
}
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
const lines = text.split("\n");
|
|
110
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
111
|
+
const trimmed = lines[index].trim();
|
|
112
|
+
if (!trimmed)
|
|
113
|
+
continue;
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(trimmed);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new TicketProvenanceWatermarkError("malformed-json", `ticket-provenance offered watermark malformed JSON at line ${index + 1} (${offeredWatermarkFile})`, { cause: error });
|
|
120
|
+
}
|
|
121
|
+
if (typeof parsed !== "object" ||
|
|
122
|
+
parsed === null ||
|
|
123
|
+
Array.isArray(parsed) ||
|
|
124
|
+
typeof parsed.identity !== "string" ||
|
|
125
|
+
parsed.identity.length === 0) {
|
|
126
|
+
throw new TicketProvenanceWatermarkError("bad-shape", `ticket-provenance offered watermark bad shape at line ${index + 1} (${offeredWatermarkFile})`);
|
|
127
|
+
}
|
|
128
|
+
seen.add(parsed.identity);
|
|
129
|
+
}
|
|
130
|
+
return seen;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Append offered identities after a successful collector pass (selected or not).
|
|
134
|
+
* Idempotent per identity within the file (skip already-present). Creates volume
|
|
135
|
+
* dir only when writing the first watermark row.
|
|
136
|
+
*/
|
|
137
|
+
export function recordOfferedIdentities(input) {
|
|
138
|
+
if (input.identities.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
const { volumeDir, offeredWatermarkFile } = resolveTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
|
|
141
|
+
const already = new Set(readOfferedIdentities(input.ticketNumber, input.cwd, input.home));
|
|
142
|
+
const rows = [];
|
|
143
|
+
for (const identity of input.identities) {
|
|
144
|
+
if (identity.length === 0 || already.has(identity))
|
|
145
|
+
continue;
|
|
146
|
+
rows.push(`${JSON.stringify({ identity })}\n`);
|
|
147
|
+
already.add(identity);
|
|
148
|
+
}
|
|
149
|
+
if (rows.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
mkdirSync(volumeDir, { recursive: true });
|
|
152
|
+
appendFileSync(offeredWatermarkFile, rows.join(""), "utf8");
|
|
153
|
+
}
|
|
154
|
+
/** Read projected diary entries + diagnostics for a ticket (empty when volume absent). */
|
|
155
|
+
export async function readTicketProvenance(ticketNumber, cwd, home) {
|
|
156
|
+
const { recordFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
|
|
157
|
+
const { records } = await readSitianRecords(recordFile);
|
|
158
|
+
const entries = [];
|
|
159
|
+
const diagnostics = [];
|
|
160
|
+
let skipped = 0;
|
|
161
|
+
for (const record of records) {
|
|
162
|
+
if (record.kind !== TICKET_PROVENANCE_KIND) {
|
|
163
|
+
skipped += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Diagnostics first — same partition, separate projection (never body).
|
|
167
|
+
const diagnostic = projectTicketProvenanceDiagnostic(record.payload);
|
|
168
|
+
if (diagnostic !== undefined) {
|
|
169
|
+
diagnostics.push(diagnostic);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const entry = projectTicketProvenanceEntry(record.payload);
|
|
173
|
+
if (entry === undefined) {
|
|
174
|
+
skipped += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
entries.push(entry);
|
|
178
|
+
}
|
|
179
|
+
return { entries, diagnostics, records, recordFile, skipped };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Fence long enough that any backtick run inside `text` cannot close the block.
|
|
183
|
+
* Presentation helper only — not a machine contract; not exported.
|
|
184
|
+
*/
|
|
185
|
+
function markdownFenceFor(text) {
|
|
186
|
+
let longest = 0;
|
|
187
|
+
const runs = text.match(/`+/g);
|
|
188
|
+
if (runs !== null) {
|
|
189
|
+
for (const run of runs) {
|
|
190
|
+
if (run.length > longest)
|
|
191
|
+
longest = run.length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Render a local human-read markdown view from entries.
|
|
198
|
+
* Presentation only — machines bite JSONL. No wording lock for consumers.
|
|
199
|
+
*/
|
|
200
|
+
export function renderTicketProvenanceMarkdown(input) {
|
|
201
|
+
const lines = [
|
|
202
|
+
`# 起居录 · #${input.ticketNumber}`,
|
|
203
|
+
"",
|
|
204
|
+
`条目数:${input.entries.length}`,
|
|
205
|
+
"",
|
|
206
|
+
];
|
|
207
|
+
let index = 0;
|
|
208
|
+
for (const entry of input.entries) {
|
|
209
|
+
index += 1;
|
|
210
|
+
lines.push(`## ${index}. ${entry.sourceKind} · ${entry.timestamp}`);
|
|
211
|
+
lines.push("");
|
|
212
|
+
lines.push(`- basis.method: \`${entry.basis.method}\``);
|
|
213
|
+
if (entry.basis.anchors !== undefined && entry.basis.anchors.length > 0) {
|
|
214
|
+
lines.push(`- anchors: ${entry.basis.anchors.map((a) => `\`${a}\``).join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
if (entry.basis.note !== undefined) {
|
|
217
|
+
lines.push(`- note: ${entry.basis.note}`);
|
|
218
|
+
}
|
|
219
|
+
const refParts = [];
|
|
220
|
+
if (entry.sourceRef.sessionFile !== undefined) {
|
|
221
|
+
refParts.push(`sessionFile=${entry.sourceRef.sessionFile}`);
|
|
222
|
+
}
|
|
223
|
+
if (entry.sourceRef.entryId !== undefined) {
|
|
224
|
+
refParts.push(`entryId=${String(entry.sourceRef.entryId)}`);
|
|
225
|
+
}
|
|
226
|
+
if (entry.sourceRef.path !== undefined) {
|
|
227
|
+
refParts.push(`path=${entry.sourceRef.path}`);
|
|
228
|
+
}
|
|
229
|
+
if (entry.sourceRef.url !== undefined) {
|
|
230
|
+
refParts.push(`url=${entry.sourceRef.url}`);
|
|
231
|
+
}
|
|
232
|
+
if (refParts.length > 0) {
|
|
233
|
+
lines.push(`- sourceRef: ${refParts.join(" · ")}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push("");
|
|
236
|
+
const fence = markdownFenceFor(entry.transcript);
|
|
237
|
+
lines.push(fence);
|
|
238
|
+
lines.push(entry.transcript);
|
|
239
|
+
lines.push(fence);
|
|
240
|
+
lines.push("");
|
|
241
|
+
}
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Ensure the per-ticket JSONL volume partition exists (empty file OK).
|
|
246
|
+
* Every bound-ticket court establishes the volume even with zero diary entries
|
|
247
|
+
* (ADR 0075 ticket-provenance-file — 每票一份起居录). Does not forge entries.
|
|
248
|
+
*/
|
|
249
|
+
export function ensureTicketProvenanceVolume(ticketNumber, cwd, home) {
|
|
250
|
+
const volume = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
|
|
251
|
+
mkdirSync(volume.volumeDir, { recursive: true });
|
|
252
|
+
// Append-open creates an absent volume without ever truncating rows committed
|
|
253
|
+
// by a concurrent first writer.
|
|
254
|
+
appendFileSync(volume.recordFile, "", "utf8");
|
|
255
|
+
return volume;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Append a typed diagnostic onto the ticket-provenance volume (append-only).
|
|
259
|
+
* Same sitian kind + subject partition as diary entries; payload uses recordClass
|
|
260
|
+
* discriminator — never forged as a source entry. No sidecar / parallel ledger.
|
|
261
|
+
*/
|
|
262
|
+
export function appendTicketProvenanceDiagnostic(input) {
|
|
263
|
+
const subject = ticketProvenanceSubject(input.ticketNumber);
|
|
264
|
+
// quote-verify: content-stable identity so mid-batch retry does not duplicate.
|
|
265
|
+
// collector/issue-source: recordedAt keeps per-court failure history.
|
|
266
|
+
const identityParts = input.diagnostic.diagnosticKind === "quote-verify-failed"
|
|
267
|
+
? [
|
|
268
|
+
subject,
|
|
269
|
+
input.diagnostic.recordClass,
|
|
270
|
+
input.diagnostic.diagnosticKind,
|
|
271
|
+
input.diagnostic.cause,
|
|
272
|
+
input.diagnostic.reason ?? "",
|
|
273
|
+
]
|
|
274
|
+
: [
|
|
275
|
+
subject,
|
|
276
|
+
input.diagnostic.recordClass,
|
|
277
|
+
input.diagnostic.diagnosticKind,
|
|
278
|
+
input.diagnostic.recordedAt,
|
|
279
|
+
input.diagnostic.cause,
|
|
280
|
+
input.diagnostic.reason ?? "",
|
|
281
|
+
];
|
|
282
|
+
const identity = createHash("sha256")
|
|
283
|
+
.update(identityParts.join("\u0000"), "utf8")
|
|
284
|
+
.digest("hex");
|
|
285
|
+
return sitianReport({
|
|
286
|
+
level: "event",
|
|
287
|
+
kind: TICKET_PROVENANCE_KIND,
|
|
288
|
+
identity,
|
|
289
|
+
subject,
|
|
290
|
+
cwd: input.cwd,
|
|
291
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
292
|
+
host: input.host ?? "diarist",
|
|
293
|
+
source: input.source ?? "diarist-diagnostic",
|
|
294
|
+
payload: input.diagnostic,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** Issue-face source acquisition failure — typed diagnostic on the ticket volume. */
|
|
298
|
+
export function appendIssueSourceFailureDiagnostic(input) {
|
|
299
|
+
const recordedAt = input.recordedAt ?? new Date().toISOString();
|
|
300
|
+
return appendTicketProvenanceDiagnostic({
|
|
301
|
+
ticketNumber: input.ticketNumber,
|
|
302
|
+
cwd: input.cwd,
|
|
303
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
304
|
+
source: "diarist-issue-source",
|
|
305
|
+
diagnostic: {
|
|
306
|
+
recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
|
|
307
|
+
diagnosticKind: "issue-source-failed",
|
|
308
|
+
cause: input.cause,
|
|
309
|
+
recordedAt,
|
|
310
|
+
reason: input.reason,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/** Quote reverse-verify failure — single diagnostic expression (not a diary entry). */
|
|
315
|
+
export function appendQuoteVerifyFailureDiagnostic(input) {
|
|
316
|
+
const recordedAt = input.recordedAt ?? new Date().toISOString();
|
|
317
|
+
return appendTicketProvenanceDiagnostic({
|
|
318
|
+
ticketNumber: input.ticketNumber,
|
|
319
|
+
cwd: input.cwd,
|
|
320
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
321
|
+
source: "diarist-quote-verify",
|
|
322
|
+
diagnostic: {
|
|
323
|
+
recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
|
|
324
|
+
diagnosticKind: "quote-verify-failed",
|
|
325
|
+
cause: input.cause,
|
|
326
|
+
recordedAt,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Write the co-located human view next to the JSONL volume (derived, not dual-source).
|
|
331
|
+
* Ensures volume dir exists so empty courts still get the md face. */
|
|
332
|
+
export function writeTicketProvenanceHumanView(input) {
|
|
333
|
+
const volume = ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
|
|
334
|
+
const md = renderTicketProvenanceMarkdown({
|
|
335
|
+
ticketNumber: input.ticketNumber,
|
|
336
|
+
entries: input.entries,
|
|
337
|
+
});
|
|
338
|
+
writeFileSync(volume.humanViewFile, md, "utf8");
|
|
339
|
+
return volume.humanViewFile;
|
|
340
|
+
}
|
|
@@ -36,10 +36,15 @@ import {
|
|
|
36
36
|
formatNavigatorRoleHelp,
|
|
37
37
|
} from "../src/role-runtime.ts";
|
|
38
38
|
import { createPiJudgeAuditor } from "../src/judge-auditor.ts";
|
|
39
|
+
import { loadAuditorSoulFromSubjectInput } from "../src/auditor-soul.ts";
|
|
39
40
|
import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "../src/session-opening-materials.ts";
|
|
40
41
|
const extensionPath = fileURLToPath(import.meta.url);
|
|
41
42
|
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
42
43
|
const navigatorRoutePlaybookPath = fileURLToPath(new URL("../resources/navigator-route-playbook.md", import.meta.url));
|
|
44
|
+
// #675: nested public summons (gate/auditor/evidence-child) resolve root via env under jiti.
|
|
45
|
+
if (process.env.AK_ROLE_PACKAGE_ROOT === undefined || process.env.AK_ROLE_PACKAGE_ROOT.trim() === "") {
|
|
46
|
+
process.env.AK_ROLE_PACKAGE_ROOT = packageRoot;
|
|
47
|
+
}
|
|
43
48
|
|
|
44
49
|
// Cold `pi -e <extension> --help` must cover installed-package process startup under CI load.
|
|
45
50
|
// This bound is process-startup budget only — not settlement-to-visible presentation latency.
|
|
@@ -127,9 +132,12 @@ export default function roleRuntime(pi: ExtensionAPI): void {
|
|
|
127
132
|
loadNotarySoul: () => loadMainRoleSessionMaterials("notary"),
|
|
128
133
|
loadCountersignSoul: () => loadMainRoleSessionMaterials("countersign"),
|
|
129
134
|
loadGleanerLeftSoul: () => loadMainRoleSessionMaterials("gleaner-left"),
|
|
135
|
+
loadDiaristSoul: () => loadMainRoleSessionMaterials("diarist"),
|
|
130
136
|
loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
|
|
131
137
|
loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
|
|
132
138
|
loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
|
|
139
|
+
loadAuditorSoul: () => loadAuditorSoulFromSubjectInput(),
|
|
140
|
+
loadEvidenceChildSoul: () => loadMainRoleSessionMaterials("evidence-child"),
|
|
133
141
|
loadNotarySourceRun: loadNotarySourceRunLocator,
|
|
134
142
|
loadNavigatorWorkContext: (options) => loadNavigatorWorkContext(pi, options),
|
|
135
143
|
createNavigatorAttendance: (options) => {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# 起居郎收集方法(owner-domain)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本文件是起居郎语义收集的方法真源(ADR 0075 `diarist-llm-collector` / `transcribe-whole-blocks`)。
|
|
4
4
|
|
|
5
5
|
## 任务
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
从本票已冻结的来源块文件中,选出**与本票决策相关**的整块。文件路径见本轮材料中的路径指针,自行读取;不要等别人把正文贴进对话。
|
|
8
8
|
|
|
9
9
|
## 何谓决策相关
|
|
10
10
|
|
|
@@ -14,16 +14,12 @@
|
|
|
14
14
|
- 命名、工序先后、调用面、闸位、存量是否补档等裁决
|
|
15
15
|
- 对既有 ADR / 法源的采纳、修正或明确不适用
|
|
16
16
|
- 陛下(owner)原话、确认、否决、收口条件
|
|
17
|
-
-
|
|
17
|
+
- 为上述裁决提供必要上下文的紧邻块
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
无关闲聊、纯工具噪声、与本票决策无涉的他票材料不选。
|
|
20
20
|
|
|
21
21
|
## 誊录整块
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
23
|
+
- 入选即整块保留:指向文件中的完整 transcript,不指针化、不摘要替代正文。
|
|
24
|
+
- 引语必须是该块 transcript 内的**连续原文子串**。拼接、改写、跨段拼贴会在机械反验中失败并拒入录。
|
|
25
25
|
- 宁多勿漏:拿不准是否决策相关时入选,由下游反验与人读面承接。
|
|
26
|
-
|
|
27
|
-
## 输出
|
|
28
|
-
|
|
29
|
-
只输出一个 JSON 对象,形状由机器 prompt 给出的 `selections` 约定。不要输出 JSON 以外的文字。
|
|
@@ -14,8 +14,9 @@ const entries = [
|
|
|
14
14
|
// Navigator package-graph dependencies used by attendance settlement.
|
|
15
15
|
// navigator-session-contracts is a static import of the published attendance root
|
|
16
16
|
// (#590: published non-bundle graph must stay closed under its own relative edges).
|
|
17
|
-
"
|
|
17
|
+
"navigator-public-session",
|
|
18
18
|
"navigator-session-contracts",
|
|
19
|
+
"public-role-summons",
|
|
19
20
|
"pi/in-process-session",
|
|
20
21
|
"activation-ledger-git",
|
|
21
22
|
"activation-ledger-topology",
|
package/souls/diarist.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Diarist Soul(起居郎)
|
|
2
|
+
|
|
3
|
+
你为本案找齐并整理决策依据,收进本票已有的起居录。你只记录已经作出的决定,不立法、不批准设计、不指挥施工。
|
|
4
|
+
|
|
5
|
+
- 相关性由你裁决:不把票面已列出的材料当完整候选集合,也不以票号、引语或关键词未命中作为排除依据。
|
|
6
|
+
- 分清身份:陛下(owner)的决定、助手的提议、正式裁决、你自己的整理解释,四者不得混同;建议不得写成授权。
|
|
7
|
+
- 导航而非重写:已有设计与法源以原件指针加简短关系说明引用,不复制成第二份规范。
|
|
8
|
+
- 分清现行与历史:被修订替代的决定保留出处,但不与当前方向并列。
|
|
9
|
+
- 缺录、不可读、引用无法核实时如实说明,不猜成事实,不以缺失充当"没有问题"。
|
package/souls/doctor-auditor.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 取证子
|
|
@@ -23,6 +23,7 @@ import { loadPackagedCanonicalSkillBinding } from "../package-resources/method-s
|
|
|
23
23
|
import { createPerDispatchReviewerAgent } from "../reviewer-agent.ts";
|
|
24
24
|
import { formatNavigatorRoleHelp, type RoleRuntimeDependencies } from "../role-runtime.ts";
|
|
25
25
|
import { createReviewerPinnedGitReader } from "../reviewer-pinned-git.ts";
|
|
26
|
+
import { loadAuditorSoulFromSubjectInput } from "../auditor-soul.ts";
|
|
26
27
|
import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "../session-opening-materials.ts";
|
|
27
28
|
import { acpStdioArgs, resolveAcpBinary, type AcpHostDescription } from "./description.ts";
|
|
28
29
|
import { createComposedAcpRoleTurnHost } from "./role-envelope.ts";
|
|
@@ -61,9 +62,12 @@ export function createAcpRoleRuntimeDependencies(packageRoot: string): RoleRunti
|
|
|
61
62
|
loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
|
|
62
63
|
loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
|
|
63
64
|
loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
|
|
65
|
+
loadAuditorSoul: () => loadAuditorSoulFromSubjectInput(),
|
|
66
|
+
loadEvidenceChildSoul: () => loadMainRoleSessionMaterials("evidence-child"),
|
|
64
67
|
loadNotarySoul: () => loadMainRoleSessionMaterials("notary"),
|
|
65
68
|
loadCountersignSoul: () => loadMainRoleSessionMaterials("countersign"),
|
|
66
69
|
loadGleanerLeftSoul: () => loadMainRoleSessionMaterials("gleaner-left"),
|
|
70
|
+
loadDiaristSoul: () => loadMainRoleSessionMaterials("diarist"),
|
|
67
71
|
loadNotarySourceRun: loadNotarySourceRunLocator,
|
|
68
72
|
loadMergerSoul: () => loadMainRoleSessionMaterials("merger"),
|
|
69
73
|
loadMergerInput: async (path) => JSON.parse(await readFile(path, "utf8")),
|
|
@@ -4,7 +4,7 @@ import { createServer, type Server, type Socket } from "node:net";
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
import { requireGatekeeperPass } from "../gatekeeper-
|
|
7
|
+
import { requireGatekeeperPass } from "../gatekeeper-pass-envelope.ts";
|
|
8
8
|
import type {
|
|
9
9
|
HostContext,
|
|
10
10
|
HostEventRegistration,
|
|
@@ -78,7 +78,8 @@ export function projectAcpActivationFlags(request: RoleTurnRequest): Map<string,
|
|
|
78
78
|
: "packetPath" in activation ? activation.packetPath
|
|
79
79
|
: "casePath" in activation ? activation.casePath
|
|
80
80
|
: "inputPath" in activation ? activation.inputPath
|
|
81
|
-
: "sourceRun" in activation ? activation.sourceRun
|
|
81
|
+
: "sourceRun" in activation ? activation.sourceRun
|
|
82
|
+
: "sourcesPath" in activation ? activation.sourcesPath : undefined;
|
|
82
83
|
if (path !== undefined) flags.set(inputFlag, path);
|
|
83
84
|
}
|
|
84
85
|
if (activation.role === "fixer" && activation.prerequisitesPath !== undefined) flags.set("ak-fixer-prerequisites", activation.prerequisitesPath);
|
|
@@ -297,7 +298,7 @@ export async function prepareAcpRoleEnvelope(options: {
|
|
|
297
298
|
if (record.kind === "role_infrastructure_failure") return;
|
|
298
299
|
const code = typeof record.code === "string" && record.code.length > 0
|
|
299
300
|
? record.code
|
|
300
|
-
: record.status === "bounce" || record.status === "no_receipt"
|
|
301
|
+
: record.status === "bounce" || record.status === "no_receipt" || record.status === "unreadable"
|
|
301
302
|
? record.status
|
|
302
303
|
: undefined;
|
|
303
304
|
if (code === undefined) return;
|
|
@@ -270,7 +270,15 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
270
270
|
priorNativePaths !== undefined && priorNativePaths.length > 0
|
|
271
271
|
? `${prepared.prompt}\n${priorNativePaths.join("\n")}`
|
|
272
272
|
: prepared.prompt;
|
|
273
|
-
|
|
273
|
+
// Envelope infra abort and parent cancellation (#675 nested summons) are
|
|
274
|
+
// the same race face here: either one ends the ACP round and the finally
|
|
275
|
+
// block cancels + closes the child session.
|
|
276
|
+
const abortSignal =
|
|
277
|
+
request.signal === undefined
|
|
278
|
+
? prepared.abortSignal
|
|
279
|
+
: prepared.abortSignal === undefined
|
|
280
|
+
? request.signal
|
|
281
|
+
: AbortSignal.any([prepared.abortSignal, request.signal]);
|
|
274
282
|
const activeConnection = connection;
|
|
275
283
|
|
|
276
284
|
/** Race ACP prompt against envelope abort so infra failInfrastructure cannot hang (#593). */
|
|
@@ -434,8 +434,34 @@ function pairGateRounds(
|
|
|
434
434
|
.map((round, index) => ({ ...round, roundIndex: index + 1 }));
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
/** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
|
|
438
|
+
async function resolveOfficerSessionFromPointerFile(
|
|
439
|
+
pointerPath: string,
|
|
440
|
+
): Promise<string | undefined> {
|
|
441
|
+
const { readFile } = await import("node:fs/promises");
|
|
442
|
+
let raw: unknown;
|
|
443
|
+
try {
|
|
444
|
+
raw = JSON.parse(await readFile(pointerPath, "utf8"));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`direct officer run pointer unreadable in ${pointerPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
448
|
+
{ cause: error },
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (!isRecord(raw) || raw.kind !== "direct-officer-run-pointer" || raw.version !== 1) {
|
|
452
|
+
throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
|
|
453
|
+
}
|
|
454
|
+
const sessionFile = raw.sessionFile;
|
|
455
|
+
if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
|
|
456
|
+
throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
|
|
457
|
+
}
|
|
458
|
+
return sessionFile;
|
|
459
|
+
}
|
|
460
|
+
|
|
437
461
|
/**
|
|
438
462
|
* Read and pair gate-cycle rounds from one or more auditor-roles directories.
|
|
463
|
+
* Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
|
|
464
|
+
* files that name the independent officer session 正本.
|
|
439
465
|
* ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
|
|
440
466
|
* (failure honesty — damaged topology must not wash to zero rounds).
|
|
441
467
|
*
|
|
@@ -456,7 +482,11 @@ export async function readAnalystGateCyclesFromAuditorRoles(
|
|
|
456
482
|
try {
|
|
457
483
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
458
484
|
names = entries
|
|
459
|
-
.filter(
|
|
485
|
+
.filter(
|
|
486
|
+
(e) =>
|
|
487
|
+
e.isFile()
|
|
488
|
+
&& (e.name.endsWith(".jsonl") || e.name.endsWith(".pointer.json")),
|
|
489
|
+
)
|
|
460
490
|
.map((e) => e.name)
|
|
461
491
|
.sort();
|
|
462
492
|
} catch (error) {
|
|
@@ -464,7 +494,12 @@ export async function readAnalystGateCyclesFromAuditorRoles(
|
|
|
464
494
|
throw error;
|
|
465
495
|
}
|
|
466
496
|
for (const name of names) {
|
|
467
|
-
const
|
|
497
|
+
const path = join(directory, name);
|
|
498
|
+
const sessionPath = name.endsWith(".pointer.json")
|
|
499
|
+
? await resolveOfficerSessionFromPointerFile(path)
|
|
500
|
+
: path;
|
|
501
|
+
if (sessionPath === undefined) continue;
|
|
502
|
+
const classified = await classifyAuditorVolume(sessionPath);
|
|
468
503
|
for (const volume of classified) {
|
|
469
504
|
if (
|
|
470
505
|
options.parentSessionFile !== undefined &&
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 调用方只声明自己是谁的什么;落点由候簿拓扑算出,签名不含任何落点/路径参数。
|
|
4
4
|
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, resolve, join } from "node:path";
|
|
8
8
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
@@ -224,3 +224,47 @@ export function createRecordSessionOpen(options: CreateRecordSessionOptions): Re
|
|
|
224
224
|
export function createRecordSession(options: CreateRecordSessionOptions): SessionManager {
|
|
225
225
|
return createRecordSessionOpen(options).session;
|
|
226
226
|
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
/** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
|
|
230
|
+
export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
|
|
231
|
+
|
|
232
|
+
export type DirectOfficerRunPointer = {
|
|
233
|
+
readonly version: 1;
|
|
234
|
+
readonly kind: typeof DIRECT_OFFICER_RUN_POINTER_KIND;
|
|
235
|
+
readonly officer: "inspector" | "notary";
|
|
236
|
+
/** Absolute path to the officer session.jsonl 正本. */
|
|
237
|
+
readonly sessionFile: string;
|
|
238
|
+
/** Officer run directory when known. */
|
|
239
|
+
readonly runDirectory?: string;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Book a typed pointer under parent session/auditor-roles (same nest owner as
|
|
244
|
+
* createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
|
|
245
|
+
* Directory placement stays with the archivist record entry (ADR 0018 / 0065).
|
|
246
|
+
*/
|
|
247
|
+
export function bookDirectOfficerRunPointer(options: {
|
|
248
|
+
readonly parentSessionFile: string;
|
|
249
|
+
readonly officer: "inspector" | "notary";
|
|
250
|
+
readonly sessionFile: string;
|
|
251
|
+
readonly runDirectory?: string;
|
|
252
|
+
}): DirectOfficerRunPointer {
|
|
253
|
+
const nest = join(dirname(options.parentSessionFile), "auditor-roles");
|
|
254
|
+
mkdirSync(nest, { recursive: true });
|
|
255
|
+
const pointer: DirectOfficerRunPointer = {
|
|
256
|
+
version: 1,
|
|
257
|
+
kind: DIRECT_OFFICER_RUN_POINTER_KIND,
|
|
258
|
+
officer: options.officer,
|
|
259
|
+
sessionFile: options.sessionFile,
|
|
260
|
+
...(options.runDirectory !== undefined && options.runDirectory.trim() !== ""
|
|
261
|
+
? { runDirectory: options.runDirectory }
|
|
262
|
+
: {}),
|
|
263
|
+
};
|
|
264
|
+
writeFileSync(
|
|
265
|
+
join(nest, `${options.officer}-${Date.now().toString(36)}.pointer.json`),
|
|
266
|
+
`${JSON.stringify(pointer)}\n`,
|
|
267
|
+
"utf8",
|
|
268
|
+
);
|
|
269
|
+
return pointer;
|
|
270
|
+
}
|
package/src/audit-escalation.ts
CHANGED
|
@@ -175,6 +175,14 @@ export type ComplianceDecisionHandlers<T> = {
|
|
|
175
175
|
facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
|
|
176
176
|
usageProjection: { usage?: Usage },
|
|
177
177
|
) => T | PromiseLike<T>;
|
|
178
|
+
/**
|
|
179
|
+
* Parent work stands with typed unreadable audit fact (ADR 0055 / §0).
|
|
180
|
+
* Must not collapse into ordinary pass (forged role decision).
|
|
181
|
+
*/
|
|
182
|
+
unreadable?: (
|
|
183
|
+
facts: Extract<ComplianceDecision, { status: "unreadable" }>,
|
|
184
|
+
usageProjection: { usage?: Usage },
|
|
185
|
+
) => T | PromiseLike<T>;
|
|
178
186
|
revise: (violations: readonly unknown[]) => T | PromiseLike<T>;
|
|
179
187
|
escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
|
|
180
188
|
};
|
|
@@ -201,6 +209,15 @@ export async function disposeComplianceDecision<T>(
|
|
|
201
209
|
decision,
|
|
202
210
|
decision.usage === undefined ? {} : { usage: decision.usage },
|
|
203
211
|
);
|
|
212
|
+
case "unreadable":
|
|
213
|
+
// Parent candidate stands; audit shape failure is a typed parallel fact — not pass.
|
|
214
|
+
if (handlers.unreadable === undefined) {
|
|
215
|
+
throw new Error("Compliance unreadable projection handler is unavailable");
|
|
216
|
+
}
|
|
217
|
+
return await handlers.unreadable(
|
|
218
|
+
decision,
|
|
219
|
+
decision.usage === undefined ? {} : { usage: decision.usage },
|
|
220
|
+
);
|
|
204
221
|
case "revise":
|
|
205
222
|
return await handlers.revise(decision.violations);
|
|
206
223
|
case "escalate":
|