@tiphys/kernel 0.1.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 +56 -4
- package/assurance-modes.yaml +23 -2
- package/dist/bin/tiphys.js +86 -8
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.js +27 -20
- package/dist/src/checks.d.ts +720 -9
- package/dist/src/checks.js +1874 -163
- package/dist/src/cli.js +11 -0
- package/dist/src/commands/brief.js +27 -4
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +229 -0
- package/dist/src/commands/doctor.js +968 -27
- package/dist/src/commands/init.d.ts +3 -3
- package/dist/src/commands/init.js +57 -8
- package/dist/src/commands/lock.d.ts +33 -0
- package/dist/src/commands/lock.js +117 -6
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/pool.js +12 -1
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.js +51 -2
- package/dist/src/commands/status.d.ts +6 -4
- package/dist/src/commands/status.js +6 -4
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.js +10 -2
- package/dist/src/commands/validate.js +70 -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 +152 -2
- package/dist/src/exec/env.js +146 -2
- package/dist/src/fleet.d.ts +172 -0
- package/dist/src/fleet.js +219 -1
- package/dist/src/gates/citations.js +7 -1
- package/dist/src/gates/coverage.d.ts +113 -22
- package/dist/src/gates/coverage.js +166 -31
- package/dist/src/gates/credentials.d.ts +159 -0
- package/dist/src/gates/credentials.js +221 -2
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/red-witness.js +105 -13
- package/dist/src/gates/run.d.ts +49 -1
- package/dist/src/gates/run.js +83 -5
- package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
- package/dist/src/gates/suite.js +48 -7
- package/dist/src/hooks.d.ts +55 -3
- package/dist/src/hooks.js +69 -6
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/lock.d.ts +82 -4
- package/dist/src/lock.js +314 -22
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/path-identity.d.ts +32 -0
- package/dist/src/path-identity.js +38 -0
- package/dist/src/pool.d.ts +197 -1
- package/dist/src/pool.js +289 -22
- package/dist/src/roles.d.ts +31 -0
- package/dist/src/roles.js +42 -0
- package/dist/src/spawn.d.ts +307 -2
- package/dist/src/spawn.js +690 -19
- package/dist/src/status.d.ts +27 -2
- package/dist/src/status.js +34 -5
- package/dist/src/task.d.ts +295 -55
- package/dist/src/task.js +125 -123
- package/dist/src/teardown.d.ts +7 -0
- package/dist/src/teardown.js +120 -12
- package/dist/src/validate.d.ts +44 -11
- package/dist/src/validate.js +44 -34
- package/dist/src/watcher.js +1 -11
- package/dist/src/witness/run.d.ts +32 -7
- package/dist/src/witness/run.js +76 -30
- package/dist/src/witness/spec.d.ts +168 -0
- package/dist/src/witness/spec.js +240 -18
- package/dist/tsconfig.src.tsbuildinfo +1 -1
- package/gate-registry.yaml +136 -0
- package/gates.manifest.json +63 -1
- package/package.json +18 -3
- package/roles/implementer.md +3 -0
- package/schemas/README.md +1 -0
- package/schemas/assurance-modes.schema.json +1 -1
- package/schemas/charter.schema.json +19 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/verdict.schema.json +9 -3
- package/schemas/write-bypass.schema.json +69 -0
package/dist/src/task.js
CHANGED
|
@@ -1,108 +1,12 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
/* IMPORTED UNDER THE OLD NAMES rather than re-exported straight through,
|
|
4
|
+
because this module CALLS them as well as publishing them. A bare
|
|
5
|
+
`export ... from` publishes without binding, so the internal call sites below
|
|
6
|
+
would not resolve and the only signal would be a build error. */
|
|
7
|
+
import { classifyPathEntry as classifyEntry, readRegularPathIfPresent as readRegularFileIfPresent, refuseOpenPathForWrite as refuseOpenForWrite, } from "./fleet.js";
|
|
3
8
|
import { leaseStatus } from "./lock.js";
|
|
4
|
-
|
|
5
|
-
if (stats.isDirectory()) {
|
|
6
|
-
return "a directory";
|
|
7
|
-
}
|
|
8
|
-
if (stats.isFIFO()) {
|
|
9
|
-
return "a named pipe";
|
|
10
|
-
}
|
|
11
|
-
if (stats.isSocket()) {
|
|
12
|
-
return "a socket";
|
|
13
|
-
}
|
|
14
|
-
if (stats.isCharacterDevice()) {
|
|
15
|
-
return "a character device";
|
|
16
|
-
}
|
|
17
|
-
if (stats.isBlockDevice()) {
|
|
18
|
-
return "a block device";
|
|
19
|
-
}
|
|
20
|
-
return "an entry of an unrecognized type";
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* THE ONE ANSWER TO "may this path be opened". Every reader and every
|
|
24
|
-
* writer of a path this kernel does not itself guarantee to be a regular
|
|
25
|
-
* file goes through this, so there is one implementation of the question
|
|
26
|
-
* and not one per call site.
|
|
27
|
-
*/
|
|
28
|
-
export function classifyEntry(path) {
|
|
29
|
-
try {
|
|
30
|
-
lstatSync(path);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (error.code === "ENOENT") {
|
|
34
|
-
return { kind: "absent" };
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
kind: "unexaminable",
|
|
38
|
-
reason: `${path} could not be examined: ${String(error)}`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
let stats;
|
|
42
|
-
try {
|
|
43
|
-
stats = statSync(path);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
if (error.code === "ENOENT") {
|
|
47
|
-
return { kind: "dangling" };
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
kind: "unexaminable",
|
|
51
|
-
reason: `${path} could not be examined: ${String(error)}`,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
if (stats.isFile()) {
|
|
55
|
-
return { kind: "regular" };
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
kind: "irregular",
|
|
59
|
-
reason: `${path} is ${describeType(stats)}, not a regular file, so it was not opened`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Refuse an open-for-WRITE of a path that is not a regular file. The
|
|
64
|
-
* hazard is symmetric: open(2) for writing on a FIFO with no reader blocks
|
|
65
|
-
* exactly as reading one with no writer does, so a staged write and an
|
|
66
|
-
* append are as dangerous as a read. Returns the reason, or undefined when
|
|
67
|
-
* the path may be opened (absent included: creating it is the point).
|
|
68
|
-
*/
|
|
69
|
-
export function refuseOpenForWrite(path) {
|
|
70
|
-
const entry = classifyEntry(path);
|
|
71
|
-
if (entry.kind === "irregular" || entry.kind === "unexaminable") {
|
|
72
|
-
return entry.reason;
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* THE ONE READ of a file that might not be there and might not be a file.
|
|
78
|
-
* src/task.ts, src/liveness.ts, src/watcher.ts and src/commands/doctor.ts
|
|
79
|
-
* all read fleet state through this, so "probe before open" is a property
|
|
80
|
-
* of the read and cannot be forgotten by a new caller.
|
|
81
|
-
*/
|
|
82
|
-
export function readRegularFileIfPresent(path) {
|
|
83
|
-
const entry = classifyEntry(path);
|
|
84
|
-
if (entry.kind === "absent" || entry.kind === "dangling") {
|
|
85
|
-
return { kind: "absent" };
|
|
86
|
-
}
|
|
87
|
-
if (entry.kind === "irregular" || entry.kind === "unexaminable") {
|
|
88
|
-
return { kind: "refused", reason: entry.reason };
|
|
89
|
-
}
|
|
90
|
-
let body;
|
|
91
|
-
try {
|
|
92
|
-
body = readFileSync(path, "utf8");
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
if (error.code === "ENOENT") {
|
|
96
|
-
// Removed between the probe and the read.
|
|
97
|
-
return { kind: "absent" };
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
kind: "refused",
|
|
101
|
-
reason: `${path} could not be read: ${String(error)}`,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
return { kind: "read", body };
|
|
105
|
-
}
|
|
9
|
+
export { classifyEntry, readRegularFileIfPresent, refuseOpenForWrite };
|
|
106
10
|
export const TASK_SHAPES = ["ship", "scout"];
|
|
107
11
|
export function taskDir(fleet, taskId) {
|
|
108
12
|
return join(fleet.tasksDir, taskId);
|
|
@@ -129,10 +33,33 @@ export function renderTaskMeta(meta) {
|
|
|
129
33
|
export function writeTaskMeta(fleet, meta) {
|
|
130
34
|
writeFileSync(metaPath(fleet, meta.id), renderTaskMeta(meta));
|
|
131
35
|
}
|
|
36
|
+
/** Which required field first failed, or undefined when all of them hold. */
|
|
37
|
+
function firstBadMetaField(candidate) {
|
|
38
|
+
if (typeof candidate.id !== "string")
|
|
39
|
+
return "id (a string)";
|
|
40
|
+
if (typeof candidate.project !== "string")
|
|
41
|
+
return "project (a string)";
|
|
42
|
+
if (candidate.shape !== "ship" && candidate.shape !== "scout") {
|
|
43
|
+
return 'shape (either "ship" or "scout")';
|
|
44
|
+
}
|
|
45
|
+
if (typeof candidate.branch !== "string")
|
|
46
|
+
return "branch (a string)";
|
|
47
|
+
if (typeof candidate.worktree !== "string")
|
|
48
|
+
return "worktree (a string)";
|
|
49
|
+
if (typeof candidate.baseSha !== "string")
|
|
50
|
+
return "baseSha (a string)";
|
|
51
|
+
if (typeof candidate.baseOffline !== "boolean")
|
|
52
|
+
return "baseOffline (a boolean)";
|
|
53
|
+
if (candidate.status !== "open" && candidate.status !== "closed") {
|
|
54
|
+
return 'status (either "open" or "closed")';
|
|
55
|
+
}
|
|
56
|
+
if (typeof candidate.createdAt !== "string")
|
|
57
|
+
return "createdAt (a string)";
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
132
60
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* is not a readable record, and it is not evidence that the task finished.
|
|
61
|
+
* THE ONE READ of a task record, and the one that says WHICH of the four
|
|
62
|
+
* outcomes happened.
|
|
136
63
|
*
|
|
137
64
|
* The type probe is INSIDE this function and not in front of one of its
|
|
138
65
|
* callers (CR-520, CR-521). There is exactly one implementation of "read a
|
|
@@ -140,32 +67,71 @@ export function writeTaskMeta(fleet, meta) {
|
|
|
140
67
|
* cannot reopen the hole: src/teardown.ts reaches this directly, without
|
|
141
68
|
* going through the liveness classifier, and a named pipe here used to
|
|
142
69
|
* hang it forever.
|
|
70
|
+
*
|
|
71
|
+
* WHY THIS IS A SEPARATE FUNCTION FROM `readTaskMeta` RATHER THAN ITS NEW
|
|
72
|
+
* SIGNATURE, declared rather than left to be inferred. Widening the return
|
|
73
|
+
* type of `readTaskMeta` is the shape this repair wants, and it is a
|
|
74
|
+
* compile-time break in four modules that this fix round's file list does not
|
|
75
|
+
* own (src/pool.ts, src/liveness.ts, src/teardown.ts, src/commands/next.ts).
|
|
76
|
+
* The distinction is therefore made AVAILABLE upstream of all of them here,
|
|
77
|
+
* `readTaskMeta` is documented as the deliberate NARROWING of it, and the
|
|
78
|
+
* sites that still collapse are named in the fix round's work history rather
|
|
79
|
+
* than quietly left.
|
|
143
80
|
*/
|
|
144
|
-
export function
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
81
|
+
export function classifyTaskMeta(fleet, taskId) {
|
|
82
|
+
const path = metaPath(fleet, taskId);
|
|
83
|
+
const read = readRegularFileIfPresent(path);
|
|
84
|
+
if (read.kind === "absent") {
|
|
85
|
+
return { kind: "absent" };
|
|
86
|
+
}
|
|
87
|
+
if (read.kind === "refused") {
|
|
88
|
+
return { kind: "unreadable", reason: read.reason };
|
|
148
89
|
}
|
|
149
90
|
let parsed;
|
|
150
91
|
try {
|
|
151
92
|
parsed = JSON.parse(read.body);
|
|
152
93
|
}
|
|
153
|
-
catch {
|
|
154
|
-
|
|
94
|
+
catch (error) {
|
|
95
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
96
|
+
return {
|
|
97
|
+
kind: "unparsable",
|
|
98
|
+
reason: `${path} is present (${read.body.length} byte(s)) and does not parse as ` +
|
|
99
|
+
`JSON (${detail}), which is what a task record killed mid-write looks ` +
|
|
100
|
+
`like; it is not evidence that there is no task here`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
104
|
+
return {
|
|
105
|
+
kind: "malformed",
|
|
106
|
+
reason: `${path} parses as JSON but not as an object, so it is not a task record`,
|
|
107
|
+
};
|
|
155
108
|
}
|
|
156
109
|
const candidate = parsed;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
(candidate.status !== "open" && candidate.status !== "closed") ||
|
|
165
|
-
typeof candidate.createdAt !== "string") {
|
|
166
|
-
return undefined;
|
|
110
|
+
const bad = firstBadMetaField(candidate);
|
|
111
|
+
if (bad !== undefined) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "malformed",
|
|
114
|
+
reason: `${path} parses as JSON but is not a task record: it needs ${bad}; it is ` +
|
|
115
|
+
`not evidence that there is no task here`,
|
|
116
|
+
};
|
|
167
117
|
}
|
|
168
|
-
return candidate;
|
|
118
|
+
return { kind: "read", meta: candidate };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read meta.json, or undefined when it is absent, is not a regular file,
|
|
122
|
+
* does not parse, or parses and is not a task record.
|
|
123
|
+
*
|
|
124
|
+
* THIS IS A DELIBERATE NARROWING OF `classifyTaskMeta` AND THE COLLAPSE IS THE
|
|
125
|
+
* WHOLE OF CR-F02. `undefined` answers "is there a readable record" and it
|
|
126
|
+
* cannot answer "is there a task here", because it is returned both when the
|
|
127
|
+
* kernel looked and found nothing and when it found something it could not
|
|
128
|
+
* read. A caller that reports an absence, skips an entry, or decides nothing
|
|
129
|
+
* is in flight must call `classifyTaskMeta` instead; a caller that REFUSES on
|
|
130
|
+
* every one of the four (teardown does) loses nothing by using this.
|
|
131
|
+
*/
|
|
132
|
+
export function readTaskMeta(fleet, taskId) {
|
|
133
|
+
const read = classifyTaskMeta(fleet, taskId);
|
|
134
|
+
return read.kind === "read" ? read.meta : undefined;
|
|
169
135
|
}
|
|
170
136
|
/** Set meta.json status (teardown's last step; C-1's state authority). */
|
|
171
137
|
export function setTaskStatus(fleet, meta, status) {
|
|
@@ -303,3 +269,39 @@ export function runStep(what, step) {
|
|
|
303
269
|
return { ok: false, reason: `${what} failed: ${detail}` };
|
|
304
270
|
}
|
|
305
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* runStep's shape for a step that returns a PROMISE (M4-P2 step 2).
|
|
274
|
+
*
|
|
275
|
+
* This is a PROMOTION, not a new function. It lived module-private in
|
|
276
|
+
* src/watcher.ts, where its own comment said "src/task.ts covers the sync
|
|
277
|
+
* one"; spawn's launch call site needs the same shape now that
|
|
278
|
+
* `ExecutorAdapter.launch` returns a promise, and T-005's one-mechanism
|
|
279
|
+
* rule makes copying it a third time the wrong answer. The promotion was
|
|
280
|
+
* measured before it was made: the two functions were run side by side over
|
|
281
|
+
* six `what`/thrown-error pairs covering an ordinary Error, an Error with an
|
|
282
|
+
* empty message, a thrown string, a thrown object with a custom toString, an
|
|
283
|
+
* Error subclass and a multi-line pair, and their reason strings were
|
|
284
|
+
* compared as buffers. All six were byte-identical, so no watcher message
|
|
285
|
+
* changes on any of those six. The capture is in the M4-P2 work history
|
|
286
|
+
* under `delivery/work-history/m4-p2.md`.
|
|
287
|
+
*
|
|
288
|
+
* WHY A SEPARATE FUNCTION RATHER THAN WIDENING runStep. `runStep` returns
|
|
289
|
+
* `{ok: true, value}` the instant its callback returns, and a callback that
|
|
290
|
+
* returns a promise returns one IMMEDIATELY. So `runStep` over an async
|
|
291
|
+
* step is not merely imprecise: its `ok: true` is a claim that the step
|
|
292
|
+
* SUCCEEDED when nothing has run yet, and a later rejection escapes the
|
|
293
|
+
* result type entirely as an unhandled rejection. That is the exact defect
|
|
294
|
+
* this phase's criterion 4 witness reddens against.
|
|
295
|
+
*
|
|
296
|
+
* It never swallows, on the same terms as the sync one: the caller gets a
|
|
297
|
+
* reason naming the step and still decides what to do about it.
|
|
298
|
+
*/
|
|
299
|
+
export async function runStepAsync(what, step) {
|
|
300
|
+
try {
|
|
301
|
+
return { ok: true, value: await step() };
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
305
|
+
return { ok: false, reason: `${what} failed: ${detail}` };
|
|
306
|
+
}
|
|
307
|
+
}
|
package/dist/src/teardown.d.ts
CHANGED
|
@@ -28,5 +28,12 @@ export declare function landedness(contextDir: string, branchRef: string, defaul
|
|
|
28
28
|
export interface TeardownOptions {
|
|
29
29
|
taskId: string;
|
|
30
30
|
salvage: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Proceed on a pool record RECONSTRUCTED from tasks/<id>/meta.json and
|
|
33
|
+
* git, when worktrees/<id>.pool.json did not survive a reclaim
|
|
34
|
+
* (M4-P19, M4-D-12). See the FROM-RECONSTRUCTED header below for what
|
|
35
|
+
* this flag does and, more importantly, what it does NOT do.
|
|
36
|
+
*/
|
|
37
|
+
fromReconstructed: boolean;
|
|
31
38
|
}
|
|
32
39
|
export declare function teardownTask(fleet: Fleet, options: TeardownOptions): Promise<TeardownResult>;
|
package/dist/src/teardown.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { MACHINE_IDENTITY_EMAIL, MACHINE_IDENTITY_NAME } from "./commands/init.js";
|
|
4
|
-
import {
|
|
4
|
+
import { guardSharedRegister } from "./exclusion.js";
|
|
5
|
+
import { poolDestroy, readPoolRecord, reconstructPoolRecord, worktreePath, } from "./pool.js";
|
|
5
6
|
import { checkHoldership, metaPath, readTaskMeta, reportPath, runStep, setTaskStatus, singleLine, } from "./task.js";
|
|
6
7
|
/**
|
|
7
8
|
* Local git runner. It is deliberately not the pool's: src/pool.ts is
|
|
@@ -80,7 +81,7 @@ export function landedness(contextDir, branchRef, defaultRef) {
|
|
|
80
81
|
* Read-only with respect to the task: the fetch updates only the
|
|
81
82
|
* project's remote-tracking ref.
|
|
82
83
|
*/
|
|
83
|
-
function resolveContext(fleet, taskId) {
|
|
84
|
+
function resolveContext(fleet, taskId, fromReconstructed) {
|
|
84
85
|
const meta = readTaskMeta(fleet, taskId);
|
|
85
86
|
if (meta === undefined) {
|
|
86
87
|
return {
|
|
@@ -88,13 +89,45 @@ function resolveContext(fleet, taskId) {
|
|
|
88
89
|
reason: `no readable task meta for task id ${taskId}; teardown needs tasks/${taskId}/meta.json`,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
+
let record = readPoolRecord(fleet, taskId);
|
|
93
|
+
let reconstructed = false;
|
|
92
94
|
if (record === undefined) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
|
|
95
|
+
if (!fromReconstructed) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: `no readable pool record for task id ${taskId}; teardown needs it for the ` +
|
|
99
|
+
`project remote and default branch, and refuses rather than guessing them; ` +
|
|
100
|
+
`pass --from-reconstructed to rebuild them from tasks/${taskId}/meta.json ` +
|
|
101
|
+
`and git, which keeps every other refusal in force`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// NETWORK ALLOWED here and nowhere else in this kernel's
|
|
105
|
+
// reconstruction (M4-P19 fix round). Teardown is a command the
|
|
106
|
+
// operator invoked in order to destroy something, it is about to
|
|
107
|
+
// fetch from this remote on the next line regardless, and it is
|
|
108
|
+
// allowed to take as long as that fetch takes. `pool list` and
|
|
109
|
+
// doctor are not, and they pass `{ network: false }`.
|
|
110
|
+
const rebuilt = reconstructPoolRecord(fleet, taskId, { network: true });
|
|
111
|
+
if (rebuilt.kind === "absent") {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: `cannot reconstruct the pool record for task id ${taskId}: ${rebuilt.reason}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (rebuilt.kind === "incomplete") {
|
|
118
|
+
// The missing FIELD is named, not merely the failure, because the
|
|
119
|
+
// remedy differs per field: `remote` is a git configuration repair
|
|
120
|
+
// in the clone, `branch` is usually an origin/HEAD that was never
|
|
121
|
+
// set or a remote that cannot be reached to advertise it.
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `cannot reconstruct the pool record for task id ${taskId}: unresolved ` +
|
|
125
|
+
`field(s) ${rebuilt.unresolved.join(", ")} (${rebuilt.detail}); teardown ` +
|
|
126
|
+
`refuses rather than guessing them`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
record = rebuilt.record;
|
|
130
|
+
reconstructed = true;
|
|
98
131
|
}
|
|
99
132
|
const worktree = worktreePath(fleet, taskId);
|
|
100
133
|
const defaultRef = `refs/remotes/${record.remote}/${record.branch}`;
|
|
@@ -111,7 +144,7 @@ function resolveContext(fleet, taskId) {
|
|
|
111
144
|
`be judged against fresh remote state: ${singleLine(fetched.stderr)}`,
|
|
112
145
|
};
|
|
113
146
|
}
|
|
114
|
-
return { ok: true, value: { meta, record, worktree, defaultRef } };
|
|
147
|
+
return { ok: true, value: { meta, record, reconstructed, worktree, defaultRef } };
|
|
115
148
|
}
|
|
116
149
|
/** Uncommitted changes or untracked files in the task worktree. */
|
|
117
150
|
function worktreeDirty(worktree) {
|
|
@@ -171,6 +204,11 @@ async function finish(fleet, context, options) {
|
|
|
171
204
|
taskId: context.meta.id,
|
|
172
205
|
discard: options.discard,
|
|
173
206
|
deleteBranchForce: options.deleteBranchForce,
|
|
207
|
+
// M4-P19: in memory only, and only when the record was rebuilt. This
|
|
208
|
+
// makes the destroy's own base-sha gate ABLE to judge instead of
|
|
209
|
+
// abstaining with its "pool record missing" refusal; it does not
|
|
210
|
+
// create a file and it does not relax a gate.
|
|
211
|
+
...(context.reconstructed ? { reconstructed: context.record } : {}),
|
|
174
212
|
});
|
|
175
213
|
if (!destroyed.ok) {
|
|
176
214
|
// The destroy's own reason distinguishes a stage-2 refusal (a true
|
|
@@ -213,13 +251,69 @@ export async function teardownTask(fleet, options) {
|
|
|
213
251
|
if (!holdership.ok) {
|
|
214
252
|
return { ok: false, reason: holdership.reason };
|
|
215
253
|
}
|
|
216
|
-
|
|
254
|
+
/* THE CROSS-ENVIRONMENT HALF (M4-P22 criterion 3), rule 0's second limb.
|
|
255
|
+
`checkHoldership` above returns OK when this environment holds the local
|
|
256
|
+
lease, and the local lease is evidence about this filesystem alone
|
|
257
|
+
(src/lock.ts:63). With the shared register naming another environment,
|
|
258
|
+
the old guard is green and teardown would remove a worktree the other
|
|
259
|
+
orchestrator is working in. One reason line, nonzero exit, and NOTHING
|
|
260
|
+
REMOVED: it runs before `resolveContext`, so no pool record is read, no
|
|
261
|
+
worktree is probed and no branch is deleted. A teardown that refuses
|
|
262
|
+
after deleting something has failed in the way that matters. */
|
|
263
|
+
const sharedGuard = guardSharedRegister(fleet.root, "teardown");
|
|
264
|
+
if (sharedGuard.kind === "refused") {
|
|
265
|
+
return { ok: false, reason: sharedGuard.reason };
|
|
266
|
+
}
|
|
267
|
+
const resolved = resolveContext(fleet, options.taskId, options.fromReconstructed);
|
|
217
268
|
if (!resolved.ok) {
|
|
218
269
|
return resolved;
|
|
219
270
|
}
|
|
220
271
|
const context = resolved.value;
|
|
221
272
|
const { meta, record, worktree } = context;
|
|
222
273
|
if (meta.shape === "scout") {
|
|
274
|
+
// M4-P19 FIX ROUND, finding F-5. THE RECONSTRUCTED PATH IS STRICTER
|
|
275
|
+
// THAN THE WITH-RECORD PATH FOR A SCOUT, DELIBERATELY.
|
|
276
|
+
//
|
|
277
|
+
// The scout arm below discards a dirty scratch tree by design
|
|
278
|
+
// (PR-010: a scout is judged by its report and never pushes), and it
|
|
279
|
+
// reaches `finish` without ever probing cleanliness. That was
|
|
280
|
+
// reachable only by an operator who had the pool record in front of
|
|
281
|
+
// them. This phase made it reachable from a RECLAIM, where the
|
|
282
|
+
// record is the one thing that did not survive, and measured at head
|
|
283
|
+
// abde402: `teardown --task s1 --from-reconstructed` against a scout
|
|
284
|
+
// worktree holding ` M readme.md` and `?? important.md` exited 0 and
|
|
285
|
+
// removed the worktree, where the same fixture on the phase base
|
|
286
|
+
// exited 1 and left it standing. Plan criterion 4 states the refusal
|
|
287
|
+
// with no shape qualifier, so the plan is what is followed here.
|
|
288
|
+
//
|
|
289
|
+
// WHY NOT INSTEAD MAKE THE WITH-RECORD SCOUT PATH REFUSE TOO. That
|
|
290
|
+
// is a change to a decided scout policy (PR-010) which this phase
|
|
291
|
+
// does not own and which the plan does not ask for. The asymmetry is
|
|
292
|
+
// therefore REAL and is stated rather than smoothed over: the
|
|
293
|
+
// difference in force is the difference in what the operator knows.
|
|
294
|
+
// With the record present they are tearing down a scout they are
|
|
295
|
+
// tracking; arriving here from a reclaim they are recovering a fleet
|
|
296
|
+
// whose bookkeeping is already known to be incomplete, and the
|
|
297
|
+
// leavings in that tree may be the only copy.
|
|
298
|
+
//
|
|
299
|
+
// --salvage is NOT the escape, because salvage pushes (PR-010: a
|
|
300
|
+
// scout never pushes), so the remedy named is the one that works.
|
|
301
|
+
if (context.reconstructed) {
|
|
302
|
+
const scoutDirty = worktreeDirty(worktree);
|
|
303
|
+
if (!scoutDirty.ok) {
|
|
304
|
+
return { ok: false, reason: scoutDirty.reason };
|
|
305
|
+
}
|
|
306
|
+
if (scoutDirty.dirty) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
reason: `scout worktree ${worktree} has uncommitted changes or untracked ` +
|
|
310
|
+
`files and its pool record did not survive, so teardown is running ` +
|
|
311
|
+
`on a reconstruction and refuses to discard them: copy anything ` +
|
|
312
|
+
`worth keeping out of ${worktree}, then re-run once ` +
|
|
313
|
+
`"git -C ${worktree} status --porcelain" is empty`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
223
317
|
// (a) A scout is judged by its report, never by its scratch tree.
|
|
224
318
|
if (!existsSync(reportPath(fleet, options.taskId))) {
|
|
225
319
|
return {
|
|
@@ -287,10 +381,24 @@ export async function teardownTask(fleet, options) {
|
|
|
287
381
|
};
|
|
288
382
|
}
|
|
289
383
|
if (landed.kind === "unlanded") {
|
|
384
|
+
// M4-P19 criterion 5: the refusal NAMES THE TIP. That sha is the
|
|
385
|
+
// operator's recovery handle, exactly as the deleted-branch sha is on
|
|
386
|
+
// the success path (V-1), and it is the one fact that makes this
|
|
387
|
+
// refusal actionable: it says which commit is at risk, not merely
|
|
388
|
+
// that something is. It is named on every unlanded refusal rather
|
|
389
|
+
// than only the reconstructed one, so there is one message and not
|
|
390
|
+
// two that can drift apart.
|
|
391
|
+
const tip = runGit(record.project, [
|
|
392
|
+
"rev-parse",
|
|
393
|
+
"--verify",
|
|
394
|
+
"--quiet",
|
|
395
|
+
`${branchRef}^{commit}`,
|
|
396
|
+
]);
|
|
397
|
+
const tipSha = tip.status === 0 ? tip.stdout.trim() : "unresolvable";
|
|
290
398
|
return {
|
|
291
399
|
ok: false,
|
|
292
|
-
reason: `branch ${record.branchName} is not landed on
|
|
293
|
-
|
|
400
|
+
reason: `branch ${record.branchName} (tip ${tipSha}) is not landed on ` +
|
|
401
|
+
`${record.remote}/${record.branch}; land it before tearing the task down` +
|
|
294
402
|
(options.salvage ? " (--salvage rescues leavings, it never lands work)" : ""),
|
|
295
403
|
};
|
|
296
404
|
}
|
package/dist/src/validate.d.ts
CHANGED
|
@@ -23,11 +23,22 @@
|
|
|
23
23
|
* deliberately identical: the two modules now share this one engine.
|
|
24
24
|
*
|
|
25
25
|
* THE MESSAGE TABLE IS THE CONTRACT, not Ajv's `message` field. A keyword
|
|
26
|
-
* whose Ajv error reaches `renderAjvError`
|
|
27
|
-
* `
|
|
28
|
-
* keyword, rather than being papered over with
|
|
29
|
-
* refusal is what stops Ajv wording leaking into a
|
|
30
|
-
* omission (DR-0013 criterion 8).
|
|
26
|
+
* whose Ajv error reaches `renderAjvError` and matches no `case` of that
|
|
27
|
+
* function's switch falls to its `default:` arm, which is a Tiphys DEFECT and
|
|
28
|
+
* is reported as one, naming the keyword, rather than being papered over with
|
|
29
|
+
* Ajv's own sentence. That refusal is what stops Ajv wording leaking into a
|
|
30
|
+
* public contract by omission (DR-0013 criterion 8).
|
|
31
|
+
*
|
|
32
|
+
* THIS PARAGRAPH NAMED THE WRONG MECHANISM UNTIL THE DR-0047 SWEEP (CR-VS-004).
|
|
33
|
+
* It said the refusal came from a `MESSAGE_BY_KEYWORD` set. That set existed,
|
|
34
|
+
* nothing ever read it (three references: its own declaration and two comments
|
|
35
|
+
* saying it was the contract), and its contents disagreed with
|
|
36
|
+
* `AUTHORING_VOCABULARY` in BOTH directions: `$ref`, `items`, `properties` and
|
|
37
|
+
* `then` were in the vocabulary and not in the set, `minimum`, `maximum` and
|
|
38
|
+
* `maxItems` were in the set and not in the vocabulary. A dead constant that a
|
|
39
|
+
* file's own documentation calls the contract is how the next reader edits the
|
|
40
|
+
* thing that does nothing, so it is deleted and this sentence now names the
|
|
41
|
+
* switch, which is what actually refuses.
|
|
31
42
|
*
|
|
32
43
|
* YAML IS INPUT DECODING AND IS A SEPARATE STAGE (DR-0013 YAML clause 3).
|
|
33
44
|
* `decodeDocument` decodes; `validateInstance` validates an already-decoded
|
|
@@ -50,12 +61,34 @@ export interface Diagnostic {
|
|
|
50
61
|
message: string;
|
|
51
62
|
}
|
|
52
63
|
/**
|
|
53
|
-
* THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7)
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
64
|
+
* THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7). THIS LIST IS THE SOURCE
|
|
65
|
+
* OF TRUTH. Ajv supplies Draft 2020-12 entire; this is what Tiphys schemas are
|
|
66
|
+
* ALLOWED to use, so a keyword outside it is a deliberate expansion rather than
|
|
67
|
+
* an accident. Every entry has both a positive and a negative test (validator
|
|
68
|
+
* criterion 2), asserted by `test/schemas.test.ts`, which DERIVES its cases
|
|
69
|
+
* from this array rather than from a hand-written list.
|
|
70
|
+
*
|
|
71
|
+
* `schemas/README.md` RENDERS THIS LIST FOR A HUMAN READER AND IS NOT ITSELF
|
|
72
|
+
* THE CONTRACT, and the distinction was paid for (CR-VS-004, FIND-02 of the
|
|
73
|
+
* DR-0047 sweep). The sentence here used to say the vocabulary IS "documented
|
|
74
|
+
* in `schemas/README.md`", which reads as a guarantee that the two agree.
|
|
75
|
+
* Measured at the swept head they did not: this array holds sixteen keywords
|
|
76
|
+
* and that document's table declared fifteen, `uniqueItems` being the missing
|
|
77
|
+
* row, since the day M3-P1 wrote both. Nothing asserted the relation, so no gate
|
|
78
|
+
* could see it.
|
|
79
|
+
*
|
|
80
|
+
* THE DRIFT IS NOW CLOSED, AND BY THE TEST RATHER THAN BY THE ROW (DR-0047
|
|
81
|
+
* sweep round 2). The row was added to `schemas/README.md`, which closes the one
|
|
82
|
+
* instance; what closes the MECHANISM is that test/schemas.test.ts:340 parses
|
|
83
|
+
* that document's keyword column and compares it to this array, so the two
|
|
84
|
+
* halves can no longer disagree silently. The document still does not DECIDE
|
|
85
|
+
* anything: this array is the contract and that one renders it, which is why
|
|
86
|
+
* the comparison is written as an assertion about the document rather than as a
|
|
87
|
+
* generator.
|
|
88
|
+
*
|
|
89
|
+
* AND THE SECOND HALF OF THAT SENTENCE IS GONE. It said every entry "therefore
|
|
90
|
+
* also has an entry in `MESSAGE_BY_KEYWORD`", which was false in both
|
|
91
|
+
* directions and about a constant nothing read; see the module header.
|
|
59
92
|
*/
|
|
60
93
|
export declare const AUTHORING_VOCABULARY: readonly string[];
|
|
61
94
|
/** Annotations that carry no constraint and are permitted everywhere. */
|
package/dist/src/validate.js
CHANGED
|
@@ -23,11 +23,22 @@
|
|
|
23
23
|
* deliberately identical: the two modules now share this one engine.
|
|
24
24
|
*
|
|
25
25
|
* THE MESSAGE TABLE IS THE CONTRACT, not Ajv's `message` field. A keyword
|
|
26
|
-
* whose Ajv error reaches `renderAjvError`
|
|
27
|
-
* `
|
|
28
|
-
* keyword, rather than being papered over with
|
|
29
|
-
* refusal is what stops Ajv wording leaking into a
|
|
30
|
-
* omission (DR-0013 criterion 8).
|
|
26
|
+
* whose Ajv error reaches `renderAjvError` and matches no `case` of that
|
|
27
|
+
* function's switch falls to its `default:` arm, which is a Tiphys DEFECT and
|
|
28
|
+
* is reported as one, naming the keyword, rather than being papered over with
|
|
29
|
+
* Ajv's own sentence. That refusal is what stops Ajv wording leaking into a
|
|
30
|
+
* public contract by omission (DR-0013 criterion 8).
|
|
31
|
+
*
|
|
32
|
+
* THIS PARAGRAPH NAMED THE WRONG MECHANISM UNTIL THE DR-0047 SWEEP (CR-VS-004).
|
|
33
|
+
* It said the refusal came from a `MESSAGE_BY_KEYWORD` set. That set existed,
|
|
34
|
+
* nothing ever read it (three references: its own declaration and two comments
|
|
35
|
+
* saying it was the contract), and its contents disagreed with
|
|
36
|
+
* `AUTHORING_VOCABULARY` in BOTH directions: `$ref`, `items`, `properties` and
|
|
37
|
+
* `then` were in the vocabulary and not in the set, `minimum`, `maximum` and
|
|
38
|
+
* `maxItems` were in the set and not in the vocabulary. A dead constant that a
|
|
39
|
+
* file's own documentation calls the contract is how the next reader edits the
|
|
40
|
+
* thing that does nothing, so it is deleted and this sentence now names the
|
|
41
|
+
* switch, which is what actually refuses.
|
|
31
42
|
*
|
|
32
43
|
* YAML IS INPUT DECODING AND IS A SEPARATE STAGE (DR-0013 YAML clause 3).
|
|
33
44
|
* `decodeDocument` decodes; `validateInstance` validates an already-decoded
|
|
@@ -76,12 +87,34 @@ export const TIPHYS_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
|
76
87
|
/** The document root, in RFC 6901 URI-fragment form. */
|
|
77
88
|
export const ROOT_POINTER = "#";
|
|
78
89
|
/**
|
|
79
|
-
* THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7)
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
90
|
+
* THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7). THIS LIST IS THE SOURCE
|
|
91
|
+
* OF TRUTH. Ajv supplies Draft 2020-12 entire; this is what Tiphys schemas are
|
|
92
|
+
* ALLOWED to use, so a keyword outside it is a deliberate expansion rather than
|
|
93
|
+
* an accident. Every entry has both a positive and a negative test (validator
|
|
94
|
+
* criterion 2), asserted by `test/schemas.test.ts`, which DERIVES its cases
|
|
95
|
+
* from this array rather than from a hand-written list.
|
|
96
|
+
*
|
|
97
|
+
* `schemas/README.md` RENDERS THIS LIST FOR A HUMAN READER AND IS NOT ITSELF
|
|
98
|
+
* THE CONTRACT, and the distinction was paid for (CR-VS-004, FIND-02 of the
|
|
99
|
+
* DR-0047 sweep). The sentence here used to say the vocabulary IS "documented
|
|
100
|
+
* in `schemas/README.md`", which reads as a guarantee that the two agree.
|
|
101
|
+
* Measured at the swept head they did not: this array holds sixteen keywords
|
|
102
|
+
* and that document's table declared fifteen, `uniqueItems` being the missing
|
|
103
|
+
* row, since the day M3-P1 wrote both. Nothing asserted the relation, so no gate
|
|
104
|
+
* could see it.
|
|
105
|
+
*
|
|
106
|
+
* THE DRIFT IS NOW CLOSED, AND BY THE TEST RATHER THAN BY THE ROW (DR-0047
|
|
107
|
+
* sweep round 2). The row was added to `schemas/README.md`, which closes the one
|
|
108
|
+
* instance; what closes the MECHANISM is that test/schemas.test.ts:340 parses
|
|
109
|
+
* that document's keyword column and compares it to this array, so the two
|
|
110
|
+
* halves can no longer disagree silently. The document still does not DECIDE
|
|
111
|
+
* anything: this array is the contract and that one renders it, which is why
|
|
112
|
+
* the comparison is written as an assertion about the document rather than as a
|
|
113
|
+
* generator.
|
|
114
|
+
*
|
|
115
|
+
* AND THE SECOND HALF OF THAT SENTENCE IS GONE. It said every entry "therefore
|
|
116
|
+
* also has an entry in `MESSAGE_BY_KEYWORD`", which was false in both
|
|
117
|
+
* directions and about a constant nothing read; see the module header.
|
|
85
118
|
*/
|
|
86
119
|
export const AUTHORING_VOCABULARY = [
|
|
87
120
|
"$ref",
|
|
@@ -252,29 +285,6 @@ function valueAt(instance, instancePath) {
|
|
|
252
285
|
}
|
|
253
286
|
return node;
|
|
254
287
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Ajv keywords this engine translates. A keyword absent from this map is a
|
|
257
|
-
* defect and says so: see the module header. The map's KEY SET is also the
|
|
258
|
-
* mechanical half of "every keyword in the declared vocabulary has a
|
|
259
|
-
* diagnostic", asserted in `test/schemas.test.ts`.
|
|
260
|
-
*/
|
|
261
|
-
const MESSAGE_BY_KEYWORD = new Set([
|
|
262
|
-
"type",
|
|
263
|
-
"required",
|
|
264
|
-
"additionalProperties",
|
|
265
|
-
"enum",
|
|
266
|
-
"const",
|
|
267
|
-
"minimum",
|
|
268
|
-
"maximum",
|
|
269
|
-
"minItems",
|
|
270
|
-
"maxItems",
|
|
271
|
-
"minLength",
|
|
272
|
-
"pattern",
|
|
273
|
-
"uniqueItems",
|
|
274
|
-
"contains",
|
|
275
|
-
"oneOf",
|
|
276
|
-
"if",
|
|
277
|
-
]);
|
|
278
288
|
/**
|
|
279
289
|
* Translate ONE Ajv error into the Tiphys contract. `undefined` means the
|
|
280
290
|
* error carries no independent information for a reader (Ajv reports a
|