create-principles-disciple 1.146.7 → 1.146.9

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.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Fixed server-side catalog. Each entry maps to a deterministic list of
3
+ * `makeExaminerV3Case` variants from the lab's authored calibration battery
4
+ * (`examiner-v07-cases.mjs`) — the same frozen assets the committed
5
+ * acceptance studies use. Case ids are minted per study by the executor.
6
+ */
7
+ export declare const SUITE_CATALOG: readonly [{
8
+ readonly key: 'didactic-pair';
9
+ readonly title: 'Didactic pair (v0.9 default)';
10
+ readonly description: 'Two synthetic calibration cases: the canonical observe-then-report world plus the boundary (principle-not-applicable) variant. Same suite every v0.9 console study froze.';
11
+ readonly variants: readonly [{}, {
12
+ readonly boundary: true;
13
+ }];
14
+ readonly goldProvenance: 'SYNTHETIC_OFFLINE';
15
+ }, {
16
+ readonly key: 'calibration-core';
17
+ readonly title: 'Calibration core (six worlds)';
18
+ readonly description: 'Six distinct worlds from the authored v0.7 calibration battery: canonical, boundary, deploy-cycle (owner signoff), unknown-evidence, preseeded-evidence and stale-source variants. Richer applicability mix; still SYNTHETIC gold.';
19
+ readonly variants: readonly [{}, {
20
+ readonly boundary: true;
21
+ }, {
22
+ readonly deploy: true;
23
+ }, {
24
+ readonly unknown: true;
25
+ }, {
26
+ readonly preseed: true;
27
+ }, {
28
+ readonly staleRequiredSource: true;
29
+ }];
30
+ readonly goldProvenance: 'SYNTHETIC_OFFLINE';
31
+ }];
32
+ export type SuiteKey = (typeof SUITE_CATALOG)[number]['key'];
33
+ export declare const SUITE_KEYS: readonly string[];
34
+ export interface SuiteCatalogEntry {
35
+ key: string;
36
+ title: string;
37
+ description: string;
38
+ caseCount: number;
39
+ variantLabels: string[];
40
+ goldProvenance: string;
41
+ goldGate: string;
42
+ suiteHash: string | null;
43
+ }
44
+ export declare function listSuiteCatalog(labRoot: string): Promise<SuiteCatalogEntry[]>;
45
+ export declare function isSuiteKey(value: unknown): value is SuiteKey;
46
+ /**
47
+ * Compile the case definitions for one suite under a STUDY-MINTED id prefix.
48
+ * The case ids are server-minted (`<studyId>-CASE-NN`); the request never
49
+ * contributes a case id or a path.
50
+ */
51
+ export declare function suiteCaseDefs(labRoot: string, studyId: string, suiteKey: SuiteKey): Promise<Record<string, unknown>[]>;
52
+ export interface ProductionSnapshot {
53
+ schema: 'examiner-production-snapshot.v1';
54
+ asOf: string;
55
+ targetPrincipleId: string;
56
+ promptRoute: {
57
+ status: 'confirmed' | 'unconfirmed';
58
+ routeName: string | null;
59
+ note: string;
60
+ };
61
+ budget: {
62
+ /** Production budget the projection actually resolved (UTF-16 chars). */
63
+ chars: number | null;
64
+ capacitySource: string | null;
65
+ capacityStatus: string | null;
66
+ policyVersion: string | null;
67
+ unit: 'utf16-chars';
68
+ };
69
+ runtimeActivation: {
70
+ /**
71
+ * The activation read model's own verdict ('ok' | 'degraded'). P1-2: a
72
+ * degraded read (missing state.db / table / unreadable) means the runtime
73
+ * truth is UNKNOWN — an empty row list here is NEVER "zero activations".
74
+ */
75
+ status: 'ok' | 'degraded';
76
+ reason?: string;
77
+ nextAction?: string;
78
+ /** Principles with an ACTIVE prompt activation row (runtime truth). */
79
+ promptActivePrincipleIds: string[];
80
+ /** Principles the production projection would inject right now. */
81
+ projectedInjectedPrincipleIds: string[] | null;
82
+ /** Bounded display list — NEVER the statistics basis (see ruleCodeCount). */
83
+ ruleCodeActivations: {
84
+ activationId: string;
85
+ principleId: string;
86
+ mode: string | null;
87
+ status: string;
88
+ }[];
89
+ /**
90
+ * P1-2: the COMPLETE workspace-wide code_tool_hook row count, computed
91
+ * before any display truncation (the UI list above is capped at 50).
92
+ */
93
+ ruleCodeActivationTotal: number;
94
+ };
95
+ ledgerActivePrincipleIds: string[];
96
+ divergence: {
97
+ /** Ledger-active principles with NO active runtime prompt activation. */
98
+ activeInLedgerNotRuntime: string[];
99
+ /** Ledger-active principles the current projection would NOT inject. */
100
+ activeInLedgerNotProjected: string[] | null;
101
+ };
102
+ targetPrinciple: {
103
+ runtimePromptActive: boolean;
104
+ projectedInjected: boolean | null;
105
+ ruleCodeActivationCount: number;
106
+ ledgerRuleCount: number;
107
+ };
108
+ experimentVsProduction: {
109
+ studyBudgetChars: number;
110
+ productionBudgetChars: number | null;
111
+ markers: string[];
112
+ note: string;
113
+ };
114
+ }
115
+ /** The single frozen marker for a configuration that is not the production injection path. */
116
+ export declare const EXPERIMENT_CONFIG_MARKER = "EXPERIMENT_CONFIGURATION_DIFFERS_FROM_PRODUCTION";
117
+ /**
118
+ * Gather the production facts a study must freeze at creation time. Every
119
+ * fact comes from an existing authority (projection / activation read model /
120
+ * ledger); nothing is inferred and nothing is written.
121
+ */
122
+ export declare function gatherProductionSnapshot(workspaceDir: string, targetPrincipleId: string, studyBudgetChars: number): Promise<ProductionSnapshot>;
123
+ export interface StudyRuleCodeFacts {
124
+ ruleCodes: {
125
+ ruleCodeId: string;
126
+ principleId: string;
127
+ baselineState: 'ON' | 'OFF';
128
+ }[];
129
+ principleRuleCodeIndependence: boolean | null;
130
+ targetRuleCodeCount: number;
131
+ evidenceNote: string;
132
+ }
133
+ /**
134
+ * Derive the study's RuleCode facts from RUNTIME activation rows
135
+ * (authoritative) cross-checked against the ledger rule count.
136
+ *
137
+ * Honesty rules (SPEC §7.2):
138
+ * - a live-enforcing RuleCode bound to the target is a REAL coupling → the
139
+ * count resolves but independence is unproven → INTERVENTION_NOT_IDENTIFIABLE
140
+ * (the lab refuses creation; nothing is silently labelled principle_only);
141
+ * - shadow (non-enforcing) rows resolve to baseline OFF — still a coupling
142
+ * attribution, so independence stays unproven while any row exists;
143
+ * - ledger-attributed rules with no runtime row stay UNRESOLVED (v0.9
144
+ * semantics: unresolved baseline refuses, never assumed OFF).
145
+ */
146
+ export declare function deriveStudyRuleCodeFacts(snapshot: ProductionSnapshot): StudyRuleCodeFacts;
147
+ export declare function readProductionSnapshot(studyDir: string): Record<string, unknown> | null;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * SPEC-PD-EXAMINER-001 v0.10.1 §6/§9/§4 — host-backed study inputs.
3
+ *
4
+ * Assembles the REAL inputs a v0.10.1 study freezes:
5
+ * - the Challenge Suite catalog (server-fixed keys over the lab's existing
6
+ * authored case variants — no new authoring system, no request strings
7
+ * crossing into the research pipeline: a request only SELECTS a key);
8
+ * - the production snapshot (runtime activation truth + the production
9
+ * prompt projection + budget provenance), frozen into the study as
10
+ * `production-snapshot.json` so the report can state exactly where the
11
+ * A/B starting point differs from production;
12
+ * - RuleCode facts derived from RUNTIME activation rows (stronger evidence
13
+ * than the ledger rule count alone; both are cross-checked and any
14
+ * unresolved attribution stays a refusal — never an assumed OFF).
15
+ *
16
+ * Read-only against production: gathering a snapshot writes nothing outside
17
+ * the study directory the executor already owns.
18
+ */
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { pathToFileURL } from 'node:url';
22
+ import { resolveLivePromptInjectionProjection } from '@principles/host-runtime';
23
+ import { ActivationsConsoleModel } from '../models/ActivationsConsoleModel.js';
24
+ import { PrinciplesConsoleModel } from '../models/PrinciplesConsoleModel.js';
25
+ // ── Challenge Suite catalog (SPEC §9: reuse frozen research assets) ─────────
26
+ /**
27
+ * Fixed server-side catalog. Each entry maps to a deterministic list of
28
+ * `makeExaminerV3Case` variants from the lab's authored calibration battery
29
+ * (`examiner-v07-cases.mjs`) — the same frozen assets the committed
30
+ * acceptance studies use. Case ids are minted per study by the executor.
31
+ */
32
+ export const SUITE_CATALOG = [
33
+ {
34
+ key: 'didactic-pair',
35
+ title: 'Didactic pair (v0.9 default)',
36
+ description: 'Two synthetic calibration cases: the canonical observe-then-report world plus the boundary (principle-not-applicable) variant. Same suite every v0.9 console study froze.',
37
+ variants: [{}, { boundary: true }],
38
+ goldProvenance: 'SYNTHETIC_OFFLINE',
39
+ },
40
+ {
41
+ key: 'calibration-core',
42
+ title: 'Calibration core (six worlds)',
43
+ description: 'Six distinct worlds from the authored v0.7 calibration battery: canonical, boundary, deploy-cycle (owner signoff), unknown-evidence, preseeded-evidence and stale-source variants. Richer applicability mix; still SYNTHETIC gold.',
44
+ variants: [{}, { boundary: true }, { deploy: true }, { unknown: true }, { preseed: true }, { staleRequiredSource: true }],
45
+ goldProvenance: 'SYNTHETIC_OFFLINE',
46
+ },
47
+ ];
48
+ export const SUITE_KEYS = SUITE_CATALOG.map((suite) => suite.key);
49
+ /** Stable definition hash over the catalog entry (lab canonical hashing). */
50
+ async function suiteDefinitionHash(labRoot, entry) {
51
+ try {
52
+ const freezeUrl = pathToFileURL(path.join(labRoot, 'lib', 'challenge-freeze.mjs')).href;
53
+ const freeze = (await import(freezeUrl));
54
+ return freeze.hashOfCanonical({ key: entry.key, variants: entry.variants, goldProvenance: entry.goldProvenance });
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ function variantLabel(variant) {
61
+ const flags = Object.keys(variant).filter((key) => variant[key] === true);
62
+ return flags.length > 0 ? flags.join('+') : 'canonical';
63
+ }
64
+ export async function listSuiteCatalog(labRoot) {
65
+ const entries = [];
66
+ for (const suite of SUITE_CATALOG) {
67
+ entries.push({
68
+ key: suite.key,
69
+ title: suite.title,
70
+ description: suite.description,
71
+ caseCount: suite.variants.length,
72
+ variantLabels: suite.variants.map((variant) => variantLabel(variant)),
73
+ goldProvenance: suite.goldProvenance,
74
+ goldGate: 'SYNTHETIC gold validates the offline contract only — formal real-behavior conclusions stay blocked behind Gate 1 (independent human Gold review)',
75
+ suiteHash: await suiteDefinitionHash(labRoot, suite),
76
+ });
77
+ }
78
+ return entries;
79
+ }
80
+ export function isSuiteKey(value) {
81
+ return typeof value === 'string' && SUITE_KEYS.includes(value);
82
+ }
83
+ /**
84
+ * Compile the case definitions for one suite under a STUDY-MINTED id prefix.
85
+ * The case ids are server-minted (`<studyId>-CASE-NN`); the request never
86
+ * contributes a case id or a path.
87
+ */
88
+ export async function suiteCaseDefs(labRoot, studyId, suiteKey) {
89
+ const suite = SUITE_CATALOG.find((entry) => entry.key === suiteKey);
90
+ if (!suite)
91
+ throw new Error(`suiteCaseDefs: unknown suite key '${String(suiteKey)}'`);
92
+ const casesUrl = pathToFileURL(path.join(labRoot, 'lib', 'examiner-v07-cases.mjs')).href;
93
+ const mod = (await import(casesUrl));
94
+ return suite.variants.map((variant, index) => mod.makeExaminerV3Case({
95
+ ...variant,
96
+ caseId: `${studyId}-CASE-${String(index + 1).padStart(2, '0')}`,
97
+ }));
98
+ }
99
+ /** The single frozen marker for a configuration that is not the production injection path. */
100
+ export const EXPERIMENT_CONFIG_MARKER = 'EXPERIMENT_CONFIGURATION_DIFFERS_FROM_PRODUCTION';
101
+ /**
102
+ * Gather the production facts a study must freeze at creation time. Every
103
+ * fact comes from an existing authority (projection / activation read model /
104
+ * ledger); nothing is inferred and nothing is written.
105
+ */
106
+ export async function gatherProductionSnapshot(workspaceDir, targetPrincipleId, studyBudgetChars) {
107
+ const asOf = new Date().toISOString();
108
+ // Production prompt projection — the SAME computation the runtime performs
109
+ // (route, budget, injected ids, selection policy).
110
+ const resolution = await resolveLivePromptInjectionProjection({ workspaceDir });
111
+ const confirmed = resolution.status === 'confirmed';
112
+ const projection = confirmed ? resolution.projection : null;
113
+ const perHost = confirmed ? [] : resolution.perHost;
114
+ const routeName = projection?.route ?? null;
115
+ const budgetChars = typeof projection?.budget === 'number' ? projection.budget : null;
116
+ const projectedInjectedIds = projection?.injectedPrincipleIds ?? null;
117
+ const budgetProvenance = projection ?? null;
118
+ // Runtime activation truth + ledger truth.
119
+ const activationsModel = new ActivationsConsoleModel(workspaceDir);
120
+ const activations = await activationsModel.getActivations();
121
+ const promptActivePrincipleIds = [...new Set(activations.activations
122
+ .filter((row) => row.channel === 'prompt' && row.status === 'active')
123
+ .map((row) => row.principleId))].sort();
124
+ const ruleCodeActivations = activations.activations
125
+ .filter((row) => row.channel === 'code_tool_hook')
126
+ .map((row) => ({ activationId: row.activationId, principleId: row.principleId, mode: row.mode ?? null, status: row.status }));
127
+ const principlesModel = new PrinciplesConsoleModel(workspaceDir);
128
+ const list = await principlesModel.listPrinciples();
129
+ const ledgerActivePrincipleIds = (list.principles ?? []).filter((p) => p.status === 'active').map((p) => p.id).sort();
130
+ const targetRules = activations.activations.filter((row) => row.principleId === targetPrincipleId && row.channel === 'code_tool_hook');
131
+ const ledgerTarget = (list.principles ?? []).find((p) => p.id === targetPrincipleId);
132
+ const activeInLedgerNotRuntime = ledgerActivePrincipleIds.filter((id) => !promptActivePrincipleIds.includes(id));
133
+ const activeInLedgerNotProjected = projectedInjectedIds === null
134
+ ? null
135
+ : ledgerActivePrincipleIds.filter((id) => !projectedInjectedIds.includes(id));
136
+ const markers = [];
137
+ if (budgetChars === null || budgetChars !== studyBudgetChars)
138
+ markers.push(EXPERIMENT_CONFIG_MARKER);
139
+ if (!confirmed)
140
+ markers.push('PROMPT_ROUTE_UNCONFIRMED_AT_FREEZE');
141
+ if (activeInLedgerNotProjected !== null && activeInLedgerNotProjected.length > 0) {
142
+ markers.push('PROJECTION_EVICTS_ACTIVE_LIBRARY_PRINCIPLES');
143
+ }
144
+ // P1-2: a degraded activation read is an Owner-visible freeze condition —
145
+ // the runtime truth is UNKNOWN, never "zero activations".
146
+ if (activations.status !== 'ok')
147
+ markers.push('ACTIVATION_DATA_DEGRADED');
148
+ return {
149
+ schema: 'examiner-production-snapshot.v1',
150
+ asOf,
151
+ targetPrincipleId,
152
+ promptRoute: {
153
+ status: confirmed ? 'confirmed' : 'unconfirmed',
154
+ routeName,
155
+ note: confirmed
156
+ ? 'the production injection route was resolved from workspace host facts at freeze time'
157
+ : `the production route could not be confirmed (${perHost.length} candidate host(s)) — the study freezes an unconfirmed-route baseline`,
158
+ },
159
+ budget: {
160
+ chars: budgetChars,
161
+ capacitySource: budgetProvenance?.budgetCapacitySource ?? null,
162
+ capacityStatus: budgetProvenance?.budgetCapacityStatus ?? null,
163
+ policyVersion: budgetProvenance?.budgetPolicyVersion ?? null,
164
+ unit: 'utf16-chars',
165
+ },
166
+ runtimeActivation: {
167
+ status: activations.status,
168
+ reason: activations.reason,
169
+ nextAction: activations.nextAction,
170
+ promptActivePrincipleIds,
171
+ projectedInjectedPrincipleIds: projectedInjectedIds,
172
+ ruleCodeActivations: ruleCodeActivations.slice(0, 50),
173
+ ruleCodeActivationTotal: ruleCodeActivations.length,
174
+ },
175
+ ledgerActivePrincipleIds,
176
+ divergence: {
177
+ activeInLedgerNotRuntime,
178
+ activeInLedgerNotProjected,
179
+ },
180
+ targetPrinciple: {
181
+ runtimePromptActive: promptActivePrincipleIds.includes(targetPrincipleId),
182
+ projectedInjected: projectedInjectedIds === null ? null : projectedInjectedIds.includes(targetPrincipleId),
183
+ ruleCodeActivationCount: targetRules.length,
184
+ ledgerRuleCount: ledgerTarget?.ruleCount ?? 0,
185
+ },
186
+ experimentVsProduction: {
187
+ studyBudgetChars,
188
+ productionBudgetChars: budgetChars,
189
+ markers,
190
+ note: 'the experiment renders arms under the frozen study budget with the offline deterministic renderer; the production prompt hook computes its own budget and selection — a marker above states every detected difference',
191
+ },
192
+ };
193
+ }
194
+ /**
195
+ * Derive the study's RuleCode facts from RUNTIME activation rows
196
+ * (authoritative) cross-checked against the ledger rule count.
197
+ *
198
+ * Honesty rules (SPEC §7.2):
199
+ * - a live-enforcing RuleCode bound to the target is a REAL coupling → the
200
+ * count resolves but independence is unproven → INTERVENTION_NOT_IDENTIFIABLE
201
+ * (the lab refuses creation; nothing is silently labelled principle_only);
202
+ * - shadow (non-enforcing) rows resolve to baseline OFF — still a coupling
203
+ * attribution, so independence stays unproven while any row exists;
204
+ * - ledger-attributed rules with no runtime row stay UNRESOLVED (v0.9
205
+ * semantics: unresolved baseline refuses, never assumed OFF).
206
+ */
207
+ export function deriveStudyRuleCodeFacts(snapshot) {
208
+ // P1-2: a degraded activation read means the runtime truth is UNKNOWN — an
209
+ // empty row list is NEVER "zero activations". Independence may only be the
210
+ // evidenced TRUE when the read model reported status=ok.
211
+ const activationDegraded = snapshot.runtimeActivation.status !== 'ok';
212
+ // ONLY the rows bound to the TARGET principle count as its coupling — a
213
+ // RuleCode activation for a DIFFERENT principle is part of the frozen
214
+ // baseline, not the target's intervention variable (SPEC §7.2).
215
+ const targetRows = activationDegraded
216
+ ? []
217
+ : snapshot.runtimeActivation.ruleCodeActivations
218
+ .filter((row) => row.principleId === snapshot.targetPrincipleId);
219
+ // Statistics come from the COMPLETE counts (never the 50-row display list):
220
+ // the snapshot freezes ruleCodeActivationTotal for the whole workspace and
221
+ // ruleCodeActivationCount for the target, both computed pre-truncation.
222
+ const targetRuleCodeCount = Math.max(snapshot.targetPrinciple.ruleCodeActivationCount, snapshot.targetPrinciple.ledgerRuleCount);
223
+ const resolved = targetRows.map((row) => ({
224
+ ruleCodeId: row.activationId,
225
+ principleId: snapshot.targetPrincipleId,
226
+ baselineState: row.mode === 'live' && row.status === 'active' ? 'ON' : 'OFF',
227
+ }));
228
+ const independence = !activationDegraded && targetRuleCodeCount === 0 ? true : null;
229
+ const degradedNote = activationDegraded
230
+ ? `RUNTIME ACTIVATION DATA DEGRADED (${snapshot.runtimeActivation.reason ?? 'unknown reason'}; next: ${snapshot.runtimeActivation.nextAction ?? 'inspect the workspace runtime state'}) — the activation truth is UNKNOWN, so independence is never assumed; ${snapshot.targetPrinciple.ledgerRuleCount} ledger-attributed rule(s) remain the only count evidence`
231
+ : null;
232
+ const evidenceNote = degradedNote
233
+ ?? (targetRuleCodeCount === 0
234
+ ? 'no RuleCode is attributed to the target principle by either the runtime activation table or the ledger — independence is an evidenced zero'
235
+ : `${targetRows.length} runtime RuleCode activation(s) and ${snapshot.targetPrinciple.ledgerRuleCount} ledger-attributed rule(s) bound to the TARGET; independent control of prompt injection while holding these at baseline is UNPROVEN — INTERVENTION_NOT_IDENTIFIABLE unless separately evidenced`);
236
+ return { ruleCodes: resolved, principleRuleCodeIndependence: independence, targetRuleCodeCount, evidenceNote };
237
+ }
238
+ /**
239
+ * Read a study's frozen production snapshot for DISPLAY (read-only; null when
240
+ * absent/unrecognized). The snapshot is frozen study data, not a trusted
241
+ * contract: the return type stays a plain untrusted record (rc-1) — no typed
242
+ * facade over parsed JSON.
243
+ */
244
+ /** rc-1/rc-2: plain-object type guard over parsed JSON (no `as` narrowing). */
245
+ function isPlainObject(value) {
246
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
247
+ }
248
+ export function readProductionSnapshot(studyDir) {
249
+ const file = path.join(studyDir, 'production-snapshot.json');
250
+ if (!fs.existsSync(file))
251
+ return null;
252
+ try {
253
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
254
+ if (!isPlainObject(parsed))
255
+ return null;
256
+ if (parsed.schema !== 'examiner-production-snapshot.v1')
257
+ return null;
258
+ return parsed;
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SPEC-PD-EXAMINER-001 v0.9 §8 — Examiner Console API (offline-first).
2
+ * SPEC-PD-EXAMINER-001 v0.9 §8 + v0.10.1 §4/§5/§6/§12 — Examiner Console API.
3
3
  *
4
4
  * Product entry for the Examiner research pipeline on top of the existing
5
5
  * principle-learning-lab (single research engine — no parallel Runner/Ledger/
@@ -7,34 +7,43 @@
7
7
  * (`console/lab`), so this surface is available in an installed runtime; a
8
8
  * developer checkout is served from the repository's scripts/dev/ lab.
9
9
  *
10
- * Offline only: every study carries SYNTHETIC_OFFLINE gold and is executed by
11
- * the deterministic local stub. Real model calls and real host runs stay
12
- * behind the Owner-authorization / Stage R gate and are refused here.
13
- *
14
- * Surface (all offline, zero real model/host calls):
15
- * GET /api/v1/examiner/overview — gates, host capability, study list
16
- * GET /api/v1/examiner/principles — workspace principles as study inputs
17
- * POST /api/v1/examiner/studies — create an offline examiner study
18
- * GET /api/v1/examiner/studies/:studyId — manifest + execution + report status
19
- * POST /api/v1/examiner/studies/:studyId/preflight — fail-closed preflight
20
- * POST /api/v1/examiner/studies/:studyId/run — offline-stub execution (real modes refused)
21
- * GET /api/v1/examiner/studies/:studyId/report — Report V2 (read-only)
22
- * POST /api/v1/examiner/studies/:studyId/report — generate Report V2 (idempotent)
10
+ * Surface:
11
+ * v0.9 (offline-first, semantics unchanged):
12
+ * GET /api/v1/examiner/overview
13
+ * GET /api/v1/examiner/principles
14
+ * POST /api/v1/examiner/studies (v0.10.1: + suiteKey)
15
+ * GET /api/v1/examiner/studies/:studyId
16
+ * POST /api/v1/examiner/studies/:studyId/preflight
17
+ * POST /api/v1/examiner/studies/:studyId/run (offline-stub, synchronous)
18
+ * GET /api/v1/examiner/studies/:studyId/report
19
+ * POST /api/v1/examiner/studies/:studyId/report
20
+ * v0.10.1:
21
+ * GET /api/v1/examiner/principles/:id/evidence — production evidence aggregate
22
+ * GET /api/v1/examiner/suites — challenge-suite catalog
23
+ * GET /api/v1/examiner/hosts/openclaw/readiness — host readiness projection
24
+ * POST /api/v1/examiner/studies/:studyId/run — modes 'host-dry-run' /
25
+ * 'f2-real-host' follow the §12.3 async contract: 202 + runId, state
26
+ * derived from the study ledger, one live run per study, restart
27
+ * recovery, RESULT_UNKNOWN never auto-retried, f2 gated on an APPROVED
28
+ * Stage R authorization bound to the frozen study.
23
29
  *
24
30
  * Safety contract: this route layer contains NO filesystem access — every
25
31
  * study path is resolved inside examiner-executor by exact membership in the
26
32
  * server's own directory enumeration, and study identities are always
27
33
  * server-minted crypto-random handles. A request only SELECTS: the target
28
34
  * principle travels onward as an index into the server's own ledger list,
29
- * the experiment mode as a boolean. No request string crosses into the
30
- * research pipeline, and no request value constructs a path.
35
+ * the experiment mode as a boolean, the suite as a fixed-catalog key. No
36
+ * request string crosses into the research pipeline, no request value
37
+ * constructs a path, and no request value selects an executable.
31
38
  *
32
39
  * Two roots (P1-1): NEW console studies are WORKSPACE DATA — written under
33
40
  * `<workspace>/.pd/examiner/studies` — and require the server's own ownership
34
41
  * marker; the committed `examiner-studies/` research assets that travel with
35
42
  * the lab payload are visible but ALWAYS read-only here (POST /run and
36
43
  * POST /report answer 403). The frozen `challenge-studies/` area is never a
37
- * write target. Real-host execution is refused by this route class itself.
44
+ * write target. Real-host execution is refused until an APPROVED Stage R
45
+ * authorization exists inside the study — and the runner re-verifies that
46
+ * authority per unit regardless.
38
47
  */
39
48
  import type { IncomingMessage, ServerResponse } from 'node:http';
40
49
  /**