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.
Files changed (57) hide show
  1. package/_release/product-identity.json +2 -2
  2. package/console/dist/server/routes/examiner-executor.d.ts +8 -2
  3. package/console/dist/server/routes/examiner-executor.js +10 -4
  4. package/console/dist/server/routes/examiner.d.ts +23 -17
  5. package/console/dist/server/routes/examiner.js +30 -27
  6. package/console/dist/ui/App.js +1 -1
  7. package/console/dist/ui/components/layout/app-sidebar.d.ts +3 -4
  8. package/console/dist/ui/components/layout/app-sidebar.js +5 -7
  9. package/console/dist/ui/i18n/en.json +1 -1
  10. package/console/dist/ui/i18n/zh-CN.json +1 -1
  11. package/console/dist/ui/utils/runtime-mode.d.ts +2 -1
  12. package/console/dist/ui/utils/runtime-mode.js +2 -1
  13. package/console/dist/web/assets/app.js +23 -23
  14. package/console/lab/lib/challenge-actor.mjs +394 -0
  15. package/console/lab/lib/challenge-arms.mjs +102 -0
  16. package/console/lab/lib/challenge-behavior-report.mjs +2484 -0
  17. package/console/lab/lib/challenge-compile.mjs +211 -0
  18. package/console/lab/lib/challenge-freeze.mjs +215 -0
  19. package/console/lab/lib/challenge-isolation-probe.mjs +550 -0
  20. package/console/lab/lib/challenge-ledger.mjs +206 -0
  21. package/console/lab/lib/challenge-openclaw-host.mjs +630 -0
  22. package/console/lab/lib/challenge-runner.mjs +868 -0
  23. package/console/lab/lib/challenge-stage-r-gate.mjs +458 -0
  24. package/console/lab/lib/challenge-statistics.mjs +591 -0
  25. package/console/lab/lib/claim-evidence-env.mjs +652 -0
  26. package/console/lab/lib/claim-evidence-scorer.mjs +784 -0
  27. package/console/lab/lib/examiner-injection-evidence.mjs +367 -0
  28. package/console/lab/lib/examiner-study.mjs +1203 -0
  29. package/console/lab/lib/examiner-v07-cases.mjs +187 -0
  30. package/console/lab/lib/examiner-v2-cases.mjs +61 -0
  31. package/console/lab/lib/fixture.mjs +382 -0
  32. package/console/lab/lib/g2.mjs +146 -0
  33. package/console/lab/lib/host-actor.mjs +987 -0
  34. package/console/lab/lib/sidecar.mjs +273 -0
  35. package/core/dist/runtime-v2/pain-recall-capability.d.ts +140 -0
  36. package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -0
  37. package/core/dist/runtime-v2/pain-recall-capability.js +334 -0
  38. package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -0
  39. package/core/dist/runtime-v2/pain-recall-read-model.d.ts +113 -0
  40. package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -0
  41. package/{pd-cli/dist/services/pain-recall.js → core/dist/runtime-v2/pain-recall-read-model.js} +7 -10
  42. package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -0
  43. package/core/dist/runtime-v2/pain-recall-snapshot-hooks.d.ts +39 -0
  44. package/core/dist/runtime-v2/pain-recall-snapshot-hooks.d.ts.map +1 -0
  45. package/core/dist/runtime-v2/pain-recall-snapshot-hooks.js +15 -0
  46. package/core/dist/runtime-v2/pain-recall-snapshot-hooks.js.map +1 -0
  47. package/core/package.json +9 -1
  48. package/package.json +2 -2
  49. package/pd-cli/dist/commands/pain-recall.js +1 -1
  50. package/pd-cli/dist/commands/pain-recall.js.map +1 -1
  51. package/plugin/dist/openclaw.plugin.json +1 -1
  52. package/plugin/openclaw.plugin.json +1 -1
  53. package/plugin/package.json +1 -1
  54. package/release-manager/package.json +2 -2
  55. package/pd-cli/dist/services/pain-recall.d.ts +0 -192
  56. package/pd-cli/dist/services/pain-recall.d.ts.map +0 -1
  57. package/pd-cli/dist/services/pain-recall.js.map +0 -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
+ }