@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.
- package/AGENTS.md +663 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +299 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +92 -0
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +66 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +1539 -0
- package/dist/src/checks.js +5025 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +68 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +343 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +264 -0
- package/dist/src/commands/doctor.js +1546 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +177 -0
- package/dist/src/commands/lock.d.ts +34 -0
- package/dist/src/commands/lock.js +340 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +139 -0
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +195 -0
- package/dist/src/commands/status.d.ts +21 -0
- package/dist/src/commands/status.js +122 -0
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +87 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +430 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/cutover.d.ts +584 -0
- package/dist/src/cutover.js +1444 -0
- package/dist/src/exclusion.d.ts +389 -0
- package/dist/src/exclusion.js +843 -0
- package/dist/src/exec/env.d.ts +278 -0
- package/dist/src/exec/env.js +334 -0
- package/dist/src/fleet.d.ts +223 -0
- package/dist/src/fleet.js +298 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1169 -0
- package/dist/src/gates/coverage.d.ts +375 -0
- package/dist/src/gates/coverage.js +836 -0
- package/dist/src/gates/credentials.d.ts +233 -0
- package/dist/src/gates/credentials.js +752 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +482 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +614 -0
- package/dist/src/gates/run.js +1614 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +968 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +84 -0
- package/dist/src/hooks.js +125 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +256 -0
- package/dist/src/lock.js +792 -0
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +34 -0
- package/dist/src/path-identity.js +48 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +326 -0
- package/dist/src/pool.js +988 -0
- package/dist/src/roles.d.ts +461 -0
- package/dist/src/roles.js +776 -0
- package/dist/src/spawn.d.ts +482 -0
- package/dist/src/spawn.js +1003 -0
- package/dist/src/status.d.ts +116 -0
- package/dist/src/status.js +148 -0
- package/dist/src/task.d.ts +504 -0
- package/dist/src/task.js +307 -0
- package/dist/src/teardown.d.ts +39 -0
- package/dist/src/teardown.js +422 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +263 -0
- package/dist/src/validate.js +742 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +849 -0
- package/dist/src/witness/run.d.ts +299 -0
- package/dist/src/witness/run.js +1373 -0
- package/dist/src/witness/spec.d.ts +270 -0
- package/dist/src/witness/spec.js +475 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +526 -0
- package/gates.manifest.json +257 -0
- package/package.json +72 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +463 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +82 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +185 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +295 -0
- package/schemas/work-history.schema.json +183 -0
- package/schemas/write-bypass.schema.json +69 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- 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
|
+
}
|