@skill-harness/core 0.17.0 → 0.18.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.
Files changed (93) hide show
  1. package/README.md +1 -2
  2. package/dist/adapters/types.d.ts +0 -12
  3. package/dist/capture-trace-types.d.ts +0 -96
  4. package/dist/capture-trace-types.js +0 -4
  5. package/dist/command-cost.d.ts +1 -1
  6. package/dist/command-cost.js +0 -5
  7. package/dist/execution-trace.js +1 -1
  8. package/dist/index.d.ts +3 -23
  9. package/dist/index.js +3 -23
  10. package/dist/instruction-coverage.d.ts +0 -7
  11. package/dist/instruction-coverage.js +5 -7
  12. package/dist/journal.d.ts +0 -8
  13. package/dist/metrics.d.ts +18 -0
  14. package/dist/metrics.js +28 -0
  15. package/dist/redaction.d.ts +6 -0
  16. package/dist/redaction.js +55 -0
  17. package/dist/regate.js +6 -71
  18. package/dist/regrade.d.ts +1 -3
  19. package/dist/regrade.js +1 -21
  20. package/dist/report.d.ts +1 -15
  21. package/dist/report.js +1 -13
  22. package/dist/reps.d.ts +1 -2
  23. package/dist/reps.js +0 -2
  24. package/dist/results.d.ts +0 -9
  25. package/dist/results.js +2 -19
  26. package/dist/run.js +11 -72
  27. package/dist/seeded.d.ts +1 -7
  28. package/dist/seeded.js +5 -11
  29. package/dist/sources.js +4 -9
  30. package/dist/spec.d.ts +0 -13
  31. package/dist/spec.js +6 -48
  32. package/dist/trajectory-events.d.ts +58 -0
  33. package/dist/trajectory-events.js +119 -0
  34. package/package.json +1 -1
  35. package/schemas/results-v2.schema.json +376 -75
  36. package/schemas/specification-v1.schema.json +128 -174
  37. package/dist/adjudication.d.ts +0 -219
  38. package/dist/adjudication.js +0 -422
  39. package/dist/adoption.d.ts +0 -128
  40. package/dist/adoption.js +0 -102
  41. package/dist/affected.d.ts +0 -88
  42. package/dist/affected.js +0 -222
  43. package/dist/capture.d.ts +0 -193
  44. package/dist/capture.js +0 -344
  45. package/dist/comparison.d.ts +0 -97
  46. package/dist/comparison.js +0 -189
  47. package/dist/factory-calibration.d.ts +0 -63
  48. package/dist/factory-calibration.js +0 -115
  49. package/dist/intervention-results.d.ts +0 -6
  50. package/dist/intervention-results.js +0 -52
  51. package/dist/intervention.d.ts +0 -123
  52. package/dist/intervention.js +0 -218
  53. package/dist/investigation.d.ts +0 -94
  54. package/dist/investigation.js +0 -148
  55. package/dist/judge-agreement.d.ts +0 -19
  56. package/dist/judge-agreement.js +0 -31
  57. package/dist/qualification-capture.d.ts +0 -44
  58. package/dist/qualification-capture.js +0 -163
  59. package/dist/qualification-config.d.ts +0 -233
  60. package/dist/qualification-config.js +0 -694
  61. package/dist/qualification-lock.d.ts +0 -15
  62. package/dist/qualification-lock.js +0 -87
  63. package/dist/qualification-oauth-directory.d.ts +0 -79
  64. package/dist/qualification-oauth-directory.js +0 -444
  65. package/dist/qualification-panel-store.d.ts +0 -14
  66. package/dist/qualification-panel-store.js +0 -188
  67. package/dist/qualification-panels.d.ts +0 -99
  68. package/dist/qualification-panels.js +0 -91
  69. package/dist/qualification-process.d.ts +0 -13
  70. package/dist/qualification-process.js +0 -190
  71. package/dist/qualification-runner.d.ts +0 -203
  72. package/dist/qualification-runner.js +0 -2037
  73. package/dist/qualification-store.d.ts +0 -187
  74. package/dist/qualification-store.js +0 -833
  75. package/dist/qualification-validation.d.ts +0 -12
  76. package/dist/qualification-validation.js +0 -8
  77. package/dist/screen.d.ts +0 -31
  78. package/dist/screen.js +0 -105
  79. package/dist/trajectory-gates.d.ts +0 -137
  80. package/dist/trajectory-gates.js +0 -576
  81. package/dist/trusted-host-supervision.d.ts +0 -25
  82. package/dist/trusted-host-supervision.js +0 -94
  83. package/dist/work-capture-schema.d.ts +0 -38
  84. package/dist/work-capture-schema.js +0 -33
  85. package/dist/work-capture.d.ts +0 -43
  86. package/dist/work-capture.js +0 -81
  87. package/dist/work-signals.d.ts +0 -55
  88. package/dist/work-signals.js +0 -71
  89. package/schemas/qualification-cell-result-v1.schema.json +0 -32
  90. package/schemas/qualification-config-v1.schema.json +0 -244
  91. package/schemas/qualification-invocation-request-v1.schema.json +0 -59
  92. package/schemas/qualification-judge-panel-result-v1.schema.json +0 -42
  93. package/schemas/trajectory-event-v1.schema.json +0 -80
@@ -1,188 +0,0 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { collapseQualificationJudgePanel, aggregateQualificationCell } from "./qualification-panels.js";
4
- import { readQualificationJudgePanelMember } from "./qualification-runner.js";
5
- import { atomicWriteCanonical, qualificationSpoolPaths, readCanonicalJson, readQualificationInvocation, readQualificationSpoolConfig } from "./qualification-store.js";
6
- import { qualificationCanonicalJson } from "./qualification-config.js";
7
- export function recordQualificationJudgePanel(options) {
8
- qualificationDerivedId(options.panel_id, "panel_id");
9
- const config = readQualificationSpoolConfig(options.spool_dir);
10
- if (!config.judge_panel || !config.board)
11
- throw new Error("qualification spool does not select judge panels");
12
- const declaredCell = config.board.cells.find((cell) => cell.panels.some((panel) => panel.id === options.panel_id));
13
- if (!declaredCell)
14
- throw new Error(`qualification panel ${options.panel_id} is not declared by the approved board`);
15
- const invocations = options.member_invocation_ids.map((id) => readQualificationInvocation(options.spool_dir, id));
16
- if (invocations.length < 2 || invocations.length > 3)
17
- throw new Error("qualification panel requires two or three member invocation ids");
18
- const first = invocations[0];
19
- if (!first.panel || first.panel.id !== options.panel_id)
20
- throw new Error("qualification panel id does not match its first member");
21
- for (const invocation of invocations)
22
- assertInvocationMatchesDeclaredCell(invocation, declaredCell);
23
- for (const [index, invocation] of invocations.entries()) {
24
- if (!invocation.panel || invocation.panel.id !== options.panel_id || invocation.panel.member_ordinal !== index + 1 ||
25
- invocation.panel.subject_invocation_id !== first.panel.subject_invocation_id ||
26
- invocation.panel.subject_artifact_sha256 !== first.panel.subject_artifact_sha256 ||
27
- invocation.measurement_identity_sha256 !== first.measurement_identity_sha256 ||
28
- invocation.scenario.id !== first.scenario.id || invocation.scenario.version !== first.scenario.version ||
29
- invocation.scenario.stimulus_sha256 !== first.scenario.stimulus_sha256 || invocation.scenario.rubric_sha256 !== first.scenario.rubric_sha256 ||
30
- invocation.repetition !== first.repetition || invocation.arms.subject !== first.arms.subject || invocation.arms.judge !== first.arms.judge) {
31
- throw new Error(`qualification panel member ${invocation.invocation_id} binding mismatch`);
32
- }
33
- }
34
- const members = options.member_invocation_ids.map((id) => readQualificationJudgePanelMember(options.spool_dir, id));
35
- const result = collapseQualificationJudgePanel({
36
- panel_id: options.panel_id,
37
- scenario_id: declaredCell.scenario_id,
38
- subject_arm: declaredCell.subject_arm,
39
- repetition: declaredCell.panels.find((panel) => panel.id === options.panel_id).repetition,
40
- critical: declaredCell.critical,
41
- members,
42
- });
43
- if (result.state === "unresolved" && result.disagreement.initial_split)
44
- throw new Error(`qualification panel ${options.panel_id} requires its authorized tie-break before a final result can be recorded`);
45
- const path = join(qualificationSpoolPaths(options.spool_dir).root, "panels", `${options.panel_id}.json`);
46
- atomicWriteCanonical(path, result, true);
47
- return result;
48
- }
49
- export function recordQualificationCell(options) {
50
- qualificationDerivedId(options.cell_id, "cell_id");
51
- const config = readQualificationSpoolConfig(options.spool_dir);
52
- const declared = config.board?.cells.find((cell) => cell.id === options.cell_id);
53
- if (!config.judge_panel || !declared)
54
- throw new Error(`qualification cell ${options.cell_id} is not declared by the approved panel board`);
55
- const panelIds = declared.panels.map((panel) => panel.id);
56
- const panels = panelIds.map((id) => readAndValidatePanel(options.spool_dir, id));
57
- if (new Set(panels.map((panel) => panel.repetition)).size !== panels.length)
58
- throw new Error("qualification cell contains duplicate repetitions");
59
- if (panels.some((panel) => panel.critical !== declared.critical))
60
- throw new Error("qualification cell criticality does not match its approved board declaration");
61
- const result = aggregateQualificationCell({
62
- cell_id: options.cell_id,
63
- scenario_id: declared.scenario_id,
64
- subject_arm: declared.subject_arm,
65
- critical: declared.critical,
66
- pass_threshold: declared.pass_threshold,
67
- panels,
68
- });
69
- atomicWriteCanonical(join(qualificationSpoolPaths(options.spool_dir).root, "cells", `${options.cell_id}.json`), result, true);
70
- return result;
71
- }
72
- export function validateQualificationPanelOutputs(spoolDir) {
73
- const root = qualificationSpoolPaths(spoolDir).root;
74
- const config = readQualificationSpoolConfig(spoolDir);
75
- const invocationIds = readdirSync(qualificationSpoolPaths(spoolDir).invocations, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
76
- const invocations = invocationIds.map((id) => readQualificationInvocation(spoolDir, id));
77
- const panelIds = jsonRecordIds(join(root, "panels"));
78
- if (config.judge_panel) {
79
- if (!config.board)
80
- throw new Error("qualification panel configuration lacks its approved board");
81
- const expectedPanels = config.board.cells.flatMap((cell) => cell.panels.map((panel) => panel.id)).sort();
82
- if (qualificationCanonicalJson(panelIds) !== qualificationCanonicalJson(expectedPanels))
83
- throw new Error("qualification panel outputs are incomplete or contain undeclared panels");
84
- const expectedSubjectSlots = config.board.cells.flatMap((cell) => cell.panels.map((panel) => ({ cell, repetition: panel.repetition })));
85
- const subjectInvocations = invocations.filter((invocation) => invocation.role === "subject");
86
- if (subjectInvocations.length !== expectedSubjectSlots.length)
87
- throw new Error("qualification approved board subject invocations are incomplete or contain extras");
88
- for (const slot of expectedSubjectSlots) {
89
- const matching = subjectInvocations.filter((invocation) => invocation.repetition === slot.repetition && invocation.scenario.id === slot.cell.scenario_id && invocation.arms.selected === slot.cell.subject_arm);
90
- if (matching.length !== 1)
91
- throw new Error("qualification approved board must have exactly one subject invocation per scenario/arm/repetition");
92
- assertInvocationMatchesDeclaredCell(matching[0], slot.cell);
93
- }
94
- }
95
- else if (panelIds.length > 0)
96
- throw new Error("historical qualification spool contains undeclared panel outputs");
97
- for (const id of panelIds)
98
- readAndValidatePanel(spoolDir, id);
99
- const cellIds = jsonRecordIds(join(root, "cells"));
100
- if (config.judge_panel) {
101
- const expectedCells = config.board.cells.map((cell) => cell.id).sort();
102
- if (qualificationCanonicalJson(cellIds) !== qualificationCanonicalJson(expectedCells))
103
- throw new Error("qualification cell outputs are incomplete or contain undeclared cells");
104
- }
105
- else if (cellIds.length > 0)
106
- throw new Error("historical qualification spool contains undeclared cell outputs");
107
- for (const id of cellIds) {
108
- const path = join(root, "cells", `${id}.json`);
109
- const recorded = readCanonicalJson(path, `qualification cell ${id}`);
110
- const declared = config.board?.cells.find((cell) => cell.id === id);
111
- if (!declared)
112
- throw new Error(`qualification cell ${id} is not declared by the approved board`);
113
- const panels = declared.panels.map((panel) => readAndValidatePanel(spoolDir, panel.id));
114
- const rebuilt = aggregateQualificationCell({
115
- cell_id: declared.id,
116
- scenario_id: declared.scenario_id,
117
- subject_arm: declared.subject_arm,
118
- critical: declared.critical,
119
- pass_threshold: declared.pass_threshold,
120
- panels,
121
- });
122
- if (qualificationCanonicalJson(recorded) !== qualificationCanonicalJson(rebuilt))
123
- throw new Error(`qualification cell ${id} does not match validated panel evidence`);
124
- }
125
- return { panels: panelIds.length, cells: cellIds.length };
126
- }
127
- function readAndValidatePanel(spoolDir, id) {
128
- const path = join(qualificationSpoolPaths(spoolDir).root, "panels", `${id}.json`);
129
- const recorded = readCanonicalJson(path, `qualification judge panel ${id}`);
130
- const config = readQualificationSpoolConfig(spoolDir);
131
- const declared = config.board?.cells.find((cell) => cell.panels.some((panel) => panel.id === id));
132
- if (!declared)
133
- throw new Error(`qualification judge panel ${id} is not declared by the approved board`);
134
- const invocations = recorded.members.map((member) => readQualificationInvocation(spoolDir, member.invocation_id));
135
- const first = invocations[0];
136
- if (!first?.panel || first.panel.id !== id)
137
- throw new Error(`qualification judge panel ${id} member binding mismatch`);
138
- for (const [index, invocation] of invocations.entries()) {
139
- if (!invocation.panel || invocation.panel.id !== id || invocation.panel.member_ordinal !== index + 1 ||
140
- invocation.panel.subject_invocation_id !== first.panel.subject_invocation_id ||
141
- invocation.panel.subject_artifact_sha256 !== first.panel.subject_artifact_sha256 ||
142
- invocation.measurement_identity_sha256 !== first.measurement_identity_sha256 ||
143
- invocation.scenario.id !== first.scenario.id || invocation.scenario.version !== first.scenario.version ||
144
- invocation.scenario.stimulus_sha256 !== first.scenario.stimulus_sha256 || invocation.scenario.rubric_sha256 !== first.scenario.rubric_sha256 ||
145
- invocation.repetition !== first.repetition || invocation.arms.subject !== first.arms.subject || invocation.arms.judge !== first.arms.judge) {
146
- throw new Error(`qualification judge panel ${id} member binding mismatch`);
147
- }
148
- }
149
- for (const invocation of invocations)
150
- assertInvocationMatchesDeclaredCell(invocation, declared);
151
- const members = recorded.members.map((member) => readQualificationJudgePanelMember(spoolDir, member.invocation_id));
152
- const rebuilt = collapseQualificationJudgePanel({
153
- panel_id: id,
154
- scenario_id: declared.scenario_id,
155
- subject_arm: declared.subject_arm,
156
- repetition: declared.panels.find((panel) => panel.id === id).repetition,
157
- critical: declared.critical,
158
- members,
159
- });
160
- if (rebuilt.state === "unresolved" && rebuilt.disagreement.initial_split)
161
- throw new Error(`qualification judge panel ${id} is incomplete: its clean split requires an authorized tie-break`);
162
- if (qualificationCanonicalJson(recorded) !== qualificationCanonicalJson(rebuilt))
163
- throw new Error(`qualification judge panel ${id} does not match validated invocation evidence`);
164
- return recorded;
165
- }
166
- function assertInvocationMatchesDeclaredCell(invocation, cell) {
167
- if (invocation.measurement_identity_sha256 !== cell.measurement_identity_sha256 || invocation.scenario.id !== cell.scenario_id ||
168
- invocation.scenario.version !== cell.scenario_version || invocation.scenario.stimulus_sha256 !== cell.stimulus_sha256 ||
169
- invocation.scenario.rubric_sha256 !== cell.rubric_sha256 || invocation.arms.subject !== cell.subject_arm ||
170
- (invocation.role === "subject" && (invocation.scenario.input_sha256 !== cell.subject_input_sha256 || invocation.arms.selected !== cell.subject_arm)) ||
171
- (invocation.role === "judge" && (!invocation.panel || cell.judge_arms[invocation.panel.member_ordinal - 1] !== invocation.arms.selected))) {
172
- throw new Error(`qualification invocation ${invocation.invocation_id} does not match its approved board cell identity`);
173
- }
174
- }
175
- function qualificationDerivedId(value, label) {
176
- if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value))
177
- throw new Error(`qualification ${label} must be a bounded ASCII identifier`);
178
- }
179
- function jsonRecordIds(path) {
180
- if (!existsSync(path))
181
- return [];
182
- const entries = readdirSync(path, { withFileTypes: true });
183
- const invalid = entries.find((entry) => !entry.isFile() || !entry.name.endsWith(".json"));
184
- if (invalid)
185
- throw new Error(`qualification derived-output directory contains undeclared entry ${invalid.name}`);
186
- return entries.map((entry) => entry.name.slice(0, -5)).sort();
187
- }
188
- //# sourceMappingURL=qualification-panel-store.js.map
@@ -1,99 +0,0 @@
1
- import { type PanelVote } from "./vote-panel.js";
2
- export declare const QUALIFICATION_JUDGE_PANEL_POLICY_VERSION: "qualification-judge-panel-policy-v1";
3
- export declare const QUALIFICATION_JUDGE_PANEL_RESULT_VERSION: "qualification-judge-panel-result-v1";
4
- export declare const QUALIFICATION_CELL_RESULT_VERSION: "qualification-cell-result-v1";
5
- /** Closed policy persisted in qualification configuration. */
6
- export declare const QUALIFICATION_JUDGE_PANEL_POLICY: {
7
- readonly schema_version: "qualification-judge-panel-policy-v1";
8
- readonly initial_judge_calls: 2;
9
- readonly agreement: "two-matching-clean-votes";
10
- readonly split: "one-conditional-tie-break";
11
- readonly max_judge_calls: 3;
12
- readonly error: "record-as-non-vote";
13
- readonly unresolved: "inconclusive-blocks-acceptance";
14
- readonly behavioral_failure: "record-and-continue-read-only-board";
15
- readonly integrity_failure: "halt";
16
- readonly wave_a_call_budget: {
17
- readonly artifacts: 54;
18
- readonly minimum: 108;
19
- readonly maximum: 162;
20
- };
21
- readonly complete_program_call_budget: {
22
- readonly artifacts: 642;
23
- readonly minimum: 1284;
24
- readonly maximum: 1926;
25
- };
26
- };
27
- export interface QualificationJudgePanelMember extends PanelVote {
28
- invocation_id: string;
29
- judge: {
30
- provider: string;
31
- model: string;
32
- };
33
- reason: string;
34
- artifact: {
35
- sha256: string;
36
- bytes: number;
37
- } | null;
38
- terminal_receipt_sha256: string;
39
- }
40
- export interface QualificationJudgePanelResult {
41
- schema_version: typeof QUALIFICATION_JUDGE_PANEL_RESULT_VERSION;
42
- panel_id: string;
43
- scenario_id: string;
44
- subject_arm: string;
45
- repetition: number;
46
- critical: boolean;
47
- state: "confirmed" | "tie_broken" | "unresolved";
48
- verdict?: "PASS" | "FAIL";
49
- members: QualificationJudgePanelMember[];
50
- judge_calls: number;
51
- clean_votes: number;
52
- disagreement: {
53
- initial_split: boolean;
54
- minority_rate: number;
55
- };
56
- acceptance: "pass" | "fail" | "inconclusive";
57
- collection: "continue" | "halt";
58
- }
59
- /**
60
- * Collapse members that have already passed the qualification invocation,
61
- * OAuth, accounting, receipt, and artifact boundary.
62
- */
63
- export declare function collapseQualificationJudgePanel(options: {
64
- panel_id: string;
65
- scenario_id: string;
66
- subject_arm: string;
67
- repetition: number;
68
- critical: boolean;
69
- members: QualificationJudgePanelMember[];
70
- }): QualificationJudgePanelResult;
71
- export interface QualificationCellResult {
72
- schema_version: typeof QUALIFICATION_CELL_RESULT_VERSION;
73
- cell_id: string;
74
- scenario_id: string;
75
- subject_arm: string;
76
- critical: boolean;
77
- pass_threshold: number;
78
- panels: QualificationJudgePanelResult[];
79
- verdict?: "PASS" | "FAIL";
80
- critical_failure: boolean;
81
- acceptance: "pass" | "fail" | "inconclusive";
82
- collection: "continue" | "halt";
83
- disagreement: {
84
- judge_calls: number;
85
- clean_votes: number;
86
- split_artifacts: number;
87
- artifacts_with_two_clean_initial_votes: number;
88
- unresolved_artifacts: number;
89
- judge_split_rate: number;
90
- };
91
- }
92
- export declare function aggregateQualificationCell(options: {
93
- cell_id: string;
94
- scenario_id: string;
95
- subject_arm: string;
96
- critical: boolean;
97
- pass_threshold: number;
98
- panels: QualificationJudgePanelResult[];
99
- }): QualificationCellResult;
@@ -1,91 +0,0 @@
1
- import { collapseVotePanel } from "./vote-panel.js";
2
- export const QUALIFICATION_JUDGE_PANEL_POLICY_VERSION = "qualification-judge-panel-policy-v1";
3
- export const QUALIFICATION_JUDGE_PANEL_RESULT_VERSION = "qualification-judge-panel-result-v1";
4
- export const QUALIFICATION_CELL_RESULT_VERSION = "qualification-cell-result-v1";
5
- /** Closed policy persisted in qualification configuration. */
6
- export const QUALIFICATION_JUDGE_PANEL_POLICY = {
7
- schema_version: QUALIFICATION_JUDGE_PANEL_POLICY_VERSION,
8
- initial_judge_calls: 2,
9
- agreement: "two-matching-clean-votes",
10
- split: "one-conditional-tie-break",
11
- max_judge_calls: 3,
12
- error: "record-as-non-vote",
13
- unresolved: "inconclusive-blocks-acceptance",
14
- behavioral_failure: "record-and-continue-read-only-board",
15
- integrity_failure: "halt",
16
- wave_a_call_budget: { artifacts: 54, minimum: 108, maximum: 162 },
17
- complete_program_call_budget: { artifacts: 642, minimum: 1284, maximum: 1926 },
18
- };
19
- /**
20
- * Collapse members that have already passed the qualification invocation,
21
- * OAuth, accounting, receipt, and artifact boundary.
22
- */
23
- export function collapseQualificationJudgePanel(options) {
24
- const collapse = collapseVotePanel(options.members);
25
- if (options.members.length < 2 || options.members.length > 3)
26
- throw new Error("qualification judge panel must contain two or three members");
27
- if (options.members.some((member, index) => member.ordinal !== index + 1))
28
- throw new Error("qualification judge panel member ordinals must be contiguous from one");
29
- if (options.members.length === 3 && !collapse.split)
30
- throw new Error("qualification judge panel third member is permitted only after a clean initial split");
31
- const acceptance = collapse.state === "unresolved" ? "inconclusive" : collapse.verdict === "PASS" ? "pass" : "fail";
32
- return {
33
- schema_version: QUALIFICATION_JUDGE_PANEL_RESULT_VERSION,
34
- panel_id: options.panel_id,
35
- scenario_id: options.scenario_id,
36
- subject_arm: options.subject_arm,
37
- repetition: options.repetition,
38
- critical: options.critical,
39
- state: collapse.state,
40
- ...(collapse.verdict ? { verdict: collapse.verdict } : {}),
41
- members: options.members,
42
- judge_calls: options.members.length,
43
- clean_votes: collapse.clean_votes,
44
- disagreement: { initial_split: collapse.split, minority_rate: collapse.minority_rate },
45
- acceptance,
46
- collection: "continue",
47
- };
48
- }
49
- export function aggregateQualificationCell(options) {
50
- if (!Number.isSafeInteger(options.pass_threshold) || options.pass_threshold < 1)
51
- throw new Error("qualification cell pass_threshold must be a positive integer");
52
- if (options.panels.length === 0)
53
- throw new Error("qualification cell requires at least one panel");
54
- if (new Set(options.panels.map((panel) => panel.panel_id)).size !== options.panels.length || new Set(options.panels.map((panel) => panel.repetition)).size !== options.panels.length)
55
- throw new Error("qualification cell contains duplicate panels or repetitions");
56
- for (const panel of options.panels) {
57
- if (panel.scenario_id !== options.scenario_id || panel.subject_arm !== options.subject_arm || panel.critical !== options.critical)
58
- throw new Error("qualification cell panel identity or criticality mismatch");
59
- }
60
- const unresolved = options.panels.filter((panel) => panel.state === "unresolved").length;
61
- const passes = options.panels.filter((panel) => panel.verdict === "PASS").length;
62
- // Same safety rule as ordinary skill-harness repetitions: a declared threshold
63
- // cannot weaken Critical semantics; every clean Critical repetition must pass.
64
- const effectiveThreshold = options.critical ? options.panels.length : options.pass_threshold;
65
- const verdict = unresolved > 0 ? undefined : passes >= effectiveThreshold ? "PASS" : "FAIL";
66
- const denominator = options.panels.filter((panel) => panel.members.filter((member) => member.ordinal <= 2 && !member.suspect && (member.verdict === "PASS" || member.verdict === "FAIL")).length === 2).length;
67
- const splitArtifacts = options.panels.filter((panel) => panel.disagreement.initial_split).length;
68
- const acceptance = verdict === undefined ? "inconclusive" : verdict === "PASS" ? "pass" : "fail";
69
- return {
70
- schema_version: QUALIFICATION_CELL_RESULT_VERSION,
71
- cell_id: options.cell_id,
72
- scenario_id: options.scenario_id,
73
- subject_arm: options.subject_arm,
74
- critical: options.critical,
75
- pass_threshold: effectiveThreshold,
76
- panels: options.panels,
77
- ...(verdict ? { verdict } : {}),
78
- critical_failure: options.critical && verdict === "FAIL",
79
- acceptance,
80
- collection: "continue",
81
- disagreement: {
82
- judge_calls: options.panels.reduce((sum, panel) => sum + panel.judge_calls, 0),
83
- clean_votes: options.panels.reduce((sum, panel) => sum + panel.clean_votes, 0),
84
- split_artifacts: splitArtifacts,
85
- artifacts_with_two_clean_initial_votes: denominator,
86
- unresolved_artifacts: unresolved,
87
- judge_split_rate: denominator === 0 ? 0 : splitArtifacts / denominator,
88
- },
89
- };
90
- }
91
- //# sourceMappingURL=qualification-panels.js.map
@@ -1,13 +0,0 @@
1
- import type { ChildProcess } from "node:child_process";
2
- export interface QualificationProcessIdentity {
3
- pid: number;
4
- platform: NodeJS.Platform;
5
- boot_id: string | null;
6
- start_ticks: string | null;
7
- }
8
- export declare function qualificationProcessIdentity(pid: number): QualificationProcessIdentity;
9
- export declare function qualificationProcessMatches(identity: QualificationProcessIdentity): boolean;
10
- export declare function cleanupQualificationProcessGroupAfterLeaderExit(pid: number | undefined, original: QualificationProcessIdentity | null): Promise<void>;
11
- export declare function terminateQualificationProcess(child: ChildProcess, identity: QualificationProcessIdentity | null): void;
12
- export declare function terminateQualificationByIdentity(identity: QualificationProcessIdentity): void;
13
- export declare function terminateInterruptedQualificationProcessGroup(identity: QualificationProcessIdentity): Promise<void>;
@@ -1,190 +0,0 @@
1
- import { readFileSync, readdirSync } from "node:fs";
2
- import { setTimeout as sleep } from "node:timers/promises";
3
- export function qualificationProcessIdentity(pid) {
4
- if (!Number.isInteger(pid) || pid < 1)
5
- throw new Error(`qualification process ${String(pid)} is not live`);
6
- if (process.platform === "linux") {
7
- let stat;
8
- try {
9
- stat = readFileSync(`/proc/${pid}/stat`, "utf8");
10
- }
11
- catch {
12
- throw new Error(`qualification process ${pid} is not live`);
13
- }
14
- const close = stat.lastIndexOf(")");
15
- if (close < 0)
16
- throw new Error(`qualification process ${pid} identity is unreadable`);
17
- const remainder = stat.slice(close + 2).trim().split(/\s+/);
18
- const startTicks = remainder[19]; // field 22; remainder starts at field 3
19
- if (!/^\d+$/.test(startTicks ?? ""))
20
- throw new Error(`qualification process ${pid} start identity is unreadable`);
21
- let bootId;
22
- try {
23
- bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
24
- }
25
- catch {
26
- throw new Error("qualification Linux boot identity is unreadable");
27
- }
28
- return { pid, platform: process.platform, boot_id: bootId, start_ticks: startTicks };
29
- }
30
- try {
31
- process.kill(pid, 0);
32
- }
33
- catch {
34
- throw new Error(`qualification process ${pid} is not live`);
35
- }
36
- return { pid, platform: process.platform, boot_id: null, start_ticks: null };
37
- }
38
- export function qualificationProcessMatches(identity) {
39
- try {
40
- const current = qualificationProcessIdentity(identity.pid);
41
- return sameProcessIdentity(current, identity);
42
- }
43
- catch {
44
- return false;
45
- }
46
- }
47
- export async function cleanupQualificationProcessGroupAfterLeaderExit(pid, original) {
48
- if (process.platform !== "linux" || !pid)
49
- return;
50
- const members = qualificationOwnedLinuxProcessGroupMembers(pid, original);
51
- signalQualificationOccurrences(members, "SIGTERM");
52
- if (members.length === 0)
53
- return;
54
- await sleep(75);
55
- signalQualificationOccurrences(members, "SIGKILL");
56
- }
57
- export function terminateQualificationProcess(child, identity) {
58
- if (identity && !qualificationProcessMatches(identity))
59
- return;
60
- if (process.platform === "linux" && child.pid) {
61
- const members = qualificationOwnedLinuxProcessGroupMembers(child.pid, identity);
62
- signalQualificationOccurrences(members, "SIGTERM");
63
- setTimeout(() => signalQualificationOccurrences(members, "SIGKILL"), 75).unref();
64
- return;
65
- }
66
- try {
67
- child.kill("SIGTERM");
68
- }
69
- catch { /* process already exited */ }
70
- setTimeout(() => {
71
- if (identity && !qualificationProcessMatches(identity))
72
- return;
73
- try {
74
- child.kill("SIGKILL");
75
- }
76
- catch { /* process already exited */ }
77
- }, 75).unref();
78
- }
79
- export function terminateQualificationByIdentity(identity) {
80
- if (!qualificationProcessMatches(identity))
81
- throw new Error(`qualification process identity for pid ${identity.pid} is stale or reused`);
82
- if (process.platform === "linux") {
83
- const members = qualificationOwnedLinuxProcessGroupMembers(identity.pid, identity);
84
- signalQualificationOccurrences(members, "SIGTERM");
85
- setTimeout(() => signalQualificationOccurrences(members, "SIGKILL"), 75).unref();
86
- return;
87
- }
88
- try {
89
- process.kill(identity.pid, "SIGTERM");
90
- }
91
- catch { /* supervisor will reconcile the terminal state */ }
92
- setTimeout(() => {
93
- if (!qualificationProcessMatches(identity))
94
- return;
95
- try {
96
- process.kill(identity.pid, "SIGKILL");
97
- }
98
- catch { /* process already exited */ }
99
- }, 75).unref();
100
- }
101
- export async function terminateInterruptedQualificationProcessGroup(identity) {
102
- if (qualificationProcessMatches(identity)) {
103
- await terminateAndWaitByIdentity(identity);
104
- return;
105
- }
106
- if (process.platform !== "linux")
107
- return;
108
- const survivingMembers = qualificationLinuxProcessGroupMembers(identity.pid);
109
- // If the numeric group leader exists with a different occurrence, the PGID was
110
- // reused and is not ours. A leaderless group cannot be recreated while its old
111
- // descendants still hold that PGID, so only that leaderless case is safe to reap.
112
- if (survivingMembers.some((member) => member.pid === identity.pid))
113
- return;
114
- signalQualificationOccurrences(survivingMembers, "SIGTERM");
115
- await sleep(75);
116
- signalQualificationOccurrences(survivingMembers, "SIGKILL");
117
- }
118
- async function terminateAndWaitByIdentity(identity) {
119
- if (!qualificationProcessMatches(identity))
120
- return;
121
- if (process.platform === "linux") {
122
- const members = qualificationOwnedLinuxProcessGroupMembers(identity.pid, identity);
123
- signalQualificationOccurrences(members, "SIGTERM");
124
- await sleep(75);
125
- signalQualificationOccurrences(members, "SIGKILL");
126
- }
127
- else {
128
- try {
129
- process.kill(identity.pid, "SIGTERM");
130
- }
131
- catch {
132
- return;
133
- }
134
- await sleep(75);
135
- if (qualificationProcessMatches(identity)) {
136
- try {
137
- process.kill(identity.pid, "SIGKILL");
138
- }
139
- catch { /* process already exited */ }
140
- }
141
- }
142
- const deadline = Date.now() + 2000;
143
- while (qualificationProcessMatches(identity) && Date.now() < deadline)
144
- await sleep(10);
145
- }
146
- function qualificationOwnedLinuxProcessGroupMembers(processGroupId, original) {
147
- const members = qualificationLinuxProcessGroupMembers(processGroupId);
148
- const currentLeader = members.find((member) => member.pid === processGroupId);
149
- if (currentLeader && (!original || !sameProcessIdentity(currentLeader, original)))
150
- return [];
151
- return members;
152
- }
153
- function qualificationLinuxProcessGroupMembers(processGroupId) {
154
- if (process.platform !== "linux")
155
- return [];
156
- const members = [];
157
- // Dirent construction may lstat an unrelated disappearing PID before our
158
- // per-entry guard runs. Names need no metadata; validate each stat below.
159
- for (const name of readdirSync("/proc")) {
160
- if (!/^\d+$/.test(name))
161
- continue;
162
- const pid = Number(name);
163
- try {
164
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
165
- const close = stat.lastIndexOf(")");
166
- if (close < 0)
167
- continue;
168
- const fields = stat.slice(close + 2).trim().split(/\s+/);
169
- if (Number(fields[2]) !== processGroupId)
170
- continue; // field 5 (pgrp); fields start at field 3
171
- members.push(qualificationProcessIdentity(pid));
172
- }
173
- catch { /* process exited during the scan */ }
174
- }
175
- return members;
176
- }
177
- function signalQualificationOccurrences(identities, signal) {
178
- for (const identity of identities) {
179
- if (!qualificationProcessMatches(identity))
180
- continue;
181
- try {
182
- process.kill(identity.pid, signal);
183
- }
184
- catch { /* process exited after occurrence validation */ }
185
- }
186
- }
187
- function sameProcessIdentity(left, right) {
188
- return left.pid === right.pid && left.platform === right.platform && left.boot_id === right.boot_id && left.start_ticks === right.start_ticks;
189
- }
190
- //# sourceMappingURL=qualification-process.js.map