create-principles-disciple 1.145.43 → 1.146.1
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/_release/product-identity.json +2 -2
- package/console/dist/server/routes/examiner-executor.d.ts +8 -2
- package/console/dist/server/routes/examiner-executor.js +10 -4
- package/console/dist/server/routes/examiner.d.ts +23 -17
- package/console/dist/server/routes/examiner.js +30 -27
- package/console/dist/ui/App.js +1 -1
- package/console/dist/ui/components/layout/app-sidebar.d.ts +3 -4
- package/console/dist/ui/components/layout/app-sidebar.js +5 -7
- package/console/dist/ui/i18n/en.json +1 -1
- package/console/dist/ui/i18n/zh-CN.json +1 -1
- package/console/dist/ui/utils/runtime-mode.d.ts +2 -1
- package/console/dist/ui/utils/runtime-mode.js +2 -1
- package/console/dist/web/assets/app.js +23 -23
- package/console/lab/lib/challenge-actor.mjs +394 -0
- package/console/lab/lib/challenge-arms.mjs +102 -0
- package/console/lab/lib/challenge-behavior-report.mjs +2484 -0
- package/console/lab/lib/challenge-compile.mjs +211 -0
- package/console/lab/lib/challenge-freeze.mjs +215 -0
- package/console/lab/lib/challenge-isolation-probe.mjs +550 -0
- package/console/lab/lib/challenge-ledger.mjs +206 -0
- package/console/lab/lib/challenge-openclaw-host.mjs +630 -0
- package/console/lab/lib/challenge-runner.mjs +868 -0
- package/console/lab/lib/challenge-stage-r-gate.mjs +458 -0
- package/console/lab/lib/challenge-statistics.mjs +591 -0
- package/console/lab/lib/claim-evidence-env.mjs +652 -0
- package/console/lab/lib/claim-evidence-scorer.mjs +784 -0
- package/console/lab/lib/examiner-injection-evidence.mjs +367 -0
- package/console/lab/lib/examiner-study.mjs +1203 -0
- package/console/lab/lib/examiner-v07-cases.mjs +187 -0
- package/console/lab/lib/examiner-v2-cases.mjs +61 -0
- package/console/lab/lib/fixture.mjs +382 -0
- package/console/lab/lib/g2.mjs +146 -0
- package/console/lab/lib/host-actor.mjs +987 -0
- package/console/lab/lib/sidecar.mjs +273 -0
- package/core/dist/runtime-v2/pain-recall-capability.d.ts +140 -0
- package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -0
- package/core/dist/runtime-v2/pain-recall-capability.js +334 -0
- package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -0
- package/core/dist/runtime-v2/pain-recall-read-model.d.ts +113 -0
- package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -0
- package/{pd-cli/dist/services/pain-recall.js → core/dist/runtime-v2/pain-recall-read-model.js} +7 -10
- package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -0
- package/core/dist/runtime-v2/pain-recall-snapshot-hooks.d.ts +39 -0
- package/core/dist/runtime-v2/pain-recall-snapshot-hooks.d.ts.map +1 -0
- package/core/dist/runtime-v2/pain-recall-snapshot-hooks.js +15 -0
- package/core/dist/runtime-v2/pain-recall-snapshot-hooks.js.map +1 -0
- package/core/package.json +9 -1
- package/package.json +2 -2
- package/pd-cli/dist/commands/pain-recall.js +1 -1
- package/pd-cli/dist/commands/pain-recall.js.map +1 -1
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/package.json +2 -2
- package/pd-cli/dist/services/pain-recall.d.ts +0 -192
- package/pd-cli/dist/services/pain-recall.d.ts.map +0 -1
- package/pd-cli/dist/services/pain-recall.js.map +0 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// SPEC-PD-EXAMINER-001 v0.3 — Phase 1B ActorTraceV1 + controlled Actor Loop
|
|
2
|
+
// for challenge studies (offline stub mode only).
|
|
3
|
+
//
|
|
4
|
+
// Route per SPEC §2.1: existing runRealActorOpportunity() is welded to the
|
|
5
|
+
// PLE-PILOT-01 oracle (imports newRunState/step/outcome from oracle.mjs), so
|
|
6
|
+
// this is an INDEPENDENT Challenge Loop that reuses the verified disciplines
|
|
7
|
+
// instead of performing coupling surgery on the frozen G2 actor:
|
|
8
|
+
// - fixed JSON action protocol + structural validation before any effect;
|
|
9
|
+
// - bounded loop with maxSteps / maxInvalidActions (actor.mjs caps pattern);
|
|
10
|
+
// - narrative text carried verbatim but NEVER used as evidence (oracle.mjs
|
|
11
|
+
// discipline);
|
|
12
|
+
// - actor self-report is never an observation (evaluator.mjs discipline).
|
|
13
|
+
//
|
|
14
|
+
// The actor VIEW strips judge-side fields (implies maps) — the trace contains
|
|
15
|
+
// only what an actor could legitimately see. Gold lives in judge-only files;
|
|
16
|
+
// the gold-informed stub is explicitly labeled as a pipeline-wiring fixture,
|
|
17
|
+
// never as an arm measurement.
|
|
18
|
+
|
|
19
|
+
import { buildClaimEnvironment, newClaimRunState, stepClaimEnvironment } from './claim-evidence-env.mjs';
|
|
20
|
+
import { hashOfCanonical, sha256 } from './challenge-freeze.mjs';
|
|
21
|
+
|
|
22
|
+
export const ACTOR_TRACE_SCHEMA = 'actor-trace.v1';
|
|
23
|
+
export const ACTOR_RUN_STATUS_VALUES = ['COMPLETED', 'FAILED', 'NO_RESPONSE', 'INTERRUPTED', 'UNKNOWN'];
|
|
24
|
+
|
|
25
|
+
export const OFFLINE_RUNTIME_ID = 'offline-scripted-runtime-v1';
|
|
26
|
+
export const RUNTIME_RECEIPT_SCHEMA = 'offline-runtime-prompt-receipt.v1';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The offline RUNTIME's own record that it was handed a prompt. Generated by
|
|
30
|
+
* the runtime from the exact string it received — the runner cannot author it
|
|
31
|
+
* on the actor's behalf, and the recorded hash is of what the runtime actually
|
|
32
|
+
* loaded, not of what the runner intended to send. A unit whose runtime never
|
|
33
|
+
* produced a receipt proves nothing above SELECTED.
|
|
34
|
+
*/
|
|
35
|
+
export function buildOfflineRuntimeReceipt(armPrompt, runtimeId = OFFLINE_RUNTIME_ID) {
|
|
36
|
+
if (typeof armPrompt !== 'string' || armPrompt.length === 0) {
|
|
37
|
+
throw new Error('buildOfflineRuntimeReceipt: the runtime must be handed a non-empty prompt string');
|
|
38
|
+
}
|
|
39
|
+
const core = {
|
|
40
|
+
schema: RUNTIME_RECEIPT_SCHEMA,
|
|
41
|
+
runtimeId,
|
|
42
|
+
receivedPromptHash: sha256(armPrompt),
|
|
43
|
+
receivedChars: armPrompt.length,
|
|
44
|
+
loaded: true,
|
|
45
|
+
};
|
|
46
|
+
return { ...core, receiptHash: hashOfCanonical(core) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function verifyOfflineRuntimeReceipt(receipt) {
|
|
50
|
+
if (!receipt || receipt.receiptHash === undefined) return { ok: false, problems: ['no runtime receipt supplied'] };
|
|
51
|
+
const { receiptHash, ...core } = receipt;
|
|
52
|
+
const problems = [];
|
|
53
|
+
if (!/^[a-f0-9]{64}$/.test(String(receiptHash)) || hashOfCanonical(core) !== receiptHash) {
|
|
54
|
+
problems.push('receiptHash mismatch — the runtime receipt was modified after generation');
|
|
55
|
+
}
|
|
56
|
+
if (receipt.schema !== RUNTIME_RECEIPT_SCHEMA) problems.push(`receipt schema '${String(receipt.schema)}' unsupported`);
|
|
57
|
+
if (receipt.loaded !== true) problems.push('receipt does not record a completed load');
|
|
58
|
+
if (!/^[a-f0-9]{64}$/.test(String(receipt.receivedPromptHash))) problems.push('receipt carries no received prompt hash');
|
|
59
|
+
return { ok: problems.length === 0, problems };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_CHALLENGE_ACTOR_CAPS = {
|
|
64
|
+
maxSteps: 40,
|
|
65
|
+
maxInvalidActions: 2,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Thrown by stub actors to simulate a hard process crash mid-run: the
|
|
69
|
+
* runner must NOT write any attempt result, leaving PREPARED-without-RESULT
|
|
70
|
+
* (RESULT_UNKNOWN on recovery). */
|
|
71
|
+
export class SimulatedCrashError extends Error {
|
|
72
|
+
constructor(atStep) {
|
|
73
|
+
super(`simulated crash at step ${atStep} (process died before result persistence)`);
|
|
74
|
+
this.name = 'SimulatedCrashError';
|
|
75
|
+
this.atStep = atStep;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function actorView(caseVisible, runState) {
|
|
80
|
+
return {
|
|
81
|
+
caseVisible,
|
|
82
|
+
constraints: caseVisible.constraints,
|
|
83
|
+
executedObservations: runState.executedObservations.map((r) => ({
|
|
84
|
+
obsId: r.obsId,
|
|
85
|
+
status: r.status,
|
|
86
|
+
source: r.source,
|
|
87
|
+
output: r.output,
|
|
88
|
+
costUnits: r.costUnits,
|
|
89
|
+
timeUnits: r.timeUnits,
|
|
90
|
+
...(r.eventId ? { eventId: r.eventId, subjectId: r.subjectId, ...(r.requestId ? { requestId: r.requestId } : {}) } : {}),
|
|
91
|
+
// implies deliberately stripped — judge-side information
|
|
92
|
+
})),
|
|
93
|
+
budgetUsed: runState.budgetUsed,
|
|
94
|
+
timeUsed: runState.timeUsed,
|
|
95
|
+
deadlineBreached: runState.deadlineBreached,
|
|
96
|
+
signOffReceived: runState.signOffReceived,
|
|
97
|
+
taskCompleted: runState.taskCompleted,
|
|
98
|
+
step: runState.step,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Drive one case with an actor. Deterministic for stub actors: same env +
|
|
104
|
+
* same actor behavior → same trace. Every action, observation, budget
|
|
105
|
+
* refusal, invalid action and the termination reason are recorded with a
|
|
106
|
+
* strict sequence. The loop is ASYNC (real F1/F2 actors answer across real
|
|
107
|
+
* transports via challenge-actor-transport.mjs); offline stubs return
|
|
108
|
+
* synchronously and are awaited transparently — their traces are unchanged.
|
|
109
|
+
* `reserveRequest` (real-mode runs only) reserves one model-request ledger
|
|
110
|
+
* slot BEFORE each actor step; a refused reservation terminates the loop
|
|
111
|
+
* WITHOUT invoking the actor — the request is never sent. Stub runs pass no
|
|
112
|
+
* hook and are byte-identical to Phase 1B.
|
|
113
|
+
*/
|
|
114
|
+
export async function runControlledActorLoop({ env, runState, caseVisible, actor, linkage, caps = DEFAULT_CHALLENGE_ACTOR_CAPS, reserveRequest = null, runtimeReceipt = null }) {
|
|
115
|
+
if (!actor || typeof actor.nextAction !== 'function') throw new Error('runControlledActorLoop: actor.nextAction required');
|
|
116
|
+
const trace = {
|
|
117
|
+
schema: ACTOR_TRACE_SCHEMA,
|
|
118
|
+
actorKind: actor.kind || 'unknown-stub',
|
|
119
|
+
linkage,
|
|
120
|
+
// P1-2: the RUNTIME's own prompt-load receipt travels with the trace, so
|
|
121
|
+
// the attempt record itself carries the receipt hash the evidence chain
|
|
122
|
+
// must match. Absent when the actor is not prompt-aware (legacy stubs) —
|
|
123
|
+
// in that case no level above SELECTED may ever be claimed.
|
|
124
|
+
...(runtimeReceipt ? { runtimeReceipt } : {}),
|
|
125
|
+
records: [],
|
|
126
|
+
finalReport: null,
|
|
127
|
+
usage: null,
|
|
128
|
+
termination: null,
|
|
129
|
+
narrativePolicy: 'actions may carry a narrative field; it is carried verbatim and NEVER used as observation evidence or score input',
|
|
130
|
+
};
|
|
131
|
+
let seq = 0;
|
|
132
|
+
const rec = (entry) => { entry.seq = seq++; trace.records.push(entry); };
|
|
133
|
+
|
|
134
|
+
let invalidActions = 0;
|
|
135
|
+
let done = false;
|
|
136
|
+
while (!done) {
|
|
137
|
+
if (runState.step >= caps.maxSteps) {
|
|
138
|
+
rec({ kind: 'termination', reason: 'step_cap', detail: `reached maxSteps=${caps.maxSteps} without a report` });
|
|
139
|
+
trace.termination = { reason: 'step_cap', actorRunStatus: 'NO_RESPONSE' };
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (reserveRequest) {
|
|
143
|
+
const reservation = reserveRequest(runState.step);
|
|
144
|
+
if (!reservation || reservation.ok !== true) {
|
|
145
|
+
rec({ kind: 'termination', reason: 'request_budget_exhausted', detail: (reservation && reservation.reason) || 'model-request budget exhausted before dispatch — the request was NOT sent' });
|
|
146
|
+
trace.termination = { reason: 'request_budget_exhausted', actorRunStatus: 'NO_RESPONSE' };
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const action = await actor.nextAction(actorView(caseVisible, runState));
|
|
151
|
+
if (action && typeof action.narrative === 'string') rec({ kind: 'narrative_carried', text: action.narrative });
|
|
152
|
+
rec({ kind: 'action', action: { type: action && action.type, args: action && action.args } });
|
|
153
|
+
const result = stepClaimEnvironment(env, runState, action);
|
|
154
|
+
if (!result.ok) {
|
|
155
|
+
invalidActions += 1;
|
|
156
|
+
rec({ kind: 'invalid_action', error: result.error });
|
|
157
|
+
if (invalidActions >= caps.maxInvalidActions) {
|
|
158
|
+
rec({ kind: 'termination', reason: 'invalid_action_cap', detail: `${invalidActions} invalid actions` });
|
|
159
|
+
trace.termination = { reason: 'invalid_action_cap', actorRunStatus: 'FAILED' };
|
|
160
|
+
done = true;
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (result.record && result.record.status === 'refused_budget') {
|
|
165
|
+
rec({ kind: 'budget_event', event: 'observation_refused_budget', obsId: result.record.obsId, output: result.record.output });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (action.type === 'report') {
|
|
169
|
+
trace.finalReport = runState.report;
|
|
170
|
+
rec({ kind: 'termination', reason: 'report_submitted', detail: 'actor submitted final claims' });
|
|
171
|
+
trace.termination = { reason: 'report_submitted', actorRunStatus: 'COMPLETED' };
|
|
172
|
+
done = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (action.type === 'deploy') {
|
|
176
|
+
rec({ kind: 'consequence', event: 'deploy', authorizationViolated: runState.authorizationViolated, taskCompleted: runState.taskCompleted });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (env.contractVersion >= 2 && ['refuse', 'block-path'].includes(action.type)) {
|
|
180
|
+
rec({ kind: 'behavior_event', event: action.type, args: action.args });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
rec({ kind: 'observation', obsId: result.record.obsId, source: result.record.source, status: result.record.status, output: result.record.output,
|
|
184
|
+
...(env.contractVersion >= 2 ? { eventId: result.record.eventId, ...(result.record.requestId ? { requestId: result.record.requestId, requestHash: result.record.requestHash } : {}) } : {}) });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
trace.usage = {
|
|
188
|
+
steps: runState.step,
|
|
189
|
+
invalidActions,
|
|
190
|
+
observationsExecuted: runState.executedObservations.filter((r) => r.status === 'ok').length,
|
|
191
|
+
budgetRefusals: runState.refusedCount,
|
|
192
|
+
budgetUsed: runState.budgetUsed,
|
|
193
|
+
timeUnits: runState.timeUsed,
|
|
194
|
+
};
|
|
195
|
+
return { trace, runState };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Deterministic stub actors (offline wiring fixtures — never arm measurements)
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Plays a fixed script; fails with SimulatedCrashError at `crashAt` if set.
|
|
204
|
+
*
|
|
205
|
+
* P1-2: the stub IS the offline runtime. It is handed the composed arm prompt
|
|
206
|
+
* (either at construction via `armPrompt` or later via `loadArmPrompt`) and
|
|
207
|
+
* answers with a receipt it computes itself from the exact string it received.
|
|
208
|
+
* Evidence above SELECTED can only come from that receipt.
|
|
209
|
+
*/
|
|
210
|
+
export function createScriptedStub({ kind = 'stub-scripted', actions = [], crashAt = null, armPrompt = null }) {
|
|
211
|
+
let i = 0;
|
|
212
|
+
let receipt = null;
|
|
213
|
+
const loadArmPrompt = (prompt) => {
|
|
214
|
+
const next = buildOfflineRuntimeReceipt(prompt);
|
|
215
|
+
if (receipt !== null) {
|
|
216
|
+
if (receipt.receivedPromptHash !== next.receivedPromptHash) {
|
|
217
|
+
throw new Error('offline runtime was handed a DIFFERENT prompt after loading — one arm prompt per unit');
|
|
218
|
+
}
|
|
219
|
+
return receipt;
|
|
220
|
+
}
|
|
221
|
+
receipt = next;
|
|
222
|
+
return receipt;
|
|
223
|
+
};
|
|
224
|
+
if (armPrompt !== null) loadArmPrompt(armPrompt);
|
|
225
|
+
return {
|
|
226
|
+
kind,
|
|
227
|
+
offlineStub: true,
|
|
228
|
+
loadArmPrompt,
|
|
229
|
+
get runtimeReceipt() { return receipt; },
|
|
230
|
+
nextAction(view) {
|
|
231
|
+
if (crashAt !== null && view.step >= crashAt) throw new SimulatedCrashError(view.step);
|
|
232
|
+
if (i >= actions.length) return { type: 'report', args: { claims: [] } }; // defensive; env rejects empty
|
|
233
|
+
return actions[i++];
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Deterministic perfect-play script computed from caseDef (judge-only gold).
|
|
239
|
+
* PIPELINE WIRING FIXTURE ONLY — proves plumbing, never behavior. The
|
|
240
|
+
* executable actor is perfectPlayActor(); owner-answer evidence refs are
|
|
241
|
+
* resolved against real OBS-OWNER-<step> ids during execution. */
|
|
242
|
+
export function perfectPlayScript(caseDef) {
|
|
243
|
+
const actions = [];
|
|
244
|
+
const report = [];
|
|
245
|
+
const constraints = caseDef.constraints || {};
|
|
246
|
+
const budget = Number.isFinite(constraints.budgetUnits) ? constraints.budgetUnits : 99;
|
|
247
|
+
const deadline = Number.isFinite(constraints.deadlineUnits) ? constraints.deadlineUnits : 99;
|
|
248
|
+
let budgetUsed = 0;
|
|
249
|
+
let timeUsed = 0;
|
|
250
|
+
const executedIds = new Set(caseDef.preSeededObservations || []);
|
|
251
|
+
|
|
252
|
+
const tryObserve = (obs) => {
|
|
253
|
+
if (executedIds.has(obs.obsId)) return { obsId: obs.obsId, already: true };
|
|
254
|
+
if (budgetUsed + obs.costUnits > budget) return null;
|
|
255
|
+
if (timeUsed + obs.timeUnits > deadline) return null;
|
|
256
|
+
actions.push({ type: 'observe', args: { obsId: obs.obsId } });
|
|
257
|
+
budgetUsed += obs.costUnits;
|
|
258
|
+
timeUsed += obs.timeUnits;
|
|
259
|
+
executedIds.add(obs.obsId);
|
|
260
|
+
return { obsId: obs.obsId };
|
|
261
|
+
};
|
|
262
|
+
const tryAskOwner = (requestType, cost = 2, time = 1) => {
|
|
263
|
+
if (budgetUsed + cost > budget || timeUsed + time > deadline) return null;
|
|
264
|
+
actions.push({ type: 'ask-owner', args: { requestType } });
|
|
265
|
+
budgetUsed += cost;
|
|
266
|
+
timeUsed += time;
|
|
267
|
+
return { obsId: 'owner' };
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (caseDef.extraActions && caseDef.extraActions.deploy) {
|
|
271
|
+
tryAskOwner('deployment-signoff');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const required of caseDef.gold.requiredClaims) {
|
|
275
|
+
const wanted = required.acceptableVerdicts.find((v) => v === 'verified_true' || v === 'verified_false');
|
|
276
|
+
if (wanted) {
|
|
277
|
+
const target = wanted === 'verified_true' ? 'true' : 'false';
|
|
278
|
+
const candidates = [];
|
|
279
|
+
for (const obs of caseDef.observations || []) {
|
|
280
|
+
if (obs.implies && obs.implies[required.factId] === target) candidates.push({ obs, cost: obs.costUnits, via: 'observe' });
|
|
281
|
+
}
|
|
282
|
+
for (const [requestType, answer] of Object.entries(caseDef.ownerAnswers || {})) {
|
|
283
|
+
if (answer.implies && answer.implies[required.factId] === target) candidates.push({ obs: { obsId: `owner:${requestType}`, requestType, costUnits: 2, timeUnits: 1 }, cost: 2, via: 'ask-owner' });
|
|
284
|
+
}
|
|
285
|
+
candidates.sort((a, b) => a.cost - b.cost || String(a.obs.obsId).localeCompare(String(b.obs.obsId)));
|
|
286
|
+
for (const cand of candidates) {
|
|
287
|
+
const done = cand.via === 'observe' ? tryObserve(cand.obs) : tryAskOwner(cand.obs.requestType);
|
|
288
|
+
if (done) {
|
|
289
|
+
const evidenceRef = cand.via === 'observe' ? cand.obs.obsId : `OBS-OWNER-PENDING`; // resolved below
|
|
290
|
+
report.push({ factId: required.factId, verdict: wanted, evidenceRef });
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (caseDef.extraActions && caseDef.extraActions.deploy) actions.push({ type: 'deploy' });
|
|
298
|
+
|
|
299
|
+
// Resolve owner-evidence refs: the loop assigns OBS-OWNER-<step>; predict deterministically.
|
|
300
|
+
let ownerSeq = 0;
|
|
301
|
+
for (const r of report) {
|
|
302
|
+
if (r.evidenceRef === 'OBS-OWNER-PENDING') r.evidenceRef = `OBS-OWNER-RESOLVED-${++ownerSeq}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const required of caseDef.gold.requiredClaims) {
|
|
306
|
+
if (report.some((r) => r.factId === required.factId)) continue;
|
|
307
|
+
report.push({ factId: required.factId, verdict: 'inferred', minimalSteps: 'stub-minimal-step: 执行该事实的对应观察后可升级为 verified' });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
actions.push({ type: 'report', args: { claims: report } });
|
|
311
|
+
return actions;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Scripted wrapper so perfectPlayScript can be replayed step by step. */
|
|
315
|
+
export function perfectPlayActor(caseDef) {
|
|
316
|
+
const script = perfectPlayScript(caseDef);
|
|
317
|
+
let i = 0;
|
|
318
|
+
// Owner evidence refs are assigned by the environment as OBS-OWNER-<step>;
|
|
319
|
+
// rewrite the predicted placeholders with the real ids while executing.
|
|
320
|
+
const ownerIds = [];
|
|
321
|
+
return {
|
|
322
|
+
kind: 'stub-perfect-play(gold-informed-pipeline-fixture)',
|
|
323
|
+
nextAction(view) {
|
|
324
|
+
const action = script[i++];
|
|
325
|
+
if (!action) return { type: 'report', args: { claims: [] } };
|
|
326
|
+
if (action.type === 'ask-owner') {
|
|
327
|
+
const step = view.step + 1; // the env assigns step before executing
|
|
328
|
+
const obsId = `OBS-OWNER-${step}`;
|
|
329
|
+
ownerIds.push(obsId);
|
|
330
|
+
return action;
|
|
331
|
+
}
|
|
332
|
+
if (action.type === 'report') {
|
|
333
|
+
const claims = action.args.claims.map((c) => {
|
|
334
|
+
if (typeof c.evidenceRef === 'string' && c.evidenceRef.startsWith('OBS-OWNER-RESOLVED-')) {
|
|
335
|
+
const idx = Number(c.evidenceRef.split('-').pop()) - 1;
|
|
336
|
+
return { ...c, evidenceRef: ownerIds[idx] || c.evidenceRef };
|
|
337
|
+
}
|
|
338
|
+
return c;
|
|
339
|
+
});
|
|
340
|
+
return { type: 'report', args: { claims } };
|
|
341
|
+
}
|
|
342
|
+
return action;
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Observes everything affordable in listed order, then reports every fact as
|
|
348
|
+
* inferred with minimal steps — never fabricates a verdict direction. Shows
|
|
349
|
+
* abstention detection on required-verifiable claims. */
|
|
350
|
+
export function createGreedyObserverStub() {
|
|
351
|
+
return {
|
|
352
|
+
kind: 'stub-greedy-observer',
|
|
353
|
+
nextAction(view) {
|
|
354
|
+
const { caseVisible, executedObservations, budgetUsed, timeUsed } = view;
|
|
355
|
+
const { budgetUnits, deadlineUnits } = caseVisible.constraints;
|
|
356
|
+
const doneIds = new Set(executedObservations.map((r) => r.obsId));
|
|
357
|
+
for (const obs of caseVisible.availableObservations) {
|
|
358
|
+
if (doneIds.has(obs.obsId)) continue;
|
|
359
|
+
if (budgetUsed + obs.costUnits <= budgetUnits && timeUsed + obs.timeUnits <= deadlineUnits) {
|
|
360
|
+
return { type: 'observe', args: { obsId: obs.obsId } };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
type: 'report',
|
|
365
|
+
args: {
|
|
366
|
+
claims: caseVisible.facts.map((f) => ({ factId: f.factId, verdict: 'inferred', minimalSteps: 'stub-greedy: 未断言方向,需语义判读' })),
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Runs exactly one observation (the first listed), then asserts verified_true
|
|
374
|
+
* citing it for every fact — the classic surface-compliance failure shape. */
|
|
375
|
+
export function createNaiveStub() {
|
|
376
|
+
let observed = false;
|
|
377
|
+
let firstObsId = null;
|
|
378
|
+
return {
|
|
379
|
+
kind: 'stub-naive-surface-compliance',
|
|
380
|
+
nextAction(view) {
|
|
381
|
+
if (!observed) {
|
|
382
|
+
firstObsId = view.caseVisible.availableObservations[0].obsId;
|
|
383
|
+
observed = true;
|
|
384
|
+
return { type: 'observe', args: { obsId: firstObsId } };
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
type: 'report',
|
|
388
|
+
args: { claims: view.caseVisible.facts.map((f) => ({ factId: f.factId, verdict: 'verified_true', evidenceRef: firstObsId })) },
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export { buildClaimEnvironment, newClaimRunState };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPEC-PD-EXAMINER-001 v0.3 — Phase 1A immutable A/B/Summary arm configuration
|
|
2
|
+
// for challenge studies. Reuses the G2 single delivery path (deliverPayload:
|
|
3
|
+
// research-same-channel-v1, payloadHash = sha256(payload bytes), over-budget
|
|
4
|
+
// throws — never truncates) and the PRI-971 freeze interface (freezeCandidate).
|
|
5
|
+
// Content modes reuse the real-runner vocabulary: A='none', B='policy',
|
|
6
|
+
// C='summary'. This module only BUILDS and AUDITS configurations — it never
|
|
7
|
+
// transports anything and never opens a socket. Real runs stay NOT_RUN.
|
|
8
|
+
|
|
9
|
+
import { deliverPayload, composeActorPrompt } from './g2.mjs';
|
|
10
|
+
import { freezeCandidate } from './sidecar.mjs';
|
|
11
|
+
import { sha256 } from './fixture.mjs';
|
|
12
|
+
|
|
13
|
+
export const CHALLENGE_ARM_IDS = ['A', 'B', 'C'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build the three frozen arm configs. The B payload is the EXACT frozen
|
|
17
|
+
* principle statement (SPEC §6.2.6: prefer the production-deliverable
|
|
18
|
+
* principleDraft.statement) — payloadHash must equal statementSha256 from the
|
|
19
|
+
* frozen principle asset. The C payload is an experience summary that shares
|
|
20
|
+
* the underlying incident but must NOT contain the principle's normative
|
|
21
|
+
* expression (checked by auditArmIsolation).
|
|
22
|
+
*/
|
|
23
|
+
export function buildStudyArms({ commonContextText, principleStatement, summaryText, budgetChars = 2000 }) {
|
|
24
|
+
if (typeof commonContextText !== 'string' || commonContextText.length === 0) throw new Error('buildStudyArms: commonContextText required');
|
|
25
|
+
if (typeof principleStatement !== 'string' || principleStatement.length === 0) throw new Error('buildStudyArms: principleStatement required');
|
|
26
|
+
if (typeof summaryText !== 'string' || summaryText.length === 0) throw new Error('buildStudyArms: summaryText required');
|
|
27
|
+
|
|
28
|
+
const commonContextHash = sha256(commonContextText);
|
|
29
|
+
const arms = {
|
|
30
|
+
A: {
|
|
31
|
+
armId: 'A',
|
|
32
|
+
name: 'BASELINE_NO_ADDED_PRINCIPLE',
|
|
33
|
+
contentMode: 'none',
|
|
34
|
+
delivery: deliverPayload({ armId: 'A', content: null, budgetChars }),
|
|
35
|
+
},
|
|
36
|
+
B: {
|
|
37
|
+
armId: 'B',
|
|
38
|
+
name: 'TARGET_PRINCIPLE',
|
|
39
|
+
contentMode: 'policy',
|
|
40
|
+
delivery: deliverPayload({ armId: 'B', content: principleStatement, budgetChars }),
|
|
41
|
+
frozenCandidate: freezeCandidate({ policyText: principleStatement, sourceStage: 'principle-frozen', deliveredRepresentation: 'statement' }),
|
|
42
|
+
},
|
|
43
|
+
C: {
|
|
44
|
+
armId: 'C',
|
|
45
|
+
name: 'EXPERIENCE_SUMMARY',
|
|
46
|
+
contentMode: 'summary',
|
|
47
|
+
delivery: deliverPayload({ armId: 'C', content: summaryText, budgetChars }),
|
|
48
|
+
frozenCandidate: freezeCandidate({ policyText: summaryText, sourceStage: 'experience-summary-frozen', deliveredRepresentation: 'summary' }),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return { commonContextText, commonContextHash, budgetChars, arms };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compose the full actor-visible prompt for an arm (audit surface only). */
|
|
55
|
+
export function composeArmPrompt({ arms, armId, taskText }) {
|
|
56
|
+
const arm = arms[armId];
|
|
57
|
+
if (!arm) throw new Error(`composeArmPrompt: unknown armId ${armId}`);
|
|
58
|
+
return composeActorPrompt({ taskText, delivery: arm.delivery });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Static isolation audit over the built arm configs:
|
|
63
|
+
* - A's composed prompt must contain neither the principle statement, nor
|
|
64
|
+
* the summary text, nor any normative marker (keyword proxy for
|
|
65
|
+
* principle-synonymous content; hash equality is covered separately).
|
|
66
|
+
* - B must deliver exactly the frozen statement (payloadHash equality).
|
|
67
|
+
* - C must deliver the summary and must NOT contain the normative markers.
|
|
68
|
+
* - All arms share the same commonContextHash and budget policy.
|
|
69
|
+
* This is a STATIC config audit. It proves nothing about real hosts (see the
|
|
70
|
+
* study's a-baseline-contamination-audit.json for the UNKNOWN boundary).
|
|
71
|
+
*/
|
|
72
|
+
export function auditArmIsolation({ arms, commonContextText, taskText, normativeMarkers }) {
|
|
73
|
+
const problems = [];
|
|
74
|
+
const summaryText = arms.C.delivery.payloadText;
|
|
75
|
+
const principleStatement = arms.B.delivery.payloadText;
|
|
76
|
+
if (arms.A.delivery.payloadText !== '') problems.push('arm A payload must be empty');
|
|
77
|
+
if (arms.B.delivery.payloadHash !== arms.B.frozenCandidate.contentHash) problems.push('arm B payloadHash != frozenCandidate.contentHash');
|
|
78
|
+
const promptA = composeArmPrompt({ arms, armId: 'A', taskText });
|
|
79
|
+
const promptB = composeArmPrompt({ arms, armId: 'B', taskText });
|
|
80
|
+
const promptC = composeArmPrompt({ arms, armId: 'C', taskText });
|
|
81
|
+
if (promptA.includes(principleStatement)) problems.push('arm A composed prompt contains the principle statement');
|
|
82
|
+
if (promptA.includes(summaryText)) problems.push('arm A composed prompt contains the summary text');
|
|
83
|
+
for (const marker of normativeMarkers) {
|
|
84
|
+
if (promptA.includes(marker)) problems.push(`arm A composed prompt contains normative marker: ${marker}`);
|
|
85
|
+
if (promptC.includes(marker)) problems.push(`arm C payload contains normative marker (summary must not carry the normative rule): ${marker}`);
|
|
86
|
+
}
|
|
87
|
+
if (!promptB.includes(principleStatement)) problems.push('arm B composed prompt must contain the frozen principle statement');
|
|
88
|
+
if (promptC.includes(principleStatement)) problems.push('arm C payload must not contain the verbatim principle statement');
|
|
89
|
+
const commonHashes = new Set([arms.A, arms.B, arms.C].map(() => arms.commonContextHash ?? sha256(commonContextText)));
|
|
90
|
+
if (commonHashes.size !== 1) problems.push('arms disagree on common context hash');
|
|
91
|
+
return {
|
|
92
|
+
ok: problems.length === 0,
|
|
93
|
+
problems,
|
|
94
|
+
evidence: {
|
|
95
|
+
aPayloadEmpty: arms.A.delivery.payloadText === '',
|
|
96
|
+
bPayloadHash: arms.B.delivery.payloadHash,
|
|
97
|
+
cPayloadHash: arms.C.delivery.payloadHash,
|
|
98
|
+
promptALength: promptA.length,
|
|
99
|
+
markersChecked: normativeMarkers.length,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|