@tiphys/kernel 0.0.0 → 0.2.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 (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * READING AND RESOLVING REVIEW CHECKLISTS (kernel plan M3, M3-P7 steps 2 to
3
+ * 5; R-054, T-001).
4
+ *
5
+ * `checklists/` ships at the package root beside `gate-registry.yaml`,
6
+ * `assurance-modes.yaml` and `schemas/`. This module locates it, decodes a
7
+ * checklist, merges a per-phase extra probe file into it, and orders the
8
+ * result under a declared framing. It answers one question: WHICH PROBES, IN
9
+ * WHICH ORDER, does this reviewer answer.
10
+ *
11
+ * NO VALIDATION HAPPENS HERE, deliberately, and this is the same split
12
+ * `src/modes.ts` documents: `tiphys validate --type checklist` decides whether
13
+ * a document is well formed, and duplicating its rules in a reader would
14
+ * produce a second opinion to keep in sync. The COMMAND validates before it
15
+ * serves (`src/commands/checklist.ts`), which is where the M3-P3 fix round put
16
+ * that duty after shipping a reader that printed without checking.
17
+ *
18
+ * THE MERGE FAILURES ARE FAILURES HERE AND NOT WARNINGS. An extra file
19
+ * reusing a canonical probe id is a COLLISION and names both sources; an
20
+ * extra probe missing `evidence-required` is a refusal; and, added in fix
21
+ * round 2, an extra file DECLARING A FRAMING is a refusal, because framings
22
+ * are read from the canonical checklist only and the entry point was
23
+ * previously dropped without a word. Last-wins and silent-drop are the same
24
+ * dangerous state seen from two sides: the phase's own hazard class names "an
25
+ * extra-probe merge that silently overrides a canonical probe instead of
26
+ * colliding", and a silent anything is how a per-phase file quietly diverges
27
+ * from the standing checklist.
28
+ *
29
+ * ORDERING IS BY SCOPE AND FILE POSITION, NEVER BY PROBE ID. A framing names
30
+ * `applies-to` scopes; probes in a named scope lead, scope by scope, and
31
+ * inside one scope the FILE ORDER decides. That is what makes CLAUDE.md's
32
+ * "the reviewer's FIRST check is item 3" falsifiable: move the probe later in
33
+ * the file and the resolved head changes. A framing that named probe ids
34
+ * would pin the head wherever the probe sat, which is the hazard class's
35
+ * "ordering expressed as a comment rather than as position" one level up.
36
+ */
37
+ /** The shipped directory's basename, at the package root. */
38
+ export declare const CHECKLISTS_DIRNAME = "checklists";
39
+ /** One probe as the document declares it. */
40
+ export interface Probe {
41
+ id: string;
42
+ probe: string;
43
+ appliesTo: string;
44
+ evidenceRequired: boolean;
45
+ verifiesGate?: string;
46
+ /** Where this probe came from, for the collision message. */
47
+ source: string;
48
+ }
49
+ /** One declared entry point into the same probe list. */
50
+ export interface Framing {
51
+ id: string;
52
+ entryPoint: string;
53
+ ordersProbes: string[];
54
+ }
55
+ export interface Checklist {
56
+ id: string;
57
+ appliesTo: string;
58
+ probes: Probe[];
59
+ framings: Framing[];
60
+ /** The decoded document, for the validator to see unaltered. */
61
+ raw: unknown;
62
+ /** The file this was read from. */
63
+ path: string;
64
+ }
65
+ type Read<T> = {
66
+ ok: true;
67
+ value: T;
68
+ } | {
69
+ ok: false;
70
+ reason: string;
71
+ };
72
+ /** The shipped `checklists/` directory, found by walking up from this module. */
73
+ export declare function checklistsDirectory(): string;
74
+ /** Every checklist id the shipped directory declares, by filename, sorted. */
75
+ export declare function shippedChecklistIds(): string[];
76
+ /**
77
+ * Project a decoded checklist document into the shape the resolver uses.
78
+ *
79
+ * FIELDS ABSENT FROM THE DOCUMENT STAY ABSENT rather than becoming defaults,
80
+ * with one exception stated here so it is not a surprise: `evidence-required`
81
+ * is projected as `false` when it is absent, and the CALLER is what refuses
82
+ * such a probe. Defaulting it to `true` would let a probe missing the field
83
+ * merge in as though it carried the stronger obligation, which is R-054's
84
+ * second named failure passing silently.
85
+ */
86
+ export declare function projectChecklist(document: unknown, path: string, source: string): Read<Checklist>;
87
+ /**
88
+ * Read a checklist document from an explicit path.
89
+ *
90
+ * The path is OPERATOR-SUPPLIED (D-M3-27), so it is classified before it is
91
+ * opened rather than opened and hoped about: a named pipe at `--extra` is a
92
+ * reported refusal, never a command that blocks forever.
93
+ */
94
+ export declare function readChecklistFile(path: string, source: string): Read<Checklist>;
95
+ /** Read a shipped checklist by id, or say which ids exist. */
96
+ export declare function readShippedChecklist(id: string): Read<Checklist>;
97
+ export interface MergeProblem {
98
+ /** The probe id at fault. */
99
+ probe: string;
100
+ reason: string;
101
+ }
102
+ /**
103
+ * Merge an extra probe file into a canonical checklist.
104
+ *
105
+ * BOTH FAILURES ARE COLLECTED RATHER THAN THROWN AT THE FIRST, so an author
106
+ * fixing an extra file sees every problem in one run instead of one per
107
+ * invocation.
108
+ */
109
+ export declare function mergeExtraProbes(canonical: Checklist, extra: Checklist): {
110
+ probes: Probe[];
111
+ problems: MergeProblem[];
112
+ };
113
+ /**
114
+ * An extra file may not declare a framing, and saying so is a REFUSAL.
115
+ *
116
+ * M3-P7 FIX ROUND 2, H-2 MEMBER 2. `resolveChecklist` reads
117
+ * `request.checklist.framings` and only that, so a framing declared in an
118
+ * `--extra` file was neither merged nor mentioned: the run exited 0 with an
119
+ * empty stderr and the author's entry point was gone. That is a SILENT NO-OP
120
+ * on R-054's own use case, and the extra file validates as a full checklist
121
+ * document, so declaring a framing in one is the natural thing to try.
122
+ *
123
+ * BOTH SHAPES ARE REFUSED, colliding and not, because the mechanism is the
124
+ * same one: the extra document's framings are never read. Refusing only the
125
+ * COLLIDING one would fix the instance the review constructed and leave the
126
+ * mechanism, which is exactly the fix-round failure CLAUDE.md's contract
127
+ * exists against. The two get different messages because the consequences
128
+ * differ: a collision is also the shape that would let a per-phase file
129
+ * shadow a standing entry point if the merge were ever added, and the
130
+ * message says which document wins today.
131
+ *
132
+ * IT IS A REFUSAL RATHER THAN A MERGE. Merging extra framings would be a new
133
+ * capability, and nothing asks for one; what was wrong was the silence.
134
+ * Nothing depends on the old behaviour: `framings` is not in the checklist
135
+ * schema's `required`, so an extra file that declares none is unaffected.
136
+ */
137
+ export declare function extraFramingRefusals(canonical: Checklist, extra: Checklist): string[];
138
+ /**
139
+ * Order probes under a framing.
140
+ *
141
+ * Probes whose `applies-to` scope is named by the framing come first, scope
142
+ * by scope in the framing's order, and inside a scope in FILE ORDER. Every
143
+ * probe in no named scope follows, also in file order. NOTHING IS DROPPED: a
144
+ * framing changes what a reviewer reads first and never what they read at
145
+ * all, because a framing that filtered would let an entry point silently
146
+ * retire a probe.
147
+ */
148
+ export declare function orderUnderFraming(probes: readonly Probe[], framing: Framing): Probe[];
149
+ export interface ResolveRequest {
150
+ checklist: Checklist;
151
+ extra?: Checklist;
152
+ framingId?: string;
153
+ }
154
+ export interface Resolution {
155
+ probes: Probe[];
156
+ framing?: Framing;
157
+ }
158
+ export type ResolveOutcome = {
159
+ ok: true;
160
+ value: Resolution;
161
+ } | {
162
+ ok: false;
163
+ reasons: string[];
164
+ };
165
+ /** Merge, then order. The two failures above and an unknown framing are refusals. */
166
+ export declare function resolveChecklist(request: ResolveRequest): ResolveOutcome;
167
+ /** The resolved list as lines, the framing's entry point at the head. */
168
+ export declare function renderResolution(checklist: Checklist, resolution: Resolution): string[];
169
+ export {};
@@ -0,0 +1,310 @@
1
+ /**
2
+ * READING AND RESOLVING REVIEW CHECKLISTS (kernel plan M3, M3-P7 steps 2 to
3
+ * 5; R-054, T-001).
4
+ *
5
+ * `checklists/` ships at the package root beside `gate-registry.yaml`,
6
+ * `assurance-modes.yaml` and `schemas/`. This module locates it, decodes a
7
+ * checklist, merges a per-phase extra probe file into it, and orders the
8
+ * result under a declared framing. It answers one question: WHICH PROBES, IN
9
+ * WHICH ORDER, does this reviewer answer.
10
+ *
11
+ * NO VALIDATION HAPPENS HERE, deliberately, and this is the same split
12
+ * `src/modes.ts` documents: `tiphys validate --type checklist` decides whether
13
+ * a document is well formed, and duplicating its rules in a reader would
14
+ * produce a second opinion to keep in sync. The COMMAND validates before it
15
+ * serves (`src/commands/checklist.ts`), which is where the M3-P3 fix round put
16
+ * that duty after shipping a reader that printed without checking.
17
+ *
18
+ * THE MERGE FAILURES ARE FAILURES HERE AND NOT WARNINGS. An extra file
19
+ * reusing a canonical probe id is a COLLISION and names both sources; an
20
+ * extra probe missing `evidence-required` is a refusal; and, added in fix
21
+ * round 2, an extra file DECLARING A FRAMING is a refusal, because framings
22
+ * are read from the canonical checklist only and the entry point was
23
+ * previously dropped without a word. Last-wins and silent-drop are the same
24
+ * dangerous state seen from two sides: the phase's own hazard class names "an
25
+ * extra-probe merge that silently overrides a canonical probe instead of
26
+ * colliding", and a silent anything is how a per-phase file quietly diverges
27
+ * from the standing checklist.
28
+ *
29
+ * ORDERING IS BY SCOPE AND FILE POSITION, NEVER BY PROBE ID. A framing names
30
+ * `applies-to` scopes; probes in a named scope lead, scope by scope, and
31
+ * inside one scope the FILE ORDER decides. That is what makes CLAUDE.md's
32
+ * "the reviewer's FIRST check is item 3" falsifiable: move the probe later in
33
+ * the file and the resolved head changes. A framing that named probe ids
34
+ * would pin the head wherever the probe sat, which is the hazard class's
35
+ * "ordering expressed as a comment rather than as position" one level up.
36
+ */
37
+ import { readdirSync } from "node:fs";
38
+ import { join } from "node:path";
39
+ import { decodeDocument, readOperatorPath } from "./validate.js";
40
+ import { packageRoot } from "./modes.js";
41
+ /** The shipped directory's basename, at the package root. */
42
+ export const CHECKLISTS_DIRNAME = "checklists";
43
+ function asRecord(value) {
44
+ return typeof value === "object" && value !== null && !Array.isArray(value)
45
+ ? value
46
+ : undefined;
47
+ }
48
+ /** The shipped `checklists/` directory, found by walking up from this module. */
49
+ export function checklistsDirectory() {
50
+ return join(packageRoot(), CHECKLISTS_DIRNAME);
51
+ }
52
+ /** Every checklist id the shipped directory declares, by filename, sorted. */
53
+ export function shippedChecklistIds() {
54
+ return readdirSync(checklistsDirectory())
55
+ .filter((name) => name.endsWith(".yaml"))
56
+ .map((name) => name.slice(0, -".yaml".length))
57
+ .sort();
58
+ }
59
+ /**
60
+ * Project a decoded checklist document into the shape the resolver uses.
61
+ *
62
+ * FIELDS ABSENT FROM THE DOCUMENT STAY ABSENT rather than becoming defaults,
63
+ * with one exception stated here so it is not a surprise: `evidence-required`
64
+ * is projected as `false` when it is absent, and the CALLER is what refuses
65
+ * such a probe. Defaulting it to `true` would let a probe missing the field
66
+ * merge in as though it carried the stronger obligation, which is R-054's
67
+ * second named failure passing silently.
68
+ */
69
+ export function projectChecklist(document, path, source) {
70
+ const record = asRecord(document);
71
+ if (record === undefined) {
72
+ return { ok: false, reason: `${path} is not a mapping` };
73
+ }
74
+ const probes = [];
75
+ const rawProbes = Array.isArray(record["probes"]) ? record["probes"] : [];
76
+ for (const entry of rawProbes) {
77
+ const probe = asRecord(entry);
78
+ if (probe === undefined) {
79
+ continue;
80
+ }
81
+ const projected = {
82
+ id: String(probe["id"] ?? ""),
83
+ probe: String(probe["probe"] ?? ""),
84
+ appliesTo: String(probe["applies-to"] ?? ""),
85
+ evidenceRequired: probe["evidence-required"] === true,
86
+ source,
87
+ };
88
+ if (typeof probe["verifies-gate"] === "string") {
89
+ projected.verifiesGate = probe["verifies-gate"];
90
+ }
91
+ probes.push(projected);
92
+ }
93
+ const framings = [];
94
+ const rawFramings = Array.isArray(record["framings"]) ? record["framings"] : [];
95
+ for (const entry of rawFramings) {
96
+ const framing = asRecord(entry);
97
+ if (framing === undefined) {
98
+ continue;
99
+ }
100
+ framings.push({
101
+ id: String(framing["id"] ?? ""),
102
+ entryPoint: String(framing["entry-point"] ?? ""),
103
+ ordersProbes: (Array.isArray(framing["orders-probes"])
104
+ ? framing["orders-probes"]
105
+ : []).map((scope) => String(scope)),
106
+ });
107
+ }
108
+ return {
109
+ ok: true,
110
+ value: {
111
+ id: String(record["id"] ?? ""),
112
+ appliesTo: String(record["applies-to"] ?? ""),
113
+ probes,
114
+ framings,
115
+ raw: document,
116
+ path,
117
+ },
118
+ };
119
+ }
120
+ /**
121
+ * Read a checklist document from an explicit path.
122
+ *
123
+ * The path is OPERATOR-SUPPLIED (D-M3-27), so it is classified before it is
124
+ * opened rather than opened and hoped about: a named pipe at `--extra` is a
125
+ * reported refusal, never a command that blocks forever.
126
+ */
127
+ export function readChecklistFile(path, source) {
128
+ const read = readOperatorPath(path);
129
+ if (!read.ok) {
130
+ return { ok: false, reason: read.reason };
131
+ }
132
+ const decoded = decodeDocument(read.body, path);
133
+ if (!decoded.ok) {
134
+ return { ok: false, reason: decoded.reason };
135
+ }
136
+ return projectChecklist(decoded.value, path, source);
137
+ }
138
+ /** Read a shipped checklist by id, or say which ids exist. */
139
+ export function readShippedChecklist(id) {
140
+ let directory;
141
+ try {
142
+ directory = checklistsDirectory();
143
+ }
144
+ catch (error) {
145
+ return { ok: false, reason: String(error instanceof Error ? error.message : error) };
146
+ }
147
+ let declared;
148
+ try {
149
+ declared = shippedChecklistIds();
150
+ }
151
+ catch (error) {
152
+ return { ok: false, reason: String(error instanceof Error ? error.message : error) };
153
+ }
154
+ if (!declared.includes(id)) {
155
+ return {
156
+ ok: false,
157
+ reason: `no checklist ${id} is shipped; the shipped checklists are ${declared.join(", ")}`,
158
+ };
159
+ }
160
+ return readChecklistFile(join(directory, `${id}.yaml`), `checklists/${id}.yaml`);
161
+ }
162
+ /**
163
+ * Merge an extra probe file into a canonical checklist.
164
+ *
165
+ * BOTH FAILURES ARE COLLECTED RATHER THAN THROWN AT THE FIRST, so an author
166
+ * fixing an extra file sees every problem in one run instead of one per
167
+ * invocation.
168
+ */
169
+ export function mergeExtraProbes(canonical, extra) {
170
+ const problems = [];
171
+ const bySourceId = new Map();
172
+ for (const probe of canonical.probes) {
173
+ bySourceId.set(probe.id, probe);
174
+ }
175
+ const merged = [...canonical.probes];
176
+ for (const probe of extra.probes) {
177
+ const clash = bySourceId.get(probe.id);
178
+ if (clash !== undefined) {
179
+ /* NAMES BOTH SOURCES. A collision message naming only the extra file
180
+ tells an author a probe id is taken and not by what, which is the
181
+ difference between a message and a diagnosis. */
182
+ problems.push({
183
+ probe: probe.id,
184
+ reason: `probe id ${probe.id} is declared in ${clash.source} and again in ${probe.source}`,
185
+ });
186
+ continue;
187
+ }
188
+ if (!probe.evidenceRequired) {
189
+ /* R-054's second failure. Projected as `false` when the field is
190
+ absent, and the two cases are DIFFERENT documents with the same
191
+ merge outcome: an extra probe that omits the field and one that sets
192
+ it false are both refused, because a per-phase probe answerable
193
+ without citing anything is the thing the extension mechanism must
194
+ not be able to add. */
195
+ problems.push({
196
+ probe: probe.id,
197
+ reason: `probe id ${probe.id} in ${probe.source} does not require evidence; every extra probe must set evidence-required: true`,
198
+ });
199
+ continue;
200
+ }
201
+ bySourceId.set(probe.id, probe);
202
+ merged.push(probe);
203
+ }
204
+ return { probes: merged, problems };
205
+ }
206
+ /**
207
+ * An extra file may not declare a framing, and saying so is a REFUSAL.
208
+ *
209
+ * M3-P7 FIX ROUND 2, H-2 MEMBER 2. `resolveChecklist` reads
210
+ * `request.checklist.framings` and only that, so a framing declared in an
211
+ * `--extra` file was neither merged nor mentioned: the run exited 0 with an
212
+ * empty stderr and the author's entry point was gone. That is a SILENT NO-OP
213
+ * on R-054's own use case, and the extra file validates as a full checklist
214
+ * document, so declaring a framing in one is the natural thing to try.
215
+ *
216
+ * BOTH SHAPES ARE REFUSED, colliding and not, because the mechanism is the
217
+ * same one: the extra document's framings are never read. Refusing only the
218
+ * COLLIDING one would fix the instance the review constructed and leave the
219
+ * mechanism, which is exactly the fix-round failure CLAUDE.md's contract
220
+ * exists against. The two get different messages because the consequences
221
+ * differ: a collision is also the shape that would let a per-phase file
222
+ * shadow a standing entry point if the merge were ever added, and the
223
+ * message says which document wins today.
224
+ *
225
+ * IT IS A REFUSAL RATHER THAN A MERGE. Merging extra framings would be a new
226
+ * capability, and nothing asks for one; what was wrong was the silence.
227
+ * Nothing depends on the old behaviour: `framings` is not in the checklist
228
+ * schema's `required`, so an extra file that declares none is unaffected.
229
+ */
230
+ export function extraFramingRefusals(canonical, extra) {
231
+ const canonicalIds = new Set(canonical.framings.map((framing) => framing.id));
232
+ return extra.framings.map((framing) => canonicalIds.has(framing.id)
233
+ ? `framing id ${framing.id} is declared in ${canonical.path} and again in ${extra.path}; an extra file cannot declare a framing, and the canonical entry point is the one checklist resolve serves`
234
+ : `framing id ${framing.id} is declared in ${extra.path}; an extra file cannot declare a framing, because checklist resolve reads framings from the canonical checklist only`);
235
+ }
236
+ /**
237
+ * Order probes under a framing.
238
+ *
239
+ * Probes whose `applies-to` scope is named by the framing come first, scope
240
+ * by scope in the framing's order, and inside a scope in FILE ORDER. Every
241
+ * probe in no named scope follows, also in file order. NOTHING IS DROPPED: a
242
+ * framing changes what a reviewer reads first and never what they read at
243
+ * all, because a framing that filtered would let an entry point silently
244
+ * retire a probe.
245
+ */
246
+ export function orderUnderFraming(probes, framing) {
247
+ const ordered = [];
248
+ const taken = new Set();
249
+ for (const scope of framing.ordersProbes) {
250
+ for (const probe of probes) {
251
+ if (probe.appliesTo === scope && !taken.has(probe)) {
252
+ ordered.push(probe);
253
+ taken.add(probe);
254
+ }
255
+ }
256
+ }
257
+ for (const probe of probes) {
258
+ if (!taken.has(probe)) {
259
+ ordered.push(probe);
260
+ }
261
+ }
262
+ return ordered;
263
+ }
264
+ /** Merge, then order. The two failures above and an unknown framing are refusals. */
265
+ export function resolveChecklist(request) {
266
+ let probes = request.checklist.probes;
267
+ if (request.extra !== undefined) {
268
+ const merged = mergeExtraProbes(request.checklist, request.extra);
269
+ /* COLLECTED TOGETHER, not reported one class per invocation, for the
270
+ reason `mergeExtraProbes` records: an author fixing an extra file sees
271
+ every problem in one run. */
272
+ const reasons = [
273
+ ...merged.problems.map((problem) => problem.reason),
274
+ ...extraFramingRefusals(request.checklist, request.extra),
275
+ ];
276
+ if (reasons.length > 0) {
277
+ return { ok: false, reasons };
278
+ }
279
+ probes = merged.probes;
280
+ }
281
+ if (request.framingId === undefined) {
282
+ return { ok: true, value: { probes: [...probes] } };
283
+ }
284
+ const framing = request.checklist.framings.find((candidate) => candidate.id === request.framingId);
285
+ if (framing === undefined) {
286
+ const declared = request.checklist.framings.map((candidate) => candidate.id);
287
+ return {
288
+ ok: false,
289
+ reasons: [
290
+ `${request.checklist.path} declares no framing ${request.framingId}; it declares ${declared.length === 0 ? "none" : declared.join(", ")}`,
291
+ ],
292
+ };
293
+ }
294
+ return { ok: true, value: { probes: orderUnderFraming(probes, framing), framing } };
295
+ }
296
+ /** The resolved list as lines, the framing's entry point at the head. */
297
+ export function renderResolution(checklist, resolution) {
298
+ const lines = [`checklist ${checklist.id}`];
299
+ if (resolution.framing !== undefined) {
300
+ lines.push(`framing ${resolution.framing.id}`);
301
+ lines.push(`entry-point ${resolution.framing.entryPoint.trim().replace(/\s+/g, " ")}`);
302
+ }
303
+ lines.push(`probes ${String(resolution.probes.length)}`);
304
+ for (let index = 0; index < resolution.probes.length; index += 1) {
305
+ const probe = resolution.probes[index];
306
+ lines.push(`${String(index + 1)}. ${probe.id} [${probe.appliesTo}]${probe.evidenceRequired ? " evidence-required" : ""}${probe.verifiesGate === undefined ? "" : ` verifies-gate:${probe.verifiesGate}`}`);
307
+ lines.push(` ${probe.probe.trim().replace(/\s+/g, " ")}`);
308
+ }
309
+ return lines;
310
+ }