create-principles-disciple 1.146.8 → 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.
@@ -277,34 +277,6 @@ export function writableStudyDirProblem(roots, studyDir) {
277
277
  return STUDY_PATH_PROBLEM.notInside;
278
278
  return null;
279
279
  }
280
- /** Study status projection (manifest + ledger settlement counts + report status). */
281
- export function studyStatusPayload(studyDir) {
282
- const manifest = readStudyJson(studyDir, 'examiner-manifest.json');
283
- const eventsFile = path.join(studyDir, 'events.jsonl');
284
- const events = fs.existsSync(eventsFile)
285
- ? fs.readFileSync(eventsFile, 'utf8').trim().split('\n').filter(Boolean).map((line) => asJsonObject(JSON.parse(line), eventsFile))
286
- : [];
287
- const counts = {};
288
- for (const event of events) {
289
- if (typeof event.type !== 'string' || !event.type.startsWith('UNIT_FINISHED'))
290
- continue;
291
- const result = String(event.result ?? 'UNKNOWN');
292
- counts[result] = (counts[result] ?? 0) + 1;
293
- }
294
- const reportExists = fs.existsSync(path.join(studyDir, 'behavior-report-v2.json'));
295
- const report = reportExists ? readStudyJson(studyDir, 'behavior-report-v2.json') : null;
296
- return {
297
- studyId: path.basename(studyDir),
298
- experimentMode: manifest.experimentMode,
299
- interventionType: manifest.interventionType,
300
- targetPrincipleId: stringField(manifest.targetPrinciple, 'id'),
301
- plannedPairs: manifest.plannedPairs,
302
- runStarted: events.some((event) => event.type === 'RUN_STARTED'),
303
- runFinished: events.some((event) => event.type === 'RUN_FINISHED'),
304
- unitStates: counts,
305
- reportStatus: report ? (report.status) : null,
306
- };
307
- }
308
280
  /** Read-only Report V2 payload + replay verification. */
309
281
  export function studyReportPayload(modules, studyDir) {
310
282
  if (!fs.existsSync(path.join(studyDir, 'behavior-report-v2.json')))
@@ -409,7 +381,7 @@ export function preflightCheck(modules, studyDir, context) {
409
381
  * before RUN_STARTED — but a blocker here means the runner is not called at
410
382
  * all, so no attempt is reserved and no actor is created.
411
383
  */
412
- export async function offlineStudyPass(modules, studyDir, context) {
384
+ export async function offlineStudyPass(modules, studyDir, context, admissionCheck) {
413
385
  // The write boundary is re-evaluated here (not only at route start): a
414
386
  // directory swapped for a symlink/junction after access resolution must not
415
387
  // reach the runner.
@@ -435,6 +407,9 @@ export async function offlineStudyPass(modules, studyDir, context) {
435
407
  },
436
408
  actorFactory: ({ armId }) => modules.actor.createScriptedStub({ actions: didacticActions(armId) }),
437
409
  execution: { mode: 'offline-stub' },
410
+ // P1-3: the runner invokes this UNDER the study write lock, before
411
+ // RUN_STARTED / reservation / actor creation. Supplied by the route layer.
412
+ ...(admissionCheck ? { admissionCheck } : {}),
438
413
  });
439
414
  }
440
415
  export function reportMaterialize(modules, studyDir, now, context, canWrite) {
@@ -554,6 +529,8 @@ export async function materializeConsoleStudy(modules, labRoot, roots, selection
554
529
  target,
555
530
  ledgerPrinciples: selection.ledgerPrinciples,
556
531
  repetitionsPerCase: selection.repetitionsPerCase,
532
+ suiteKey: selection.suiteKey,
533
+ hostBacked: selection.hostBacked,
557
534
  });
558
535
  // The lab created the directory tree; verify the REAL location before the
559
536
  // ownership marker (the trust anchor) is written into it.
@@ -575,40 +552,67 @@ export async function materializeConsoleStudy(modules, labRoot, roots, selection
575
552
  * receives already-validated server data only. Synthetic calibration cases
576
553
  * with SYNTHETIC_OFFLINE gold provenance — offline contract validation,
577
554
  * never real-host behavior.
555
+ *
556
+ * v0.10.1: when `hostBacked` inputs are supplied, the study freezes the
557
+ * caller-selected suite cases, the RUNTIME-derived RuleCode facts and the
558
+ * real pain lineage, and `production-snapshot.json` records where the A/B
559
+ * starting point differs from production (SPEC §4.4/§6.4). The frozen
560
+ * binding files the lab verifies are unchanged — the snapshot is an
561
+ * additional frozen fact, hashed by nothing that the runner recomputes, so
562
+ * appending it after writeExaminerStudyArtifacts keeps every existing
563
+ * verification intact (events.jsonl rides the same discipline).
578
564
  */
579
565
  export async function materializeOfflineStudy(modules, labRoot, inputs) {
580
566
  const fixtureUrl = pathToFileURL(path.join(labRoot, 'lib', 'examiner-v07-cases.mjs')).href;
581
567
  const fixture = (await import(fixtureUrl));
582
568
  const freezeUrl = pathToFileURL(path.join(labRoot, 'lib', 'challenge-freeze.mjs')).href;
583
569
  const freeze = (await import(freezeUrl));
584
- const caseDefs = [
585
- fixture.makeExaminerV3Case({ caseId: `${inputs.studyId}-CASE-01` }),
586
- fixture.makeExaminerV3Case({ caseId: `${inputs.studyId}-CASE-02`, boundary: true }),
587
- ];
570
+ // v0.10.1 §9: a catalog suite key compiles the selected variants under the
571
+ // MINTED study handle (case ids are study-scoped); absent key = v0.9 default.
572
+ let caseDefs;
573
+ if (typeof inputs.suiteKey === 'string') {
574
+ const { suiteCaseDefs, isSuiteKey } = await import('./examiner-study-inputs.js');
575
+ if (!isSuiteKey(inputs.suiteKey))
576
+ throw new Error(`materializeOfflineStudy: unknown suite key '${inputs.suiteKey}'`);
577
+ caseDefs = await suiteCaseDefs(labRoot, inputs.studyId, inputs.suiteKey);
578
+ }
579
+ else {
580
+ caseDefs = [
581
+ fixture.makeExaminerV3Case({ caseId: `${inputs.studyId}-CASE-01` }),
582
+ fixture.makeExaminerV3Case({ caseId: `${inputs.studyId}-CASE-02`, boundary: true }),
583
+ ];
584
+ }
588
585
  const principleFrozen = { contentJson: { principleDraft: { statement: inputs.target.text } } };
586
+ const painRefs = inputs.hostBacked?.painRefs ?? [];
589
587
  const mapping = {
590
588
  schema: 'PainMechanismMappingV1',
591
589
  mappingId: `PMM-${inputs.studyId}`,
592
590
  principleContentHash: freeze.hashOfCanonical(principleFrozen.contentJson),
593
- observedFailure: 'console-originated offline calibration (synthetic)',
591
+ observedFailure: painRefs.length > 0
592
+ ? `console-originated offline calibration mapped to the target principle's real pain lineage (${painRefs.join(', ')})`
593
+ : 'console-originated offline calibration (synthetic)',
594
594
  failureMechanism: 'assertion_without_observation',
595
595
  ownerGroundedGoal: 'offline contract validation for the console entry',
596
+ painRefs,
596
597
  };
597
598
  mapping.mappingHash = freeze.freezeMapping(mapping).mappingHash;
598
- return modules.study.writeExaminerStudyArtifacts({
599
+ const ruleCodeFacts = inputs.hostBacked?.ruleCodeFacts ?? {
600
+ // P1-5 fallback (pre-v0.10.1 shape): ledger rule count as the only fact.
601
+ ruleCodes: [],
602
+ principleRuleCodeIndependence: (inputs.target.ruleCount ?? 0) === 0 ? true : null,
603
+ targetRuleCodeCount: inputs.target.ruleCount ?? 0,
604
+ };
605
+ const writeResult = modules.study.writeExaminerStudyArtifacts({
599
606
  studyId: inputs.studyId,
600
607
  experimentMode: inputs.experimentMode,
601
608
  hepsPrinciples: { principles: inputs.ledgerPrinciples, targetPrincipleId: inputs.target.id, host: 'offline-stub' },
602
- // P1-5: RuleCode facts are EVIDENCE, never a default. The ledger projects
603
- // only the count of RuleCodes bound to the principle; absent individual
604
- // RuleCode ids that count is passed as unresolved evidence, which makes the
605
- // policy INTERVENTION_NOT_IDENTIFIABLE and the creation refuse (422).
606
- // A zero count is a real, evidenced fact: no RuleCode exists to couple.
607
- ruleCodeFacts: {
608
- ruleCodes: [],
609
- principleRuleCodeIndependence: (inputs.target.ruleCount ?? 0) === 0 ? true : null,
610
- targetRuleCodeCount: inputs.target.ruleCount ?? 0,
611
- },
609
+ // P1-5: RuleCode facts are EVIDENCE, never a default. With hostBacked
610
+ // inputs the facts come from RUNTIME activation rows cross-checked against
611
+ // the ledger count (examiner-study-inputs.deriveStudyRuleCodeFacts); an
612
+ // unresolvable attribution still lands as INTERVENTION_NOT_IDENTIFIABLE,
613
+ // which makes the creation refuse (422). A zero count on BOTH sources is
614
+ // a real, evidenced fact: no RuleCode exists to couple.
615
+ ruleCodeFacts,
612
616
  caseDefs,
613
617
  principleFrozen,
614
618
  mapping,
@@ -624,5 +628,10 @@ export async function materializeOfflineStudy(modules, labRoot, inputs) {
624
628
  invalidationPolicy: { hard: ['preregistration tampered', 'gold leak', 'shared memory across arms'], infrastructure: 'single failures stay UNKNOWN' },
625
629
  goldProvenance: 'SYNTHETIC_OFFLINE',
626
630
  studyDir: inputs.studyDir,
631
+ // P1-1: the production baseline freezes WITH the study — hash-bound into
632
+ // the manifest, the binding and the anchor; post-freeze modification is
633
+ // drift that preflight/runner refuse.
634
+ ...(inputs.hostBacked?.productionSnapshot ? { productionSnapshot: inputs.hostBacked.productionSnapshot } : {}),
627
635
  });
636
+ return writeResult;
628
637
  }
@@ -0,0 +1,196 @@
1
+ import { type LabModules, type StudyRoots } from './examiner-executor.js';
2
+ export declare const CONSOLE_RUN_SCHEMA = "examiner-console-run.v1";
3
+ export declare const CONSOLE_RUN_EVENT_TYPES: readonly ['CONSOLE_RUN_ACCEPTED', 'CONSOLE_RUN_FINISHED', 'CONSOLE_RUN_FAILED', 'CONSOLE_RUN_ABANDONED'];
4
+ export type ConsoleRunMode = 'host-dry-run' | 'f2-real-host';
5
+ export type ConsoleRunEventType = (typeof CONSOLE_RUN_EVENT_TYPES)[number];
6
+ export declare const CONSOLE_RUN_MODES: readonly ConsoleRunMode[];
7
+ export interface ConsoleRunEvent {
8
+ type: ConsoleRunEventType;
9
+ schema: typeof CONSOLE_RUN_SCHEMA;
10
+ runId: string;
11
+ studyId: string;
12
+ mode: ConsoleRunMode;
13
+ acceptedAt: string;
14
+ pid: number;
15
+ owner: 'pd-console';
16
+ finishedAt?: string;
17
+ outcome?: string;
18
+ reason?: string;
19
+ checks?: unknown[];
20
+ }
21
+ export interface ConsoleRunState {
22
+ runId: string;
23
+ mode: ConsoleRunMode;
24
+ status: 'RUNNING' | 'FINISHED' | 'FAILED' | 'ABANDONED';
25
+ acceptedAt: string;
26
+ finishedAt: string | null;
27
+ outcome: string | null;
28
+ reason: string | null;
29
+ /** The dry-run checklist carried by the terminal event (never re-invented). */
30
+ checks?: unknown[];
31
+ }
32
+ /**
33
+ * P1-4: the study's event ledger is UNREADABLE for a reason the Owner must
34
+ * see — a corrupt or permission-blocked ledger is never silently treated as
35
+ * "no runs". Only a genuinely absent file (first-run condition) yields [].
36
+ */
37
+ export declare class ConsoleLedgerUnreadableError extends Error {
38
+ readonly code: 'ledger_unreadable' | 'ledger_corrupt';
39
+ constructor(code: 'ledger_unreadable' | 'ledger_corrupt', message: string);
40
+ }
41
+ export interface OwnedProbeDir {
42
+ path: string;
43
+ dev: bigint;
44
+ ino: bigint;
45
+ }
46
+ export interface StudyDirPin {
47
+ real: string;
48
+ dev: bigint;
49
+ ino: bigint;
50
+ }
51
+ /**
52
+ * Remove the dry-run probe directory ONLY when it is still the exact plain
53
+ * directory this run created (identity dev+ino), still resolves inside the
54
+ * same study directory, and the study directory itself has not been swapped.
55
+ * Any unprovable case keeps the residue and reports why — an unknown path is
56
+ * never deleted.
57
+ */
58
+ export declare function removeProbeDirIfStillOwned(probe: OwnedProbeDir, study: StudyDirPin): {
59
+ removed: boolean;
60
+ reason?: string;
61
+ };
62
+ /** Append one console-run lifecycle event to the study's events ledger. */
63
+ export declare function appendConsoleRunEvent(roots: StudyRoots, studyDir: string, event: ConsoleRunEvent): void;
64
+ export declare class OfflineRunInFlightError extends Error {
65
+ constructor();
66
+ }
67
+ export declare function mintRunId(): string;
68
+ /** Derive the console-run states from the study ledger (never stored separately). */
69
+ export declare function readConsoleRuns(roots: StudyRoots, studyDir: string): ConsoleRunState[];
70
+ export declare function currentConsoleRun(runs: ConsoleRunState[]): ConsoleRunState | null;
71
+ /**
72
+ * Study status projection (manifest + ledger settlement counts + report status).
73
+ * The DISPLAY projection tolerates single corrupt lines — the authoritative
74
+ * console-run reader refuses them and the route reports `ledgerProblem` — but
75
+ * it reads the ledger through the SAME secure fd boundary (never by path).
76
+ */
77
+ export declare function studyStatusPayload(roots: StudyRoots, studyDir: string): Record<string, unknown>;
78
+ /**
79
+ * P1-4: the whole acceptance transaction — read current runs, check the
80
+ * single-live constraint (async AND offline in-flight), verify the study is
81
+ * still writable, append CONSOLE_RUN_ACCEPTED — happens under the EXISTING
82
+ * lab study write lock, so two concurrent requests can never both pass the
83
+ * check. The lock guards ONLY the acceptance; the async execution itself
84
+ * runs unlocked (settlement appends race-free through the same lock the
85
+ * runner already takes for real executions).
86
+ */
87
+ export declare class ConsoleRunInProgressError extends Error {
88
+ readonly runId: string;
89
+ readonly mode: string;
90
+ readonly acceptedAt: string;
91
+ constructor(running: ConsoleRunState);
92
+ }
93
+ export interface ConsoleRunAcceptance {
94
+ runId: string;
95
+ acceptedAt: string;
96
+ }
97
+ export declare function acceptConsoleRun(roots: StudyRoots, studyDir: string, inputs: {
98
+ studyId: string;
99
+ mode: ConsoleRunMode;
100
+ labRoot: string;
101
+ }): Promise<ConsoleRunAcceptance>;
102
+ /**
103
+ * Append an async run's TERMINAL event under the study write lock. Recovery
104
+ * (which also holds the lock) therefore either sees the terminal event and
105
+ * skips, or appends ABANDONED first and the later terminal event wins — an
106
+ * already-settled run can never be reported as ABANDONED.
107
+ */
108
+ export declare function appendConsoleRunEventLocked(input: {
109
+ labRoot: string;
110
+ roots: StudyRoots;
111
+ studyDir: string;
112
+ }, event: ConsoleRunEvent): Promise<void>;
113
+ /**
114
+ * Offline→async half of the single-active contract (P1-3).
115
+ *
116
+ * The lab runner calls this UNDER the study write lock, BEFORE RUN_STARTED,
117
+ * attempt reservation or actor creation. The caller (route layer) supplies it
118
+ * so `examiner-runs` and `examiner-executor` never import each other.
119
+ */
120
+ export declare function assertOfflineAdmissionAllowed(roots: StudyRoots, studyDir: string): void;
121
+ /** Best-effort liveness probe for the process that accepted a run. */
122
+ export declare function processAlive(pid: number): boolean;
123
+ /**
124
+ * Startup recovery: walk the WRITABLE root only (historical assets are never
125
+ * console-run targets) and abandon every unfinished console run whose
126
+ * accepting process is gone. An accepting process that is still alive keeps
127
+ * its run (another console server may legitimately be serving the workspace).
128
+ *
129
+ * Path safety: only names the server itself enumerated that pass the
130
+ * study-handle charset allowlist, each directory must resolve canonically
131
+ * INSIDE the writable root, and every ledger access re-proves containment at
132
+ * the point of use — a symlinked/junctioned entry is skipped, never followed.
133
+ */
134
+ export declare function recoverUnfinishedConsoleRuns(roots: StudyRoots, labRoot: string): Promise<{
135
+ studyId: string;
136
+ runId: string;
137
+ }[]>;
138
+ export interface StageRRead {
139
+ present: boolean;
140
+ record: Record<string, unknown> | null;
141
+ problem: string | null;
142
+ }
143
+ export interface DryRunCheck {
144
+ id: string;
145
+ severity: 'blocker' | 'warning' | 'info';
146
+ ok: boolean;
147
+ detail: string;
148
+ }
149
+ export interface HostDryRunResult {
150
+ ok: boolean;
151
+ checks: DryRunCheck[];
152
+ authorization: {
153
+ present: boolean;
154
+ status: string | null;
155
+ executionMode: string | null;
156
+ model: string | null;
157
+ note: string;
158
+ };
159
+ hostProbe: {
160
+ cliVersion: string | null;
161
+ note: string;
162
+ };
163
+ modelCalls: 0;
164
+ }
165
+ /**
166
+ * The host dry-run: everything Gate 2 can check WITHOUT a model call. It is
167
+ * deliberately a checklist, not a pass/fail black box — the Owner sees exactly
168
+ * which prerequisite is missing before real execution can be requested.
169
+ */
170
+ export declare function hostDryRunPass(input: {
171
+ modules: LabModules;
172
+ labRoot: string;
173
+ studyDir: string;
174
+ context: {
175
+ labRoot: string;
176
+ roots: StudyRoots;
177
+ } | undefined;
178
+ }): Promise<HostDryRunResult>;
179
+ export interface F2GateResult {
180
+ authorized: boolean;
181
+ reasons: string[];
182
+ record: Record<string, unknown> | null;
183
+ }
184
+ /**
185
+ * P1-5: the console gate REUSES the lab's authoritative Stage R verifier
186
+ * (verifyStageRAuthorization) instead of a hand-rolled subset — schema,
187
+ * stage/status, the full study binding (studyId/suiteHash/mappingHash/
188
+ * graderHash/studyPlanHash), arms/cases/repetitions coverage, attemptCap,
189
+ * requestCap, the validity window, the retirement registry, revocation/
190
+ * halted, requestedActorIdentity and the F2 real-host probe evidence are ALL
191
+ * enforced. A record that is merely status=APPROVED fails every missing
192
+ * binding it does not carry. The runner re-verifies the same authority per
193
+ * unit under the study lock — this gate is a necessary condition, the runner
194
+ * is the authority.
195
+ */
196
+ export declare function f2AuthorizationGate(labRoot: string, studyDir: string): Promise<F2GateResult>;