create-principles-disciple 1.145.43 → 1.146.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/_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/package.json +1 -1
- package/release-manager/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// SPEC-PD-EXAMINER-001 v0.3 — Phase 1B append-only research ledger, budget
|
|
2
|
+
// reservation and single-writer lock for challenge studies (offline stub).
|
|
3
|
+
//
|
|
4
|
+
// Discipline mirrors lib/http-budget.mjs (the lab's verified budget prim
|
|
5
|
+
// itive): reserve() checks the cap BEFORE committing and NEVER sends anything
|
|
6
|
+
// when refused; reservations are never returned — a crashed holder keeps its
|
|
7
|
+
// reservation, and recovery only reads. The unit of reservation here is an
|
|
8
|
+
// execution ATTEMPT (offline stub run), not an HTTP request; no parallel
|
|
9
|
+
// budget system is introduced — http-budget itself stays untouched and is
|
|
10
|
+
// the authority for any future real-model transport.
|
|
11
|
+
//
|
|
12
|
+
// Files (SPEC §10, per study dir):
|
|
13
|
+
// budget-ledger.jsonl — RESERVE / ATTEMPT_PREPARED / ATTEMPT_RESULT /
|
|
14
|
+
// BUDGET_REFUSED rows (append-only)
|
|
15
|
+
// events.jsonl — runner state transitions (append-only)
|
|
16
|
+
// adjudications.jsonl — one line per adjudicated unit (append-only)
|
|
17
|
+
// runs/writer.lock — study-level single-writer lock (O_EXCL)
|
|
18
|
+
//
|
|
19
|
+
// seq numbers are per-file line numbers, derived by re-reading the file —
|
|
20
|
+
// crash-safe monotonic ordering without any clock. A torn final line (crash
|
|
21
|
+
// mid-append) is reported as malformedTail and dropped from entries; it is
|
|
22
|
+
// never silently interpreted.
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
export const BUDGET_FILES = {
|
|
28
|
+
budget: 'budget-ledger.jsonl',
|
|
29
|
+
events: 'events.jsonl',
|
|
30
|
+
adjudications: 'adjudications.jsonl',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class StudyLockError extends Error {
|
|
34
|
+
constructor(lockPath, owner) {
|
|
35
|
+
super(`study writer lock held: ${lockPath} (owner: ${owner}) — refusing concurrent writer`);
|
|
36
|
+
this.name = 'StudyLockError';
|
|
37
|
+
this.lockPath = lockPath;
|
|
38
|
+
this.owner = owner;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readLines(file) {
|
|
43
|
+
// The missing-file branch must carry the same `entries` field the readers
|
|
44
|
+
// destructure — a fresh study dir has no ledgers yet and the first
|
|
45
|
+
// appendEvent(nextSeq) would otherwise crash on undefined.
|
|
46
|
+
if (!fs.existsSync(file)) return { lines: [], entries: [], malformedTail: false };
|
|
47
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
48
|
+
const lines = raw === '' ? [] : raw.split('\n');
|
|
49
|
+
let malformedTail = false;
|
|
50
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
51
|
+
else if (lines.length > 0) malformedTail = true; // file ends without newline → torn append
|
|
52
|
+
const parsed = [];
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.trim() === '') continue;
|
|
55
|
+
try {
|
|
56
|
+
parsed.push(JSON.parse(line));
|
|
57
|
+
} catch {
|
|
58
|
+
malformedTail = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { entries: parsed, malformedTail };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Study-level single writer lock. Refuses when held; crash recovery is an
|
|
65
|
+
* explicit releaseStudyWriteLock by the SAME owner (never automatic). */
|
|
66
|
+
export function acquireStudyWriteLock(studyDir, { owner }) {
|
|
67
|
+
if (typeof owner !== 'string' || owner.length === 0) throw new Error('acquireStudyWriteLock: owner required');
|
|
68
|
+
fs.mkdirSync(path.join(studyDir, 'runs'), { recursive: true });
|
|
69
|
+
const lockPath = path.join(studyDir, 'runs', 'writer.lock');
|
|
70
|
+
let fd;
|
|
71
|
+
try {
|
|
72
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.code === 'EEXIST') {
|
|
75
|
+
let holder = 'unknown';
|
|
76
|
+
try {
|
|
77
|
+
holder = JSON.parse(fs.readFileSync(lockPath, 'utf8')).owner;
|
|
78
|
+
} catch { /* unreadable lock still blocks */ }
|
|
79
|
+
throw new StudyLockError(lockPath, holder);
|
|
80
|
+
}
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
fs.writeSync(fd, `${JSON.stringify({ owner, acquiredSeq: Date.now() })}\n`);
|
|
85
|
+
} finally {
|
|
86
|
+
fs.closeSync(fd);
|
|
87
|
+
}
|
|
88
|
+
return { lockPath, owner };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function releaseStudyWriteLock(studyDir, { owner }) {
|
|
92
|
+
const lockPath = path.join(studyDir, 'runs', 'writer.lock');
|
|
93
|
+
if (!fs.existsSync(lockPath)) return { released: false, reason: 'not_held' };
|
|
94
|
+
const holder = (() => { try { return JSON.parse(fs.readFileSync(lockPath, 'utf8')).owner; } catch { return 'unreadable'; } })();
|
|
95
|
+
if (holder !== owner) {
|
|
96
|
+
throw new StudyLockError(lockPath, holder);
|
|
97
|
+
}
|
|
98
|
+
fs.unlinkSync(lockPath);
|
|
99
|
+
return { released: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createChallengeLedger(studyDir) {
|
|
103
|
+
const paths = {
|
|
104
|
+
budget: path.join(studyDir, BUDGET_FILES.budget),
|
|
105
|
+
events: path.join(studyDir, BUDGET_FILES.events),
|
|
106
|
+
adjudications: path.join(studyDir, BUDGET_FILES.adjudications),
|
|
107
|
+
};
|
|
108
|
+
const append = (file, entry) => {
|
|
109
|
+
fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, 'utf8');
|
|
110
|
+
};
|
|
111
|
+
const readBudget = () => readLines(paths.budget);
|
|
112
|
+
const nextSeq = (which) => {
|
|
113
|
+
const { entries } = which === 'budget' ? readBudget() : readLines(paths.events);
|
|
114
|
+
return entries.length + 1;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
paths,
|
|
119
|
+
|
|
120
|
+
budgetEntries: () => readBudget().entries,
|
|
121
|
+
budgetMalformedTail: () => readBudget().malformedTail,
|
|
122
|
+
eventEntries: () => readLines(paths.events).entries,
|
|
123
|
+
adjudicationEntries: () => readLines(paths.adjudications).entries,
|
|
124
|
+
|
|
125
|
+
appendEvent(entry) {
|
|
126
|
+
append(paths.events, { seq: nextSeq('events'), ...entry });
|
|
127
|
+
},
|
|
128
|
+
appendAdjudication(entry) {
|
|
129
|
+
append(paths.adjudications, entry);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** Reserve one attempt against the study-wide attempt budget. Existing
|
|
133
|
+
* reservation for the same unit is IDEMPOTENT (crash after reserve but
|
|
134
|
+
* before prepare does not double-charge). Refusal appends a
|
|
135
|
+
* BUDGET_REFUSED audit row and commits nothing. */
|
|
136
|
+
reserveAttempt({ unit, maxAttemptsTotal, reason }) {
|
|
137
|
+
const { entries } = readBudget();
|
|
138
|
+
const existing = entries.filter((e) => e.type === 'RESERVE' && sameUnit(e.unit, unit));
|
|
139
|
+
const used = entries.filter((e) => e.type === 'RESERVE').length;
|
|
140
|
+
if (existing.length > 0) return { ok: true, alreadyReserved: true, seq: existing[0].seq, used };
|
|
141
|
+
if (used + 1 > maxAttemptsTotal) {
|
|
142
|
+
append(paths.budget, { seq: entries.length + 1, type: 'BUDGET_REFUSED', unit, used, cap: maxAttemptsTotal, reason });
|
|
143
|
+
return { ok: false, code: 'budget_exhausted', used, cap: maxAttemptsTotal };
|
|
144
|
+
}
|
|
145
|
+
const seq = entries.length + 1;
|
|
146
|
+
append(paths.budget, { seq, type: 'RESERVE', unit, amount: 1, reason });
|
|
147
|
+
return { ok: true, alreadyReserved: false, seq, used: used + 1 };
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/** Reserve ONE real model request (real-mode actors may answer over many
|
|
151
|
+
* requests per attempt — the attempt cap is NOT a request budget). Same
|
|
152
|
+
* ledger, same file, same discipline as reserveAttempt: idempotent by
|
|
153
|
+
* the caller's deterministic key (a re-reserve of the same step never
|
|
154
|
+
* double-charges), refusal appends a REQUEST_REFUSED audit row and
|
|
155
|
+
* commits nothing, and reservations are never returned (a request that
|
|
156
|
+
* was dispatched but never answered keeps its slot — RESULT_UNKNOWN
|
|
157
|
+
* semantics, no auto-retry). */
|
|
158
|
+
reserveModelRequest({ key, unit, maxRequestsTotal, reason }) {
|
|
159
|
+
if (typeof key !== 'string' || key.length === 0) throw new Error('reserveModelRequest: key required (deterministic per attempt+step)');
|
|
160
|
+
const { entries } = readBudget();
|
|
161
|
+
const existing = entries.filter((e) => e.type === 'REQUEST_RESERVE' && e.key === key);
|
|
162
|
+
const used = entries.filter((e) => e.type === 'REQUEST_RESERVE').length;
|
|
163
|
+
if (existing.length > 0) return { ok: true, alreadyReserved: true, seq: existing[0].seq, used };
|
|
164
|
+
if (used + 1 > maxRequestsTotal) {
|
|
165
|
+
append(paths.budget, { seq: entries.length + 1, type: 'REQUEST_REFUSED', key, unit, used, cap: maxRequestsTotal, reason });
|
|
166
|
+
return { ok: false, code: 'request_budget_exhausted', used, cap: maxRequestsTotal, reason: 'model-request budget exhausted before dispatch — the request was NOT sent' };
|
|
167
|
+
}
|
|
168
|
+
const seq = entries.length + 1;
|
|
169
|
+
append(paths.budget, { seq, type: 'REQUEST_RESERVE', key, unit, amount: 1, reason });
|
|
170
|
+
return { ok: true, alreadyReserved: false, seq, used: used + 1 };
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
markPrepared(unit) {
|
|
174
|
+
const { entries } = readBudget();
|
|
175
|
+
if (!entries.some((e) => e.type === 'ATTEMPT_PREPARED' && sameUnit(e.unit, unit))) {
|
|
176
|
+
append(paths.budget, { seq: entries.length + 1, type: 'ATTEMPT_PREPARED', unit });
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
markResult(unit, result, detail) {
|
|
181
|
+
const { entries } = readBudget();
|
|
182
|
+
append(paths.budget, { seq: entries.length + 1, type: 'ATTEMPT_RESULT', unit, result, detail: detail || null });
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/** Recovery classification for one (case, arm, repetition, attempt):
|
|
186
|
+
* NOT_STARTED / RESERVED_NOT_PREPARED / RESULT_UNKNOWN / terminal. */
|
|
187
|
+
classifyUnit(unit) {
|
|
188
|
+
const { entries } = readBudget();
|
|
189
|
+
const mine = entries.filter((e) => sameUnit(e.unit, unit));
|
|
190
|
+
const reserved = mine.some((e) => e.type === 'RESERVE');
|
|
191
|
+
const prepared = mine.some((e) => e.type === 'ATTEMPT_PREPARED');
|
|
192
|
+
const results = mine.filter((e) => e.type === 'ATTEMPT_RESULT');
|
|
193
|
+
if (results.length > 0) {
|
|
194
|
+
const last = results[results.length - 1];
|
|
195
|
+
return { state: last.result, prepared, entries: mine };
|
|
196
|
+
}
|
|
197
|
+
if (prepared) return { state: 'RESULT_UNKNOWN', prepared, entries: mine };
|
|
198
|
+
if (reserved) return { state: 'RESERVED_NOT_PREPARED', prepared: false, entries: mine };
|
|
199
|
+
return { state: 'NOT_STARTED', prepared: false, entries: mine };
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sameUnit(a, b) {
|
|
205
|
+
return a && b && a.caseId === b.caseId && a.armId === b.armId && String(a.repetitionId) === String(b.repetitionId) && String(a.attemptId) === String(b.attemptId);
|
|
206
|
+
}
|