@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,340 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3
|
+
import { EX_USAGE } from "../cli.js";
|
|
4
|
+
import { loadFleet, refuseOpenPathForWrite } from "../fleet.js";
|
|
5
|
+
import { acquireLease, leaseStatus, observeLease, releaseLease, renewLease, } from "../lock.js";
|
|
6
|
+
/**
|
|
7
|
+
* tiphys lock <acquire [--take-over] [--duration <seconds>] | renew
|
|
8
|
+
* --holder <id> [--duration <seconds>] | release --holder <id> | status>
|
|
9
|
+
* (kernel plan v1, M1-P3 step 2). Runs in a fleet home (cwd). The holder
|
|
10
|
+
* identity for renew and release is the opaque holderId that acquire
|
|
11
|
+
* printed; it outlives any one command invocation (FM-022) and is never
|
|
12
|
+
* derived from the running program (plan constraint C-2).
|
|
13
|
+
*/
|
|
14
|
+
const USAGE = "usage: tiphys lock <acquire [--take-over] [--duration <seconds>] | " +
|
|
15
|
+
"renew --holder <id> [--duration <seconds>] | " +
|
|
16
|
+
"release --holder <id> | status>";
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic race-witness hold point (the plan's test determinism
|
|
19
|
+
* rule: scripted interleaves over sleep-based timing). When
|
|
20
|
+
* TIPHYS_LOCK_TEST_HOLD names a barrier path, the mutating subcommands
|
|
21
|
+
* observe the lease and freeze their decision clock first, write
|
|
22
|
+
* <barrier>.observed as a ready marker, then wait for the barrier file
|
|
23
|
+
* to appear before deciding and applying through the one mutation
|
|
24
|
+
* primitive. A test can thereby interleave two real CLI invocations at
|
|
25
|
+
* the exact compare-and-swap point. Inert unless the variable is set.
|
|
26
|
+
*
|
|
27
|
+
* The seam is LOUD about not holding (D-3). It previously had two exits
|
|
28
|
+
* from its wait and only one of them meant "held": on the other the
|
|
29
|
+
* command walked into the mutation carrying a stale observation and
|
|
30
|
+
* told nobody, so a witness could score a compare-and-swap it never
|
|
31
|
+
* staged and still pass. Both exits are now closed with a throw, the
|
|
32
|
+
* bound is monotonic (process.hrtime.bigint, not Date.now, because a
|
|
33
|
+
* realtime bound mixed with monotonic test durations is precisely what
|
|
34
|
+
* made "it failed fast" look like proof the bound was not reached), and
|
|
35
|
+
* the reason the wait ended is written to <barrier>.released so the
|
|
36
|
+
* test can assert the interleave actually happened. This is a test
|
|
37
|
+
* integrity fix; it is NOT a fix for the unattributed U-2 flake, whose
|
|
38
|
+
* trigger remains unexplained.
|
|
39
|
+
*/
|
|
40
|
+
const HOLD_WAIT_LIMIT_MS = 30_000;
|
|
41
|
+
async function maybeHoldForTest(lockPath) {
|
|
42
|
+
const barrier = process.env.TIPHYS_LOCK_TEST_HOLD;
|
|
43
|
+
if (barrier === undefined || barrier === "") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (existsSync(barrier)) {
|
|
47
|
+
throw new Error(`lock test hold point: barrier ${barrier} already existed before the ` +
|
|
48
|
+
`hold; this interleave was never staged and the run is not evidence`);
|
|
49
|
+
}
|
|
50
|
+
const observed = observeLease(lockPath);
|
|
51
|
+
const nowMs = Date.now();
|
|
52
|
+
/* THE MARKER PATHS ARE DERIVED FROM A CALLER-SUPPLIED PATH, so their entry
|
|
53
|
+
type is established before either is opened for writing. A named pipe at
|
|
54
|
+
`<barrier>.observed` blocked this seam forever with zero output, which is
|
|
55
|
+
the same class this round closed in the lease and brief readers; the seam
|
|
56
|
+
is inert unless the variable is set, which changes how it is REACHED and
|
|
57
|
+
not what it does once reached. */
|
|
58
|
+
const observedRefusal = refuseOpenPathForWrite(`${barrier}.observed`);
|
|
59
|
+
if (observedRefusal !== undefined) {
|
|
60
|
+
throw new Error(`lock test hold point: ${observedRefusal}`);
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(`${barrier}.observed`, "");
|
|
63
|
+
const startNs = process.hrtime.bigint();
|
|
64
|
+
const limitNs = BigInt(HOLD_WAIT_LIMIT_MS) * 1000000n;
|
|
65
|
+
let held = false;
|
|
66
|
+
for (;;) {
|
|
67
|
+
if (existsSync(barrier)) {
|
|
68
|
+
held = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (process.hrtime.bigint() - startNs >= limitNs) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
await sleep(10);
|
|
75
|
+
}
|
|
76
|
+
const waitedMs = Number((process.hrtime.bigint() - startNs) / 1000000n);
|
|
77
|
+
if (!held) {
|
|
78
|
+
throw new Error(`lock test hold point: barrier ${barrier} never appeared within ` +
|
|
79
|
+
`${String(HOLD_WAIT_LIMIT_MS)}ms (waited ${String(waitedMs)}ms on the ` +
|
|
80
|
+
`monotonic clock); this interleave was never staged and the run is ` +
|
|
81
|
+
`not evidence`);
|
|
82
|
+
}
|
|
83
|
+
// Record that the hold really held, and why the wait ended, so the
|
|
84
|
+
// witness can assert the interleave rather than assume it.
|
|
85
|
+
/* Re-established immediately before this second write rather than once
|
|
86
|
+
per call: the two writes are separated by a wait of up to thirty seconds
|
|
87
|
+
and the path is derived from one the CALLER supplied. */
|
|
88
|
+
const releasedRefusal = refuseOpenPathForWrite(`${barrier}.released`);
|
|
89
|
+
if (releasedRefusal !== undefined) {
|
|
90
|
+
throw new Error(`lock test hold point: ${releasedRefusal}`);
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(`${barrier}.released`, `held after ${String(waitedMs)}ms (monotonic), barrier observed\n`);
|
|
93
|
+
return { observed, nowMs };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* THE DECISION-CLOCK SEAM (M4-P21 criteria 4 and 5). When
|
|
97
|
+
* TIPHYS_LOCK_TEST_NOW_MS carries a finite number of milliseconds, every
|
|
98
|
+
* mutating subcommand decides against that instant instead of `Date.now()`,
|
|
99
|
+
* by feeding the EXISTING `nowMs` option the lease library already takes
|
|
100
|
+
* (src/lock.ts:568). It exists because the two clock-skew witnesses must
|
|
101
|
+
* move ONE environment's clock ten minutes without touching a system clock,
|
|
102
|
+
* which the plan requires by name, and because criterion 6 is about what the
|
|
103
|
+
* COMMAND prints, so the skew has to reach the command rather than only the
|
|
104
|
+
* library. Inert unless the variable is set, exactly like the hold point
|
|
105
|
+
* above, and a value that does not parse is a loud refusal rather than a
|
|
106
|
+
* silent fall back to the real clock: a seam that quietly ignores its input
|
|
107
|
+
* would make a skew witness green while measuring no skew at all.
|
|
108
|
+
*
|
|
109
|
+
* AND IT IS GATED, BECAUSE IT SHIPS. `judgeByCounter` (src/exclusion.ts:553)
|
|
110
|
+
* computes `unchangedForMs = nowMs - previous.firstSeenMs`, so the module's
|
|
111
|
+
* promised "two readings of ONE clock" become one reading of a clock the
|
|
112
|
+
* CALLER supplies. Measured in the published CLI before this gate existed:
|
|
113
|
+
* the honest challenger was refused with "fencing counter 1 has stood still
|
|
114
|
+
* for 0ms of the 900000ms this environment requires", and the SAME command
|
|
115
|
+
* with this one variable set an hour ahead took a live lease over instantly
|
|
116
|
+
* and still printed `signal=counter`. A captured witness of that run was
|
|
117
|
+
* indistinguishable from an honest one.
|
|
118
|
+
*
|
|
119
|
+
* TWO CHANGES, and they are different in kind. The ALLOWANCE
|
|
120
|
+
* (TIPHYS_ALLOW_TEST_CLOCK=1) makes reaching the seam a declared act rather
|
|
121
|
+
* than a side effect of one environment variable. The LABEL makes the run
|
|
122
|
+
* SAY SO: `injectedClockSuffix` below is appended to every verdict line the
|
|
123
|
+
* shared layer prints, so the output of a measurement can never be quoted as
|
|
124
|
+
* the output of a real judgement. An allowance alone would leave the second
|
|
125
|
+
* hole open, which is why both are here.
|
|
126
|
+
*/
|
|
127
|
+
export const TEST_CLOCK_ALLOWANCE = "TIPHYS_ALLOW_TEST_CLOCK";
|
|
128
|
+
function testClockMs() {
|
|
129
|
+
const raw = process.env.TIPHYS_LOCK_TEST_NOW_MS;
|
|
130
|
+
if (raw === undefined || raw === "") {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (process.env[TEST_CLOCK_ALLOWANCE] !== "1") {
|
|
134
|
+
throw new Error(`lock test clock: TIPHYS_LOCK_TEST_NOW_MS is set and ` +
|
|
135
|
+
`${TEST_CLOCK_ALLOWANCE}=1 is not, so this run would have decided ` +
|
|
136
|
+
`cross-environment exclusion against a clock its caller supplied ` +
|
|
137
|
+
`while printing the same verdict an honest run prints; set ` +
|
|
138
|
+
`${TEST_CLOCK_ALLOWANCE}=1 to declare that this run is a measurement, ` +
|
|
139
|
+
`or unset TIPHYS_LOCK_TEST_NOW_MS`);
|
|
140
|
+
}
|
|
141
|
+
const parsed = Number(raw);
|
|
142
|
+
if (!Number.isFinite(parsed)) {
|
|
143
|
+
throw new Error(`lock test clock: TIPHYS_LOCK_TEST_NOW_MS=${raw} does not parse as a ` +
|
|
144
|
+
`number of milliseconds; this run would have measured the real clock ` +
|
|
145
|
+
`and is not evidence`);
|
|
146
|
+
}
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* What a verdict line gains when the decision clock was injected. Empty on
|
|
151
|
+
* every honest run, so no shipped output changes for anyone who does not set
|
|
152
|
+
* the variable.
|
|
153
|
+
*/
|
|
154
|
+
function injectedClockSuffix() {
|
|
155
|
+
return process.env.TIPHYS_LOCK_TEST_NOW_MS === undefined ||
|
|
156
|
+
process.env.TIPHYS_LOCK_TEST_NOW_MS === ""
|
|
157
|
+
? ""
|
|
158
|
+
: "(injected-clock)";
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Emit the shared exclusion layer's verdict line. One line, and it always
|
|
162
|
+
* names the SIGNAL that reached the verdict (criterion 6): `signal=counter`
|
|
163
|
+
* where the register was reachable and its fencing counter decided, or
|
|
164
|
+
* `signal=clock` where it was not and the command therefore refused rather
|
|
165
|
+
* than deciding cross-environment exclusion on a local clock.
|
|
166
|
+
*/
|
|
167
|
+
function reportShared(outcome) {
|
|
168
|
+
if (outcome.shared !== undefined) {
|
|
169
|
+
process.stdout.write(`${labelClock(outcome.shared.line)}\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Mark every `signal=<basis>` token in a verdict line when this run's
|
|
174
|
+
* decision clock was injected. The token is what criterion 6 asks the
|
|
175
|
+
* command to print, so it is the token that has to carry the caveat.
|
|
176
|
+
*/
|
|
177
|
+
function labelClock(line) {
|
|
178
|
+
const suffix = injectedClockSuffix();
|
|
179
|
+
return suffix === "" ? line : line.replace(/signal=(counter|clock)/g, `signal=$1${suffix}`);
|
|
180
|
+
}
|
|
181
|
+
function usageError(message) {
|
|
182
|
+
if (message !== undefined) {
|
|
183
|
+
process.stderr.write(`tiphys lock: ${message}\n`);
|
|
184
|
+
}
|
|
185
|
+
process.stderr.write(`${USAGE}\n`);
|
|
186
|
+
return EX_USAGE;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The one failure emitter for acquire, renew, and release (CR-204).
|
|
190
|
+
* When the library classified the failure as a stale mutation claim,
|
|
191
|
+
* the operator is pointed at the claim file rather than at a holder
|
|
192
|
+
* that may not exist; the remedy text lives here, so a CLI that
|
|
193
|
+
* ignored the classification would visibly lose it. Operational
|
|
194
|
+
* failures exit 1 (usage errors alone use EX_USAGE).
|
|
195
|
+
*/
|
|
196
|
+
function failure(outcome) {
|
|
197
|
+
// D-2: the claim file is the sole serializer of lock mutations, so
|
|
198
|
+
// deleting one that is actually live lets a second mutation enter the
|
|
199
|
+
// critical section and can produce two live holders. The remedy text
|
|
200
|
+
// therefore states that cost instead of inviting the deletion, and it
|
|
201
|
+
// names the safe order: make sure no tiphys process is running first.
|
|
202
|
+
const remedy = outcome.claimTimeout === true
|
|
203
|
+
? "; a crashed mutation can leave this file behind, but deleting it while a mutation is genuinely in flight can produce two lock holders, so confirm no tiphys process is running against this fleet before removing it"
|
|
204
|
+
: "";
|
|
205
|
+
process.stderr.write(`tiphys lock: ${labelClock(outcome.reason)}${remedy}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
function parseFlags(rest) {
|
|
209
|
+
const parsed = {
|
|
210
|
+
takeover: false,
|
|
211
|
+
holder: undefined,
|
|
212
|
+
durationSeconds: undefined,
|
|
213
|
+
};
|
|
214
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
215
|
+
const flag = rest[i];
|
|
216
|
+
if (flag === "--take-over") {
|
|
217
|
+
parsed.takeover = true;
|
|
218
|
+
}
|
|
219
|
+
else if (flag === "--holder" && i + 1 < rest.length) {
|
|
220
|
+
parsed.holder = rest[i + 1];
|
|
221
|
+
i += 1;
|
|
222
|
+
}
|
|
223
|
+
else if (flag === "--duration" && i + 1 < rest.length) {
|
|
224
|
+
const seconds = Number(rest[i + 1]);
|
|
225
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
parsed.durationSeconds = seconds;
|
|
229
|
+
i += 1;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return parsed;
|
|
236
|
+
}
|
|
237
|
+
export async function cmdLock(args) {
|
|
238
|
+
const [subcommand, ...rest] = args;
|
|
239
|
+
if (subcommand === undefined) {
|
|
240
|
+
return usageError();
|
|
241
|
+
}
|
|
242
|
+
const flags = parseFlags(rest);
|
|
243
|
+
if (flags === undefined) {
|
|
244
|
+
return usageError();
|
|
245
|
+
}
|
|
246
|
+
let lockPath;
|
|
247
|
+
try {
|
|
248
|
+
lockPath = loadFleet(process.cwd()).lockPath;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
process.stderr.write(`tiphys lock: ${error.message}\n`);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
switch (subcommand) {
|
|
255
|
+
case "acquire": {
|
|
256
|
+
if (flags.holder !== undefined) {
|
|
257
|
+
return usageError("acquire does not take --holder");
|
|
258
|
+
}
|
|
259
|
+
const held = await maybeHoldForTest(lockPath);
|
|
260
|
+
const outcome = await acquireLease(lockPath, {
|
|
261
|
+
takeover: flags.takeover,
|
|
262
|
+
durationSeconds: flags.durationSeconds,
|
|
263
|
+
observed: held?.observed,
|
|
264
|
+
nowMs: testClockMs() ?? held?.nowMs,
|
|
265
|
+
});
|
|
266
|
+
if (!outcome.ok) {
|
|
267
|
+
return failure(outcome);
|
|
268
|
+
}
|
|
269
|
+
const lease = outcome.lease;
|
|
270
|
+
if (lease === null) {
|
|
271
|
+
throw new Error("unreachable: acquire produced no lease");
|
|
272
|
+
}
|
|
273
|
+
process.stdout.write(`acquired ${lease.holderId} expires ${lease.expiresAt}\n`);
|
|
274
|
+
reportShared(outcome);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
case "renew": {
|
|
278
|
+
if (flags.holder === undefined || flags.takeover) {
|
|
279
|
+
return usageError("renew requires --holder <id>");
|
|
280
|
+
}
|
|
281
|
+
const held = await maybeHoldForTest(lockPath);
|
|
282
|
+
const outcome = await renewLease(lockPath, flags.holder, {
|
|
283
|
+
durationSeconds: flags.durationSeconds,
|
|
284
|
+
observed: held?.observed,
|
|
285
|
+
nowMs: testClockMs() ?? held?.nowMs,
|
|
286
|
+
});
|
|
287
|
+
if (!outcome.ok) {
|
|
288
|
+
return failure(outcome);
|
|
289
|
+
}
|
|
290
|
+
const lease = outcome.lease;
|
|
291
|
+
if (lease === null) {
|
|
292
|
+
throw new Error("unreachable: renew produced no lease");
|
|
293
|
+
}
|
|
294
|
+
process.stdout.write(`renewed ${lease.holderId} expires ${lease.expiresAt}\n`);
|
|
295
|
+
reportShared(outcome);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
case "release": {
|
|
299
|
+
if (flags.holder === undefined || flags.takeover || flags.durationSeconds !== undefined) {
|
|
300
|
+
return usageError("release requires --holder <id> and no other flags");
|
|
301
|
+
}
|
|
302
|
+
const held = await maybeHoldForTest(lockPath);
|
|
303
|
+
const releaseOptions = {
|
|
304
|
+
observed: held?.observed,
|
|
305
|
+
};
|
|
306
|
+
const releaseClock = testClockMs();
|
|
307
|
+
if (releaseClock !== undefined) {
|
|
308
|
+
releaseOptions.nowMs = releaseClock;
|
|
309
|
+
}
|
|
310
|
+
const outcome = await releaseLease(lockPath, flags.holder, releaseOptions);
|
|
311
|
+
if (!outcome.ok) {
|
|
312
|
+
return failure(outcome);
|
|
313
|
+
}
|
|
314
|
+
process.stdout.write(`released ${flags.holder}\n`);
|
|
315
|
+
reportShared(outcome);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
case "status": {
|
|
319
|
+
if (flags.holder !== undefined || flags.takeover || flags.durationSeconds !== undefined) {
|
|
320
|
+
return usageError("status takes no flags");
|
|
321
|
+
}
|
|
322
|
+
const status = leaseStatus(lockPath);
|
|
323
|
+
if (status.state === "free") {
|
|
324
|
+
process.stdout.write("free\n");
|
|
325
|
+
}
|
|
326
|
+
else if (status.state === "corrupt") {
|
|
327
|
+
process.stdout.write(`corrupt ${status.detail}\n`);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
process.stdout.write(`${status.state} holder ${status.lease.holderId} acquired ` +
|
|
331
|
+
`${status.lease.acquiredAt} expires ${status.lease.expiresAt}\n`);
|
|
332
|
+
}
|
|
333
|
+
// status always exits 0 so a human takeover decision is informed
|
|
334
|
+
// by reading, never blocked by an exit code.
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
default:
|
|
338
|
+
return usageError(`unknown lock subcommand "${subcommand}"`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys mode show --mode <id> [--file <path>]` (kernel plan M3, M3-P3
|
|
3
|
+
* step 5; fix round 1 finding B-002).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS COMMAND EXISTS: a brief or a human has to be able to read what a
|
|
6
|
+
* declared mode requires without parsing YAML by hand. That is the whole
|
|
7
|
+
* scope. It shows; it does not resolve a project into a mode, does not
|
|
8
|
+
* enforce one, and does not run one.
|
|
9
|
+
*
|
|
10
|
+
* IT VALIDATES BEFORE IT SERVES, and that is the point of the fix round.
|
|
11
|
+
* As first shipped this command read the document and printed it, invoking
|
|
12
|
+
* neither the schema nor any registered check, so every guard the phase built
|
|
13
|
+
* was bypassed on the one path built for humans and briefs. The mechanism is
|
|
14
|
+
* A READER THAT DOES NOT VALIDATE WHAT IT READS, and the fix is not "detect
|
|
15
|
+
* one more bad state": it is that this command now runs exactly what
|
|
16
|
+
* `tiphys validate --type assurance-modes --context <dir>` runs, and REFUSES
|
|
17
|
+
* rather than printing when the document fails. A partial answer from an
|
|
18
|
+
* invalid document is worse than an error, because the reader cannot tell.
|
|
19
|
+
*
|
|
20
|
+
* THE CONTEXT IS THE DOCUMENT'S OWN DIRECTORY. The cross-document checks
|
|
21
|
+
* resolve `gate-registry.yaml` and `schemas/charter.schema.json` beside the
|
|
22
|
+
* document, which is where the npm package puts them and where this repository
|
|
23
|
+
* keeps them. That makes the two `requiresContext` checks run here rather than
|
|
24
|
+
* SKIP, which matters: a skip fails the run, and a command that always failed
|
|
25
|
+
* would be a command nobody uses.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* 0 the document is valid and the mode was found and printed
|
|
29
|
+
* 1 the document could not be read, is invalid, or no mode carries that id
|
|
30
|
+
* 64 usage error (BSD sysexits EX_USAGE)
|
|
31
|
+
*
|
|
32
|
+
* A MODE ID THAT IS NOT DECLARED IS 1, NOT 64, and the difference is not
|
|
33
|
+
* pedantry. 64 means the caller used the command wrongly; 1 means the command
|
|
34
|
+
* worked and the answer is no. `--mode direct-pr` against a document that
|
|
35
|
+
* declares only `full` is a well-formed question with a negative answer, and a
|
|
36
|
+
* caller that cannot tell those apart cannot script this.
|
|
37
|
+
*/
|
|
38
|
+
/** Exit code for usage errors, per BSD sysexits EX_USAGE. */
|
|
39
|
+
export declare const EX_USAGE = 64;
|
|
40
|
+
export declare function cmdMode(argv: string[]): number;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys mode show --mode <id> [--file <path>]` (kernel plan M3, M3-P3
|
|
3
|
+
* step 5; fix round 1 finding B-002).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS COMMAND EXISTS: a brief or a human has to be able to read what a
|
|
6
|
+
* declared mode requires without parsing YAML by hand. That is the whole
|
|
7
|
+
* scope. It shows; it does not resolve a project into a mode, does not
|
|
8
|
+
* enforce one, and does not run one.
|
|
9
|
+
*
|
|
10
|
+
* IT VALIDATES BEFORE IT SERVES, and that is the point of the fix round.
|
|
11
|
+
* As first shipped this command read the document and printed it, invoking
|
|
12
|
+
* neither the schema nor any registered check, so every guard the phase built
|
|
13
|
+
* was bypassed on the one path built for humans and briefs. The mechanism is
|
|
14
|
+
* A READER THAT DOES NOT VALIDATE WHAT IT READS, and the fix is not "detect
|
|
15
|
+
* one more bad state": it is that this command now runs exactly what
|
|
16
|
+
* `tiphys validate --type assurance-modes --context <dir>` runs, and REFUSES
|
|
17
|
+
* rather than printing when the document fails. A partial answer from an
|
|
18
|
+
* invalid document is worse than an error, because the reader cannot tell.
|
|
19
|
+
*
|
|
20
|
+
* THE CONTEXT IS THE DOCUMENT'S OWN DIRECTORY. The cross-document checks
|
|
21
|
+
* resolve `gate-registry.yaml` and `schemas/charter.schema.json` beside the
|
|
22
|
+
* document, which is where the npm package puts them and where this repository
|
|
23
|
+
* keeps them. That makes the two `requiresContext` checks run here rather than
|
|
24
|
+
* SKIP, which matters: a skip fails the run, and a command that always failed
|
|
25
|
+
* would be a command nobody uses.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* 0 the document is valid and the mode was found and printed
|
|
29
|
+
* 1 the document could not be read, is invalid, or no mode carries that id
|
|
30
|
+
* 64 usage error (BSD sysexits EX_USAGE)
|
|
31
|
+
*
|
|
32
|
+
* A MODE ID THAT IS NOT DECLARED IS 1, NOT 64, and the difference is not
|
|
33
|
+
* pedantry. 64 means the caller used the command wrongly; 1 means the command
|
|
34
|
+
* worked and the answer is no. `--mode direct-pr` against a document that
|
|
35
|
+
* declares only `full` is a well-formed question with a negative answer, and a
|
|
36
|
+
* caller that cannot tell those apart cannot script this.
|
|
37
|
+
*/
|
|
38
|
+
import { dirname } from "node:path";
|
|
39
|
+
import { runChecks } from "../checks.js";
|
|
40
|
+
import { formatDiagnostics, validateInstance } from "../validate.js";
|
|
41
|
+
import { loadTypeSchema } from "./validate.js";
|
|
42
|
+
import { readModes, renderMode } from "../modes.js";
|
|
43
|
+
/** The artifact type this command reads, and the type its checks are registered for. */
|
|
44
|
+
const MODES_TYPE = "assurance-modes";
|
|
45
|
+
/** Exit code for usage errors, per BSD sysexits EX_USAGE. */
|
|
46
|
+
export const EX_USAGE = 64;
|
|
47
|
+
function usage() {
|
|
48
|
+
return "usage: tiphys mode show --mode <id> [--file <path>]";
|
|
49
|
+
}
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const options = {};
|
|
52
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
53
|
+
const argument = argv[index];
|
|
54
|
+
if (argument === "--mode" || argument === "--file") {
|
|
55
|
+
const value = argv[index + 1];
|
|
56
|
+
if (value === undefined || value.startsWith("--")) {
|
|
57
|
+
return { usageError: `${argument} requires a value` };
|
|
58
|
+
}
|
|
59
|
+
if (argument === "--mode") {
|
|
60
|
+
options.mode = value;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
options.file = value;
|
|
64
|
+
}
|
|
65
|
+
index += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return { usageError: `unknown argument ${argument}` };
|
|
69
|
+
}
|
|
70
|
+
if (options.mode === undefined) {
|
|
71
|
+
return { usageError: "--mode is required" };
|
|
72
|
+
}
|
|
73
|
+
return { options };
|
|
74
|
+
}
|
|
75
|
+
function fail(reason, code) {
|
|
76
|
+
process.stderr.write(`tiphys mode: ${reason}\n`);
|
|
77
|
+
if (code === EX_USAGE) {
|
|
78
|
+
process.stderr.write(`${usage()}\n`);
|
|
79
|
+
}
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
export function cmdMode(argv) {
|
|
83
|
+
const [subcommand, ...rest] = argv;
|
|
84
|
+
if (subcommand !== "show") {
|
|
85
|
+
return fail(subcommand === undefined
|
|
86
|
+
? "a subcommand is required"
|
|
87
|
+
: `unknown subcommand ${subcommand}`, EX_USAGE);
|
|
88
|
+
}
|
|
89
|
+
const parsed = parseArgs(rest);
|
|
90
|
+
if (parsed.options === undefined) {
|
|
91
|
+
return fail(parsed.usageError ?? "usage error", EX_USAGE);
|
|
92
|
+
}
|
|
93
|
+
let read;
|
|
94
|
+
try {
|
|
95
|
+
read = parsed.options.file === undefined ? readModes() : readModes(parsed.options.file);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
/* `packageRoot` throws when the shipped document is not above this module,
|
|
99
|
+
which is an incomplete installation rather than a caller error. One line,
|
|
100
|
+
never a stack trace, the same policy `tiphys validate` applies to its own
|
|
101
|
+
operator-supplied input. */
|
|
102
|
+
return fail(String(error instanceof Error ? error.message : error), 1);
|
|
103
|
+
}
|
|
104
|
+
if (!read.ok) {
|
|
105
|
+
return fail(read.reason, 1);
|
|
106
|
+
}
|
|
107
|
+
/* VALIDATE, THEN SERVE. Schema first and derived checks second, in the same
|
|
108
|
+
order and through the same functions `tiphys validate` uses, so the two
|
|
109
|
+
commands cannot drift into disagreeing about what a valid document is. */
|
|
110
|
+
const lines = formatDiagnostics(validateInstance(loadTypeSchema(MODES_TYPE), read.raw));
|
|
111
|
+
const checks = runChecks(MODES_TYPE, read.raw, dirname(read.path));
|
|
112
|
+
if (lines.length > 0 || checks.failed) {
|
|
113
|
+
process.stderr.write(`tiphys mode: ${read.path} is not a valid ${MODES_TYPE} document, so it is not served\n`);
|
|
114
|
+
for (const line of [...lines, ...checks.lines]) {
|
|
115
|
+
process.stderr.write(`${line}\n`);
|
|
116
|
+
}
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
const wanted = parsed.options.mode;
|
|
120
|
+
const mode = read.modes.find((candidate) => candidate.id === wanted);
|
|
121
|
+
if (mode === undefined) {
|
|
122
|
+
const declared = read.modes.map((candidate) => candidate.id).sort();
|
|
123
|
+
return fail(`${read.path} declares no mode ${wanted}; it declares ${declared.length === 0 ? "none" : declared.join(", ")}`, 1);
|
|
124
|
+
}
|
|
125
|
+
/* WHICH DOCUMENT THIS IS, derived from the invocation rather than from the
|
|
126
|
+
path: no `--file` means `readModes()` walked to the package root and read
|
|
127
|
+
the kernel's own document. CR-004 item 2 turns on that distinction, because
|
|
128
|
+
"no phase has been delivered under this mode" is a claim about the kernel's
|
|
129
|
+
own delivery and is not knowable for a document a consumer supplied. */
|
|
130
|
+
for (const line of renderMode(mode, { shippedDocument: parsed.options.file === undefined })) {
|
|
131
|
+
process.stdout.write(`${line}\n`);
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Fleet } from "../fleet.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Work remains. DISTINCT FROM 1, which is the command failing, and from 0,
|
|
4
|
+
* which is every in-flight category empty (criterion 1).
|
|
5
|
+
*/
|
|
6
|
+
export declare const EXIT_WORK_REMAINS = 3;
|
|
7
|
+
/**
|
|
8
|
+
* WHAT THIS COMMAND CANNOT SEE, as a NAMED LIST (criterion 4).
|
|
9
|
+
*
|
|
10
|
+
* Printed unconditionally. It is not derived from a probe and it does not
|
|
11
|
+
* shorten when something is unreachable, because a list that shrinks when the
|
|
12
|
+
* network is gone tells the reader that fewer things are unknown at exactly
|
|
13
|
+
* the moment more of them are.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CANNOT_SEE: readonly string[];
|
|
16
|
+
/** The heading the cannot-see block is printed under. */
|
|
17
|
+
export declare const CANNOT_SEE_HEADING = "cannot see (this command has no network; an absence here is not evidence of absence):";
|
|
18
|
+
/** How a branch turned out to have been delivered, or why that is not settled. */
|
|
19
|
+
export type Delivery = {
|
|
20
|
+
kind: "delivered";
|
|
21
|
+
how: "ancestor" | "squash" | "patch-equivalent";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "open";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "unknown";
|
|
26
|
+
detail: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* THE DELIVERED-ELSEWHERE PREDICATE (criterion 3).
|
|
30
|
+
*
|
|
31
|
+
* `git branch --merged` reports a squash-merged branch as UNMERGED, because
|
|
32
|
+
* the squash commit's sha is not the branch tip and the branch tip is not an
|
|
33
|
+
* ancestor of the base. This process squash-merges every phase, so under the
|
|
34
|
+
* naive implementation EVERY delivered branch reads as open forever and the
|
|
35
|
+
* stop condition never goes green.
|
|
36
|
+
*
|
|
37
|
+
* THE CITATION THAT STOOD HERE UNTIL ROUND 1 WAS WRONG, AND IT WAS WRONG IN
|
|
38
|
+
* THE SILENT DIRECTION. It read `delivery/STATE.md:54`, which resolves, which
|
|
39
|
+
* is why no gate reddened; the line it resolves to is about two merge blockers
|
|
40
|
+
* and says nothing about `git branch --merged`. Measured:
|
|
41
|
+
* `grep -n 'branch --merged' delivery/STATE.md` returns nothing, at this head
|
|
42
|
+
* and at the merge base, so the sentence is absent rather than moved. The
|
|
43
|
+
* claim itself is measured in this repository and the measurement is the
|
|
44
|
+
* citation now: witness/captures/next-delivered-elsewhere-git.txt records
|
|
45
|
+
* `git branch --merged main` listing neither the squash-merged branch nor the
|
|
46
|
+
* one delivered through another pull request. The same wrong citation is in
|
|
47
|
+
* the plan at delivery/plan/kernel-plan-m4.md:3239 and in this phase's work
|
|
48
|
+
* history; the work history is corrected, and the plan is an owner-approved
|
|
49
|
+
* document this phase does not edit, so it is raised rather than changed.
|
|
50
|
+
*
|
|
51
|
+
* THREE ARMS, AND THE THIRD IS WHY THIS IS NOT JUST A CALL TO `landedness`.
|
|
52
|
+
*
|
|
53
|
+
* 1. ANCESTOR. An ordinary merge or a fast-forward. `landedness` arm one.
|
|
54
|
+
* 2. CONTENT. Merging the branch into the base changes no tree, so the base
|
|
55
|
+
* already carries everything the branch did however it got there. This is
|
|
56
|
+
* MEMBER A, the squash merge, and it is `landedness` arm two.
|
|
57
|
+
* 3. PATCH EQUIVALENCE. Every commit the branch carries has an equivalent
|
|
58
|
+
* patch already upstream, found by patch id rather than by sha. This is
|
|
59
|
+
* MEMBER B: commits that landed inside ANOTHER branch's pull request, so
|
|
60
|
+
* they appear on the base interleaved with commits the branch never had.
|
|
61
|
+
*
|
|
62
|
+
* ARM 3 IS NOT REDUNDANT WITH ARM 2, and the case that separates them is the
|
|
63
|
+
* ordinary one. Once the base moves on and edits the same region again, a
|
|
64
|
+
* three-way merge of the delivered branch CONFLICTS, so arm 2 answers `open`
|
|
65
|
+
* for a branch whose every commit is demonstrably already upstream. Arm 3
|
|
66
|
+
* answers it by patch id, which survives the later edit.
|
|
67
|
+
*
|
|
68
|
+
* ARM 2 IS NOT REDUNDANT WITH ARM 3 EITHER. A squash collapses N commits into
|
|
69
|
+
* one, whose patch id is the id of the COMBINED diff and therefore equals no
|
|
70
|
+
* individual commit's. For any branch of more than one commit, arm 3 sees only
|
|
71
|
+
* unmatched commits and answers `open`.
|
|
72
|
+
*
|
|
73
|
+
* REUSE, NOT A SECOND IMPLEMENTATION (T-005). Arms 1 and 2 are
|
|
74
|
+
* `landedness` at src/teardown.ts:123, the predicate `tiphys teardown` already
|
|
75
|
+
* refuses on. A second copy here would be a second thing to keep true.
|
|
76
|
+
*
|
|
77
|
+
* INCONCLUSIVE IS NOT `open` AND IT IS NOT `delivered`. A git failure returns
|
|
78
|
+
* `unknown`, which the caller counts as work remaining, because the one answer
|
|
79
|
+
* this predicate must never invent is a confident one.
|
|
80
|
+
*/
|
|
81
|
+
export declare function deliveredElsewhere(contextDir: string, branchRef: string, baseRef: string): Delivery;
|
|
82
|
+
/** One thing that is in flight, in the category that found it. */
|
|
83
|
+
export interface InFlightItem {
|
|
84
|
+
category: "task" | "worktree" | "branch";
|
|
85
|
+
what: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Everything in flight in a fleet home, plus every candidate left unread, plus
|
|
89
|
+
* the disclosures a reader needs to check the judgement rather than trust it.
|
|
90
|
+
*
|
|
91
|
+
* `notes` DOES NOT COUNT TOWARD THE EXIT CODE and that is deliberate. It
|
|
92
|
+
* carries facts about HOW a judgement was made (which base ref each project's
|
|
93
|
+
* branches were compared against, and by which rule that base was chosen), not
|
|
94
|
+
* facts about whether work remains. Putting them in `unknown` would hold the
|
|
95
|
+
* exit at `EXIT_WORK_REMAINS` forever; leaving them out is what let a clone
|
|
96
|
+
* whose `origin/HEAD` points at a feature branch judge every branch against
|
|
97
|
+
* that branch with nothing in the output saying so.
|
|
98
|
+
*/
|
|
99
|
+
export interface InFlight {
|
|
100
|
+
items: InFlightItem[];
|
|
101
|
+
unknown: string[];
|
|
102
|
+
notes: string[];
|
|
103
|
+
}
|
|
104
|
+
export declare function baseRefOf(projectDir: string): {
|
|
105
|
+
ok: true;
|
|
106
|
+
ref: string;
|
|
107
|
+
how: string;
|
|
108
|
+
doubt?: string;
|
|
109
|
+
} | {
|
|
110
|
+
ok: false;
|
|
111
|
+
reason: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Every in-flight category in a fleet home, in dependency order: the task
|
|
115
|
+
* record, the checkout it owns, and the branch that outlives both.
|
|
116
|
+
*/
|
|
117
|
+
export declare function collectInFlight(fleet: Fleet): InFlight;
|
|
118
|
+
/**
|
|
119
|
+
* THE ONE NEXT ACTION (criterion 1). Exactly one, chosen by the first
|
|
120
|
+
* non-empty category in the order above, so the answer is derived from the
|
|
121
|
+
* report rather than picked.
|
|
122
|
+
*/
|
|
123
|
+
export declare function nextAction(report: InFlight): string;
|
|
124
|
+
/** The cannot-see block. Unconditional, and it probes nothing to build. */
|
|
125
|
+
export declare function cannotSeeBlock(): string[];
|
|
126
|
+
/** The whole report, as lines, with exactly one `next action:` line. */
|
|
127
|
+
export declare function renderReport(fleet: Fleet, report: InFlight): string[];
|
|
128
|
+
/** Work remains whenever anything is in flight OR anything could not be read. */
|
|
129
|
+
export declare function exitCodeFor(report: InFlight): number;
|
|
130
|
+
export declare function cmdNext(args: string[]): number;
|