@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,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* READING THE SHIPPED ASSURANCE MODE DEFINITIONS (kernel plan M3, M3-P3
|
|
3
|
+
* step 5).
|
|
4
|
+
*
|
|
5
|
+
* `assurance-modes.yaml` ships at the package root, beside `gate-registry.yaml`
|
|
6
|
+
* and the `schemas/` directory. This module locates it, decodes it, and
|
|
7
|
+
* answers one question: what does a declared mode require. Nothing here
|
|
8
|
+
* RESOLVES a mode into behaviour and nothing here enforces one. M3 never
|
|
9
|
+
* executes `direct-pr` or `local-only`, and building an enforcement engine for
|
|
10
|
+
* a mode this milestone never enters is the M1-P3 failure the plan is trying
|
|
11
|
+
* not to repeat.
|
|
12
|
+
*
|
|
13
|
+
* NO VALIDATION HAPPENS HERE, deliberately. `tiphys validate --type
|
|
14
|
+
* assurance-modes` is the command that says whether the document is
|
|
15
|
+
* well-formed, and duplicating its rules in a reader would produce a second
|
|
16
|
+
* opinion to keep in sync. This module reads what is there and reports what it
|
|
17
|
+
* cannot find.
|
|
18
|
+
*/
|
|
19
|
+
import { readdirSync } from "node:fs";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { decodeDocument, readOperatorPath } from "./validate.js";
|
|
23
|
+
/** The shipped document's basename, at the package root. */
|
|
24
|
+
export const MODES_FILENAME = "assurance-modes.yaml";
|
|
25
|
+
/**
|
|
26
|
+
* Locate the package root by walking UP from this module and testing for the
|
|
27
|
+
* shipped document.
|
|
28
|
+
*
|
|
29
|
+
* The depth differs between the two layouts this code runs in: from source it
|
|
30
|
+
* is `src/` and the root is one level up, and from the built entry it is
|
|
31
|
+
* `dist/src/` and the root is two levels up. Counting `..` would be right in
|
|
32
|
+
* exactly one of them, which is the layout-dependent break
|
|
33
|
+
* `schemasDirectory()` already documents. Walking up and TESTING is right in
|
|
34
|
+
* both, and in a relocated copy as well.
|
|
35
|
+
*/
|
|
36
|
+
export function packageRoot() {
|
|
37
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
39
|
+
try {
|
|
40
|
+
if (readdirSync(directory).includes(MODES_FILENAME)) {
|
|
41
|
+
return directory;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* not readable here; keep walking */
|
|
46
|
+
}
|
|
47
|
+
const parent = dirname(directory);
|
|
48
|
+
if (parent === directory) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
directory = parent;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`the shipped ${MODES_FILENAME} was not found above this module; the installation is incomplete`);
|
|
54
|
+
}
|
|
55
|
+
function asRecord(value) {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
57
|
+
? value
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
function strings(record, key) {
|
|
61
|
+
const value = record[key];
|
|
62
|
+
return Array.isArray(value)
|
|
63
|
+
? value.filter((entry) => typeof entry === "string")
|
|
64
|
+
: [];
|
|
65
|
+
}
|
|
66
|
+
function optionalStrings(record, key) {
|
|
67
|
+
return record[key] === undefined ? undefined : strings(record, key);
|
|
68
|
+
}
|
|
69
|
+
/** Read and decode the shipped document, or say why it could not be read. */
|
|
70
|
+
export function readModes(path = join(packageRoot(), MODES_FILENAME)) {
|
|
71
|
+
const read = readOperatorPath(path);
|
|
72
|
+
if (!read.ok) {
|
|
73
|
+
return { ok: false, reason: read.reason };
|
|
74
|
+
}
|
|
75
|
+
const decoded = decodeDocument(read.body, path);
|
|
76
|
+
if (!decoded.ok) {
|
|
77
|
+
return { ok: false, reason: decoded.reason };
|
|
78
|
+
}
|
|
79
|
+
const document = asRecord(decoded.value);
|
|
80
|
+
if (document === undefined) {
|
|
81
|
+
return { ok: false, reason: `${path} does not decode to a mapping` };
|
|
82
|
+
}
|
|
83
|
+
const raw = document["modes"];
|
|
84
|
+
if (!Array.isArray(raw)) {
|
|
85
|
+
return { ok: false, reason: `${path} declares no modes list` };
|
|
86
|
+
}
|
|
87
|
+
const modes = [];
|
|
88
|
+
for (const entry of raw) {
|
|
89
|
+
const record = asRecord(entry);
|
|
90
|
+
if (record === undefined) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
modes.push({
|
|
94
|
+
id: String(record["id"] ?? ""),
|
|
95
|
+
declaredBy: String(record["declared-by"] ?? ""),
|
|
96
|
+
pipeline: strings(record, "pipeline"),
|
|
97
|
+
skips: strings(record, "skips"),
|
|
98
|
+
gateSets: strings(record, "gate-sets"),
|
|
99
|
+
mergeAuthority: String(record["merge-authority"] ?? ""),
|
|
100
|
+
grantedBy: typeof record["granted-by"] === "string" ? record["granted-by"] : undefined,
|
|
101
|
+
conditions: optionalStrings(record, "conditions"),
|
|
102
|
+
reviewContracts: optionalStrings(record, "review-contracts"),
|
|
103
|
+
escalationBounds: asRecord(record["escalation-bounds"]),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, path, raw: decoded.value, modes };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The id blueprint section 8 gives the un-downgraded process BY NAME: "The
|
|
110
|
+
* current proven process is the definition of `full`."
|
|
111
|
+
*
|
|
112
|
+
* SIBLING LITERAL, named so neither site can drift unnoticed:
|
|
113
|
+
* `REFERENCE_MODE_ID` at src/checks.ts:275 is the same string, used as the
|
|
114
|
+
* reference pipeline every other mode's omissions are measured against. The two
|
|
115
|
+
* are pinned from opposite directions by registered tests: the checks-side
|
|
116
|
+
* literal by the assertion that deleting `full` produces `no mode declares id
|
|
117
|
+
* full` (test/assurance-modes.test.ts:314), and this one by the assertion that
|
|
118
|
+
* the mode `mode show` annotates as un-downgraded is the mode named `full`.
|
|
119
|
+
*/
|
|
120
|
+
const UNDOWNGRADED_MODE_ID = "full";
|
|
121
|
+
/**
|
|
122
|
+
* The execution status of one mode, DERIVED rather than looked up in a list of
|
|
123
|
+
* ids (CR-004 item 2, DR-0020).
|
|
124
|
+
*
|
|
125
|
+
* Two facts are available and both are checkable by the reader: whether this is
|
|
126
|
+
* the kernel's own document, and whether this mode IS the one blueprint section
|
|
127
|
+
* 8 names as the un-downgraded process, "The current proven process is the
|
|
128
|
+
* definition of `full`. Downgrades are declared, never improvised."
|
|
129
|
+
*
|
|
130
|
+
* WHY THE NAME AND NOT THE SKIP COUNT (CR-002, round 9). This function used
|
|
131
|
+
* `mode.skips.length === 0` as its proxy for "this is the un-downgraded mode".
|
|
132
|
+
* The proxy held only because `full` happened to be the only mode with an empty
|
|
133
|
+
* list, nothing enforced that, and `skips[]` is shipped DATA. Two measured
|
|
134
|
+
* consequences, both at exit 0 with every registry gate green: giving `full` one
|
|
135
|
+
* bogus `skips[]` entry made this function say that no phase of the tiphys
|
|
136
|
+
* project had ever been delivered under `full`, which is false about the mode
|
|
137
|
+
* this project has delivered every phase under; and giving `direct-pr` an empty
|
|
138
|
+
* `skips[]` made a mode nobody has ever entered claim to be the one the project
|
|
139
|
+
* follows, with `merge-authority: owner` printed beneath it, which is not the
|
|
140
|
+
* regime in force (DR-0015). Blueprint section 8 defines `full` BY NAME, so the
|
|
141
|
+
* name is the primary fact and the skip count is a consequence of it.
|
|
142
|
+
*
|
|
143
|
+
* THE COUNT IS STILL REPORTED, as a fact about the mode rather than as the
|
|
144
|
+
* ground of the claim. That distinction is the whole finding: a number may be
|
|
145
|
+
* shown without being believed.
|
|
146
|
+
*
|
|
147
|
+
* WHAT MAKES THE `full` SENTENCE TRUE IS DATA, AND IT IS GUARDED IN TWO PLACES
|
|
148
|
+
* THAT COVER DIFFERENT DOCUMENTS. Keying off the name moves the burden: the
|
|
149
|
+
* claim is only as good as the `full` in the document being served genuinely
|
|
150
|
+
* being un-downgraded.
|
|
151
|
+
*
|
|
152
|
+
* The VALIDATOR carries it for EVERY document (round 10, CRB9-02).
|
|
153
|
+
* `mode-no-undeclared-downgrade` rejects a `skips[]` entry that the same mode's
|
|
154
|
+
* pipeline runs, and also one that the reference does not run; on the reference
|
|
155
|
+
* those two exhaust the cases, so a `full` declaring any skipped stage at all is
|
|
156
|
+
* refused and this sentence is never printed over it. Until that second part
|
|
157
|
+
* existed, a `full` whose stage had MOVED from `pipeline` into `skips` was an
|
|
158
|
+
* HONEST downgrade that validated at exit 0, and `tiphys mode show --mode full`
|
|
159
|
+
* printed this sentence about fifteen lines above a `skips: deploy-verify` row.
|
|
160
|
+
*
|
|
161
|
+
* A REGISTERED TEST carries it for THIS repository's own document, asserting the
|
|
162
|
+
* shipped `full` declares no skipped stage. It is kept rather than superseded:
|
|
163
|
+
* a test guards the document, the check guards every other document, and the
|
|
164
|
+
* failure this pair exists to prevent is a property asserted where it is stated
|
|
165
|
+
* and not enforced where it is consumed.
|
|
166
|
+
*
|
|
167
|
+
* WHAT THIS DELIBERATELY DOES NOT SAY. It does not say that tiphys runs
|
|
168
|
+
* anything: nothing runs on tiphys before M4. The un-downgraded mode of the
|
|
169
|
+
* kernel's own document is the process the tiphys PROJECT follows for its own
|
|
170
|
+
* delivery; the downgraded ones have never been entered at all. And for a
|
|
171
|
+
* document that is not the kernel's own, the answer is that tiphys does not
|
|
172
|
+
* know, because it does not.
|
|
173
|
+
*/
|
|
174
|
+
export function executionStatus(mode, context) {
|
|
175
|
+
if (!context.shippedDocument) {
|
|
176
|
+
return ("not determinable here. This is not the kernel's own assurance-modes.yaml, " +
|
|
177
|
+
"so nothing tiphys ships records whether any phase has been delivered under " +
|
|
178
|
+
"this mode (DR-0020).");
|
|
179
|
+
}
|
|
180
|
+
if (mode.id === UNDOWNGRADED_MODE_ID) {
|
|
181
|
+
return (`this mode is ${UNDOWNGRADED_MODE_ID}, which blueprint section 8 defines by name as the ` +
|
|
182
|
+
"un-downgraded process, and it is the one the tiphys project follows for its own delivery.");
|
|
183
|
+
}
|
|
184
|
+
return (`DECLARED AND VALIDATED, NEVER EXERCISED. This mode is not ${UNDOWNGRADED_MODE_ID}, which ` +
|
|
185
|
+
"blueprint section 8 defines by name as the un-downgraded process, so no phase of the " +
|
|
186
|
+
`tiphys project has ever been delivered under it. It declares ${String(mode.skips.length)} ` +
|
|
187
|
+
"skipped stage(s). Its pipeline and its gate selection are checked by validation only " +
|
|
188
|
+
"(DR-0020).");
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The standing limits of this release, printed on every invocation.
|
|
192
|
+
*
|
|
193
|
+
* IT SAYS ONLY WHAT THE SHIPPED SCHEMAS DO. The vocabularies really are closed
|
|
194
|
+
* enums, so "a document naming any other id is rejected" is the enum's own
|
|
195
|
+
* behaviour and not a claim about intent. The M4 sentence is attributed to
|
|
196
|
+
* DR-0020 rather than stated as a property of the code.
|
|
197
|
+
*/
|
|
198
|
+
export const RELEASE_LIMITS = "limits: the mode, stage and role vocabularies in the shipped schemas are this " +
|
|
199
|
+
"repository's own closed enums, so a document naming any other id is rejected and a " +
|
|
200
|
+
"consuming project cannot extend them at v0.1.0; whether to open them is an M4 question " +
|
|
201
|
+
"(DR-0020). This command SHOWS a declared mode: nothing in this release resolves a " +
|
|
202
|
+
"project into a mode, enforces one, or runs one.";
|
|
203
|
+
/**
|
|
204
|
+
* Render one mode for a human or for a brief.
|
|
205
|
+
*
|
|
206
|
+
* THE SHAPE IS PART OF THE CONTRACT, because criterion 2 asserts over it: a
|
|
207
|
+
* section is a line ending in a colon at column zero, and its items are the
|
|
208
|
+
* lines indented by exactly two spaces beneath it. That makes "prints exactly
|
|
209
|
+
* the twelve stage ids in order" something a test can extract rather than
|
|
210
|
+
* something a reader has to eyeball.
|
|
211
|
+
*/
|
|
212
|
+
export function renderMode(mode, context) {
|
|
213
|
+
const lines = [`mode: ${mode.id}`];
|
|
214
|
+
/* SECOND LINE, not a footnote. CR-004 measured that `mode show` printed a
|
|
215
|
+
never-exercised mode with exactly the confidence of the exercised one, and
|
|
216
|
+
that the only disclosure lived in `delivery/`, which the package excludes. */
|
|
217
|
+
lines.push(`execution-status: ${executionStatus(mode, context)}`);
|
|
218
|
+
lines.push(`merge-authority: ${mode.mergeAuthority}`);
|
|
219
|
+
if (mode.grantedBy !== undefined) {
|
|
220
|
+
lines.push(`granted-by: ${mode.grantedBy}`);
|
|
221
|
+
}
|
|
222
|
+
const section = (name, items) => {
|
|
223
|
+
if (items === undefined) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
lines.push(`${name}:`);
|
|
227
|
+
if (items.length === 0) {
|
|
228
|
+
lines.push(" (none)");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const item of items) {
|
|
232
|
+
lines.push(` ${item}`);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
section("pipeline", mode.pipeline);
|
|
236
|
+
section("skips", mode.skips);
|
|
237
|
+
section("gate-sets", mode.gateSets);
|
|
238
|
+
section("review-contracts", mode.reviewContracts);
|
|
239
|
+
if (mode.escalationBounds !== undefined) {
|
|
240
|
+
/* CR-004 item 3. The bounds are DATA an orchestrator brief cites. Nothing
|
|
241
|
+
in this release counts a fix round or detects a recurrence, so a bare
|
|
242
|
+
`escalation-bounds:` header invites the reader to assume an enforcement
|
|
243
|
+
engine that does not exist. */
|
|
244
|
+
lines.push("escalation-bounds (data an orchestrator brief cites; nothing in this release counts fix rounds, detects recurrence, or enforces these):");
|
|
245
|
+
for (const key of Object.keys(mode.escalationBounds).sort()) {
|
|
246
|
+
lines.push(` ${key}: ${String(mode.escalationBounds[key])}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (mode.conditions !== undefined) {
|
|
250
|
+
lines.push("conditions:");
|
|
251
|
+
for (const condition of mode.conditions) {
|
|
252
|
+
lines.push(` ${condition.replace(/\s+/g, " ").trim()}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
lines.push(`declared-by: ${mode.declaredBy.replace(/\s+/g, " ").trim()}`);
|
|
256
|
+
lines.push(RELEASE_LIMITS);
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Compare two existing paths by their canonical filesystem identity. */
|
|
2
|
+
export declare function pathsIdentifySameObject(left: string, right: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Decide whether two paths NAME THE SAME filesystem object when at least
|
|
5
|
+
* one of them was produced by ANOTHER PROGRAM rather than composed here.
|
|
6
|
+
*
|
|
7
|
+
* THE MECHANISM THIS EXISTS FOR, measured 2026-09-16 on the macOS smoke
|
|
8
|
+
* job of pull request #155. `path.resolve` normalizes `.`, `..` and
|
|
9
|
+
* relative segments and does NOT resolve symlinks, so two spellings of one
|
|
10
|
+
* directory stay unequal as strings. Programs the kernel shells out to do
|
|
11
|
+
* not preserve the caller's spelling: git canonicalizes every worktree
|
|
12
|
+
* path it records (`git worktree add /link/wt` then `git worktree list
|
|
13
|
+
* --porcelain` reports `/real/wt`, measured on Linux), and node reports
|
|
14
|
+
* the canonical path of a test file in its reporter's `file` field. So a
|
|
15
|
+
* kernel-composed path compared by string against such a value answers
|
|
16
|
+
* "different object" for the same object, silently, and every decision
|
|
17
|
+
* taken on that answer is wrong in the direction that does nothing.
|
|
18
|
+
*
|
|
19
|
+
* On macOS the symlink is supplied by the platform and needs no unusual
|
|
20
|
+
* setup: `/tmp` is a symlink to `/private/tmp` and `os.tmpdir()` returns a
|
|
21
|
+
* path under `/var/folders`, where `/var` is a symlink to `/private/var`.
|
|
22
|
+
* The CLI is accidentally immune because `process.cwd()` is already
|
|
23
|
+
* canonical, so only a caller that hands a path in makes this reachable,
|
|
24
|
+
* which is exactly what a library consumer does.
|
|
25
|
+
*
|
|
26
|
+
* The string comparison is tried FIRST and kept, rather than replaced, for
|
|
27
|
+
* two reasons: it answers without touching the filesystem in the common
|
|
28
|
+
* case, and it still gives the right answer for two paths that do not
|
|
29
|
+
* exist, where `realpathSync` can only raise. This function therefore says
|
|
30
|
+
* "same" strictly more often than `resolve(left) === resolve(right)` does,
|
|
31
|
+
* and never says "same" about two objects that are genuinely different:
|
|
32
|
+
* equal canonical paths ARE one object.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pathsNameSameObject(left: string, right: string): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/** Compare two existing paths by their canonical filesystem identity. */
|
|
4
|
+
export function pathsIdentifySameObject(left, right) {
|
|
5
|
+
try {
|
|
6
|
+
return realpathSync(left) === realpathSync(right);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Decide whether two paths NAME THE SAME filesystem object when at least
|
|
14
|
+
* one of them was produced by ANOTHER PROGRAM rather than composed here.
|
|
15
|
+
*
|
|
16
|
+
* THE MECHANISM THIS EXISTS FOR, measured 2026-09-16 on the macOS smoke
|
|
17
|
+
* job of pull request #155. `path.resolve` normalizes `.`, `..` and
|
|
18
|
+
* relative segments and does NOT resolve symlinks, so two spellings of one
|
|
19
|
+
* directory stay unequal as strings. Programs the kernel shells out to do
|
|
20
|
+
* not preserve the caller's spelling: git canonicalizes every worktree
|
|
21
|
+
* path it records (`git worktree add /link/wt` then `git worktree list
|
|
22
|
+
* --porcelain` reports `/real/wt`, measured on Linux), and node reports
|
|
23
|
+
* the canonical path of a test file in its reporter's `file` field. So a
|
|
24
|
+
* kernel-composed path compared by string against such a value answers
|
|
25
|
+
* "different object" for the same object, silently, and every decision
|
|
26
|
+
* taken on that answer is wrong in the direction that does nothing.
|
|
27
|
+
*
|
|
28
|
+
* On macOS the symlink is supplied by the platform and needs no unusual
|
|
29
|
+
* setup: `/tmp` is a symlink to `/private/tmp` and `os.tmpdir()` returns a
|
|
30
|
+
* path under `/var/folders`, where `/var` is a symlink to `/private/var`.
|
|
31
|
+
* The CLI is accidentally immune because `process.cwd()` is already
|
|
32
|
+
* canonical, so only a caller that hands a path in makes this reachable,
|
|
33
|
+
* which is exactly what a library consumer does.
|
|
34
|
+
*
|
|
35
|
+
* The string comparison is tried FIRST and kept, rather than replaced, for
|
|
36
|
+
* two reasons: it answers without touching the filesystem in the common
|
|
37
|
+
* case, and it still gives the right answer for two paths that do not
|
|
38
|
+
* exist, where `realpathSync` can only raise. This function therefore says
|
|
39
|
+
* "same" strictly more often than `resolve(left) === resolve(right)` does,
|
|
40
|
+
* and never says "same" about two objects that are genuinely different:
|
|
41
|
+
* equal canonical paths ARE one object.
|
|
42
|
+
*/
|
|
43
|
+
export function pathsNameSameObject(left, right) {
|
|
44
|
+
if (resolve(left) === resolve(right)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return pathsIdentifySameObject(left, right);
|
|
48
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PHASE-DECLARATION PROJECTION (kernel plan M3, M3-P1 step 2; D-M3-18).
|
|
3
|
+
*
|
|
4
|
+
* The M2-P4 scope auditor reads one document per phase from
|
|
5
|
+
* `delivery/plan/phase-declarations/<phase-id-lowercased>.json`, out of the
|
|
6
|
+
* MERGE BASE of the audited branch. Until now that document was a SECOND
|
|
7
|
+
* HAND-AUTHORED SOURCE beside the plan, and two hand-authored sources of the
|
|
8
|
+
* same fact drift. This module makes it a GENERATED VIEW of the plan, so the
|
|
9
|
+
* plan is the one source and the auditor's input is derived from it.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THE PROJECTION EMITS, taken from the DELIVERED
|
|
12
|
+
* `src/gates/schemas/phase-declaration.schema.json` rather than from a
|
|
13
|
+
* description of it. The delivered schema sets `additionalProperties: false`,
|
|
14
|
+
* so an extra property is a REJECTION and not an ignored field, and the
|
|
15
|
+
* emitted key set is exactly these five, camelCase:
|
|
16
|
+
*
|
|
17
|
+
* id, branch, filesToTouch, declaredExtras, citations
|
|
18
|
+
*
|
|
19
|
+
* TWO VOCABULARIES MEET HERE AND NOWHERE ELSE. The plan spells the same
|
|
20
|
+
* fields `files-to-touch` and `extras`, because the plan is a document
|
|
21
|
+
* authored for people. This projector is the single translation point, and
|
|
22
|
+
* that is D-M3-18's whole value.
|
|
23
|
+
*
|
|
24
|
+
* THE GLOSS IS STRIPPED HERE OR NOWHERE (M2R-016). The auditor matches a
|
|
25
|
+
* declared path as a literal string or as a directory prefix and interprets
|
|
26
|
+
* nothing, so a plan entry written as
|
|
27
|
+
*
|
|
28
|
+
* `src/cli.ts` (edit only if step 4 requires it)
|
|
29
|
+
*
|
|
30
|
+
* must project to `src/cli.ts`. A projector that passed the glossed form
|
|
31
|
+
* through would emit a declaration under which every real change is
|
|
32
|
+
* undeclared, and the auditor would be right to reject it.
|
|
33
|
+
*/
|
|
34
|
+
/** The five properties the delivered phase-declaration schema requires. */
|
|
35
|
+
export interface PhaseDeclaration {
|
|
36
|
+
id: string;
|
|
37
|
+
branch: string;
|
|
38
|
+
filesToTouch: string[];
|
|
39
|
+
declaredExtras: string[];
|
|
40
|
+
citations: string[];
|
|
41
|
+
}
|
|
42
|
+
export declare function stripGloss(entry: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Is this a literal path the scope auditor can match?
|
|
45
|
+
*
|
|
46
|
+
* The auditor compares strings exactly or as a directory prefix and
|
|
47
|
+
* interprets nothing, so an entry still carrying whitespace after the gloss
|
|
48
|
+
* strip is prose. The projector REFUSES rather than emitting it: an emitted
|
|
49
|
+
* prose entry is a declaration nobody can satisfy, and the failure would
|
|
50
|
+
* surface as an unrelated scope-gate red on some later branch.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isLiteralPath(entry: string): boolean;
|
|
53
|
+
export interface ProjectionResult {
|
|
54
|
+
ok: true;
|
|
55
|
+
declaration: PhaseDeclaration;
|
|
56
|
+
/** `m3-p1.json`, the filename the auditor looks for. */
|
|
57
|
+
filename: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ProjectionFailure {
|
|
60
|
+
ok: false;
|
|
61
|
+
reason: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Project one phase of a decoded plan into its declaration.
|
|
65
|
+
*
|
|
66
|
+
* The filename is the phase id LOWERCASED, because CI derives `--phase` from
|
|
67
|
+
* the branch with a lowercase regex and the auditor looks the declaration up
|
|
68
|
+
* by that name. An uppercase filename is a declaration the auditor never
|
|
69
|
+
* finds, which reads exactly like a phase with no declaration at all.
|
|
70
|
+
*/
|
|
71
|
+
export declare function projectPhase(plan: unknown, phaseId: string): ProjectionResult | ProjectionFailure;
|
|
72
|
+
/** The declaration as the auditor reads it: JSON, one trailing newline. */
|
|
73
|
+
export declare function renderDeclaration(declaration: PhaseDeclaration): string;
|
package/dist/src/plan.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PHASE-DECLARATION PROJECTION (kernel plan M3, M3-P1 step 2; D-M3-18).
|
|
3
|
+
*
|
|
4
|
+
* The M2-P4 scope auditor reads one document per phase from
|
|
5
|
+
* `delivery/plan/phase-declarations/<phase-id-lowercased>.json`, out of the
|
|
6
|
+
* MERGE BASE of the audited branch. Until now that document was a SECOND
|
|
7
|
+
* HAND-AUTHORED SOURCE beside the plan, and two hand-authored sources of the
|
|
8
|
+
* same fact drift. This module makes it a GENERATED VIEW of the plan, so the
|
|
9
|
+
* plan is the one source and the auditor's input is derived from it.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THE PROJECTION EMITS, taken from the DELIVERED
|
|
12
|
+
* `src/gates/schemas/phase-declaration.schema.json` rather than from a
|
|
13
|
+
* description of it. The delivered schema sets `additionalProperties: false`,
|
|
14
|
+
* so an extra property is a REJECTION and not an ignored field, and the
|
|
15
|
+
* emitted key set is exactly these five, camelCase:
|
|
16
|
+
*
|
|
17
|
+
* id, branch, filesToTouch, declaredExtras, citations
|
|
18
|
+
*
|
|
19
|
+
* TWO VOCABULARIES MEET HERE AND NOWHERE ELSE. The plan spells the same
|
|
20
|
+
* fields `files-to-touch` and `extras`, because the plan is a document
|
|
21
|
+
* authored for people. This projector is the single translation point, and
|
|
22
|
+
* that is D-M3-18's whole value.
|
|
23
|
+
*
|
|
24
|
+
* THE GLOSS IS STRIPPED HERE OR NOWHERE (M2R-016). The auditor matches a
|
|
25
|
+
* declared path as a literal string or as a directory prefix and interprets
|
|
26
|
+
* nothing, so a plan entry written as
|
|
27
|
+
*
|
|
28
|
+
* `src/cli.ts` (edit only if step 4 requires it)
|
|
29
|
+
*
|
|
30
|
+
* must project to `src/cli.ts`. A projector that passed the glossed form
|
|
31
|
+
* through would emit a declaration under which every real change is
|
|
32
|
+
* undeclared, and the auditor would be right to reject it.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Reduce one plan `files-to-touch` entry to the bare path.
|
|
36
|
+
*
|
|
37
|
+
* THIS FUNCTION FEEDS THE GATE THAT EXISTS TO PREVENT SCOPE WIDENING, so an
|
|
38
|
+
* over-eager strip here is worse than no strip at all.
|
|
39
|
+
*
|
|
40
|
+
* MEASURED DEFECT, fix round 1 (B-001, high). The first version truncated at
|
|
41
|
+
* the FIRST `(` and produced:
|
|
42
|
+
*
|
|
43
|
+
* "src/app/(marketing)/page.tsx" -> "src/app/"
|
|
44
|
+
* "src/(lib)/util.ts" -> "src/"
|
|
45
|
+
*
|
|
46
|
+
* `src/gates/scope.ts` treats a trailing slash as a DIRECTORY PREFIX GRANT, so
|
|
47
|
+
* each of those turned one declared file into an entire tree. Parenthesised
|
|
48
|
+
* path segments are ordinary in real projects (Next.js route groups are the
|
|
49
|
+
* obvious case), so this is not a corner.
|
|
50
|
+
*
|
|
51
|
+
* THE RULE NOW, and each clause is load-bearing:
|
|
52
|
+
*
|
|
53
|
+
* 1. A gloss is stripped only when it is TRAILING and the whole of it is
|
|
54
|
+
* parenthesised: `<path> (anything)` with the closing paren at the end.
|
|
55
|
+
* 2. It is stripped only when WHITESPACE separates it from the path. A path
|
|
56
|
+
* whose own last segment is parenthesised, `src/app/(marketing)`, has no
|
|
57
|
+
* such whitespace and is left alone.
|
|
58
|
+
* 3. What precedes the gloss must be ONE non-whitespace token, which is what
|
|
59
|
+
* a literal path is. That is the "plausible path" test, and it is why an
|
|
60
|
+
* interior `(` can never trigger a strip: the interior case never matches
|
|
61
|
+
* the trailing form at all.
|
|
62
|
+
*
|
|
63
|
+
* Anything the rule does not recognise is returned UNCHANGED and rejected
|
|
64
|
+
* loudly by `projectPhase`, because a prose entry that reaches the auditor as
|
|
65
|
+
* a literal string fails visibly, while a silently truncated one grants a
|
|
66
|
+
* tree.
|
|
67
|
+
*/
|
|
68
|
+
const TRAILING_GLOSS = /^(\S+)\s+\([\s\S]*\)$/;
|
|
69
|
+
export function stripGloss(entry) {
|
|
70
|
+
let text = entry.trim();
|
|
71
|
+
const trailing = TRAILING_GLOSS.exec(text);
|
|
72
|
+
if (trailing !== null) {
|
|
73
|
+
text = trailing[1].trim();
|
|
74
|
+
}
|
|
75
|
+
/* Backticks are markdown, never part of a path. Stripped AFTER the gloss,
|
|
76
|
+
because the plan writes `` `path` (gloss) `` and the backticks close
|
|
77
|
+
before the gloss opens. */
|
|
78
|
+
text = text.replace(/^`+/, "").replace(/`+$/, "").trim();
|
|
79
|
+
/* A trailing comma survives a list written inline. */
|
|
80
|
+
text = text.replace(/,+$/, "").trim();
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Is this a literal path the scope auditor can match?
|
|
85
|
+
*
|
|
86
|
+
* The auditor compares strings exactly or as a directory prefix and
|
|
87
|
+
* interprets nothing, so an entry still carrying whitespace after the gloss
|
|
88
|
+
* strip is prose. The projector REFUSES rather than emitting it: an emitted
|
|
89
|
+
* prose entry is a declaration nobody can satisfy, and the failure would
|
|
90
|
+
* surface as an unrelated scope-gate red on some later branch.
|
|
91
|
+
*/
|
|
92
|
+
export function isLiteralPath(entry) {
|
|
93
|
+
return entry !== "" && !/\s/.test(entry);
|
|
94
|
+
}
|
|
95
|
+
function asRecord(value) {
|
|
96
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
97
|
+
? value
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
function stringArray(value) {
|
|
101
|
+
return Array.isArray(value)
|
|
102
|
+
? value.filter((item) => typeof item === "string")
|
|
103
|
+
: [];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Project one phase of a decoded plan into its declaration.
|
|
107
|
+
*
|
|
108
|
+
* The filename is the phase id LOWERCASED, because CI derives `--phase` from
|
|
109
|
+
* the branch with a lowercase regex and the auditor looks the declaration up
|
|
110
|
+
* by that name. An uppercase filename is a declaration the auditor never
|
|
111
|
+
* finds, which reads exactly like a phase with no declaration at all.
|
|
112
|
+
*/
|
|
113
|
+
export function projectPhase(plan, phaseId) {
|
|
114
|
+
const document = asRecord(plan);
|
|
115
|
+
if (document === undefined) {
|
|
116
|
+
return { ok: false, reason: "the plan document is not a mapping" };
|
|
117
|
+
}
|
|
118
|
+
const phases = Array.isArray(document["phases"]) ? document["phases"] : [];
|
|
119
|
+
const phase = asRecord(phases.find((candidate) => asRecord(candidate)?.["id"] === phaseId));
|
|
120
|
+
if (phase === undefined) {
|
|
121
|
+
return { ok: false, reason: `the plan contains no phase ${phaseId}` };
|
|
122
|
+
}
|
|
123
|
+
const filesToTouch = stringArray(phase["files-to-touch"])
|
|
124
|
+
.map(stripGloss)
|
|
125
|
+
.filter((path) => path !== "");
|
|
126
|
+
const declaredExtras = stringArray(phase["extras"])
|
|
127
|
+
.map(stripGloss)
|
|
128
|
+
.filter((path) => path !== "");
|
|
129
|
+
const prose = [...filesToTouch, ...declaredExtras].filter((path) => !isLiteralPath(path));
|
|
130
|
+
if (prose.length > 0) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: `phase ${phaseId} declares an entry that is not a literal path and ` +
|
|
134
|
+
`cannot be projected: ${prose.map((p) => JSON.stringify(p)).join(", ")}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const declaration = {
|
|
138
|
+
id: String(phase["id"]),
|
|
139
|
+
branch: String(phase["branch"]),
|
|
140
|
+
filesToTouch,
|
|
141
|
+
declaredExtras,
|
|
142
|
+
citations: stringArray(phase["citations"]),
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
declaration,
|
|
147
|
+
filename: `${phaseId.toLowerCase()}.json`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** The declaration as the auditor reads it: JSON, one trailing newline. */
|
|
151
|
+
export function renderDeclaration(declaration) {
|
|
152
|
+
return `${JSON.stringify(declaration, undefined, 2)}\n`;
|
|
153
|
+
}
|