@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,597 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { EX_USAGE } from "../cli.js";
|
|
5
|
+
import { loadFleet } from "../fleet.js";
|
|
6
|
+
import { pathsIdentifySameObject } from "../path-identity.js";
|
|
7
|
+
import { TASK_ID_PATTERN, poolList } from "../pool.js";
|
|
8
|
+
import { readTaskMeta, singleLine } from "../task.js";
|
|
9
|
+
import { landedness } from "../teardown.js";
|
|
10
|
+
/**
|
|
11
|
+
* THE STOP CONDITION, COMPUTED RATHER THAN JUDGED (kernel plan M4, M4-P24
|
|
12
|
+
* criteria 1 to 4; M4-D-10 decided in delivery/plan/kernel-plan-m4.md:3211).
|
|
13
|
+
*
|
|
14
|
+
* WHY A COMMAND AND NOT A RULE. "The orchestrator does not decide when it is
|
|
15
|
+
* finished" has three recorded violations behind it, and every one of them was
|
|
16
|
+
* a JUDGMENT presented as a status report. A rule that depends on remembering
|
|
17
|
+
* does not survive a busy session; the answer this project keeps arriving at
|
|
18
|
+
* is a mechanism whose nonzero exit is a fact nobody can report their way
|
|
19
|
+
* around (CLAUDE.md, "The orchestrator does not decide when it is finished").
|
|
20
|
+
*
|
|
21
|
+
* THE EXIT CODE IS THREE-VALUED ON PURPOSE (criterion 1). `EXIT_WORK_REMAINS`
|
|
22
|
+
* is distinct from 0 AND from 1, so a caller can tell "work remains" from "the
|
|
23
|
+
* command failed". Collapsing them would make a crashing stop condition look
|
|
24
|
+
* exactly like a busy fleet, which is the T-008 shape one level up: a guard
|
|
25
|
+
* whose failure is indistinguishable from its red.
|
|
26
|
+
*
|
|
27
|
+
* WHAT CANNOT BE ESTABLISHED IS COUNTED AS WORK, NEVER AS EMPTY. A category
|
|
28
|
+
* this command failed to read is reported in `unknown` and holds the exit code
|
|
29
|
+
* at `EXIT_WORK_REMAINS`. T-036 is the measured instance of the opposite
|
|
30
|
+
* choice: a stop condition that silently treated an unreadable source as zero
|
|
31
|
+
* printed a finished milestone with six phases unbuilt
|
|
32
|
+
* (delivery/tuition/T-036-the-stop-condition-reported-a-finished-milestone.md:1).
|
|
33
|
+
*
|
|
34
|
+
* AND THAT PROPERTY HOLDS PER CANDIDATE, NOT ONLY PER CATEGORY (round 1). The
|
|
35
|
+
* first version of this file enforced it at CATEGORY granularity: the three
|
|
36
|
+
* `readdirSync` failures were recorded and every per-ROW drop inside a
|
|
37
|
+
* category was silent. The mechanism that reopens is this one sentence: A
|
|
38
|
+
* CATEGORY THAT IS EMPTY BY CONSTRUCTION IS REPORTED AS A CATEGORY THAT IS
|
|
39
|
+
* EMPTY BY OBSERVATION. Four measured instances, all of them exit 0 with
|
|
40
|
+
* `in flight: 0` and `unknown: 0`: a task whose meta.json did not read, a
|
|
41
|
+
* branch that exists only under `refs/remotes/`, a project clone reached
|
|
42
|
+
* through a symlink, and a clone whose `origin/HEAD` points at a feature
|
|
43
|
+
* branch. So every site below that removes a CANDIDATE from the walk either
|
|
44
|
+
* records it in `unknown` or carries a written reason why its absence is
|
|
45
|
+
* established rather than merely unobserved; the enumeration of all fourteen
|
|
46
|
+
* such sites is in delivery/work-history/m4-p24.md:1051.
|
|
47
|
+
*
|
|
48
|
+
* AND THE T-036 QUESTION IS ANSWERED IN THE CANNOT-SEE BLOCK RATHER THAN
|
|
49
|
+
* HIDDEN. Every in-flight category below derives from an artifact that exists
|
|
50
|
+
* only AFTER the work has started: a task record is written at dispatch, a
|
|
51
|
+
* pool record at pool create, a branch at branch create. So an empty report
|
|
52
|
+
* says "nothing is in flight", which is not the same sentence as "nothing is
|
|
53
|
+
* left to do", and `CANNOT_SEE` says so by name instead of leaving the reader
|
|
54
|
+
* to infer it. A fleet home has no artifact that names work before anyone
|
|
55
|
+
* starts it, so this is a reported limit and not a fixable one here.
|
|
56
|
+
*
|
|
57
|
+
* NO ABSOLUTE PATH LITERAL APPEARS IN THIS FILE (criterion 2). Every directory
|
|
58
|
+
* is derived from `loadFleet(process.cwd())` (src/fleet.ts:82). The script this
|
|
59
|
+
* command retires hard-codes one session's scratchpad path, which is why that
|
|
60
|
+
* is a criterion rather than a note.
|
|
61
|
+
*
|
|
62
|
+
* NO NETWORK (criterion 4). Nothing here fetches, and that is a property to
|
|
63
|
+
* keep rather than an omission: a stop condition that degrades to a shorter
|
|
64
|
+
* answer when the network is gone is indistinguishable from one reporting a
|
|
65
|
+
* quiet system, which is the shape CLAUDE.md standing warning 6 records for
|
|
66
|
+
* watchers. The cannot-see block and the exit code are therefore functions of
|
|
67
|
+
* the fleet home alone.
|
|
68
|
+
*/
|
|
69
|
+
const USAGE = "usage: tiphys next";
|
|
70
|
+
/**
|
|
71
|
+
* Work remains. DISTINCT FROM 1, which is the command failing, and from 0,
|
|
72
|
+
* which is every in-flight category empty (criterion 1).
|
|
73
|
+
*/
|
|
74
|
+
export const EXIT_WORK_REMAINS = 3;
|
|
75
|
+
/**
|
|
76
|
+
* WHAT THIS COMMAND CANNOT SEE, as a NAMED LIST (criterion 4).
|
|
77
|
+
*
|
|
78
|
+
* Printed unconditionally. It is not derived from a probe and it does not
|
|
79
|
+
* shorten when something is unreachable, because a list that shrinks when the
|
|
80
|
+
* network is gone tells the reader that fewer things are unknown at exactly
|
|
81
|
+
* the moment more of them are.
|
|
82
|
+
*/
|
|
83
|
+
export const CANNOT_SEE = [
|
|
84
|
+
"open pull requests, and whether any of them is waiting on a review",
|
|
85
|
+
"CI conclusions on any head, on either the pull-request arm or the push arm",
|
|
86
|
+
"post-merge push runs on the new main head (T-009: a pull-request green is not evidence for the push arm)",
|
|
87
|
+
"work nobody has started: every in-flight category above is a trace that work leaves BEHIND, so an empty report cannot tell 'nothing left to do' from 'nothing started' (T-036)",
|
|
88
|
+
"whether a project's base ref is still its default branch: the base is read from the clone's own origin/HEAD, which git writes once at clone time and never refreshes, so a default-branch rename upstream leaves every existing clone pointing at a branch that is no longer the default until someone runs git remote set-head. The base actually used is printed under 'base refs' above, so the judgement can be checked rather than trusted",
|
|
89
|
+
];
|
|
90
|
+
/** The heading the cannot-see block is printed under. */
|
|
91
|
+
export const CANNOT_SEE_HEADING = "cannot see (this command has no network; an absence here is not evidence of absence):";
|
|
92
|
+
/**
|
|
93
|
+
* One local git invocation in `cwd`. `-C` is how git is told where to
|
|
94
|
+
* resolve, which is NOT where this process is standing (standing warning 9),
|
|
95
|
+
* and `cwd` always arrives derived from the fleet home.
|
|
96
|
+
*/
|
|
97
|
+
function runGit(cwd, args) {
|
|
98
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
|
|
99
|
+
return {
|
|
100
|
+
status: result.status ?? -1,
|
|
101
|
+
stdout: result.stdout ?? "",
|
|
102
|
+
stderr: result.stderr ?? "",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* THE DELIVERED-ELSEWHERE PREDICATE (criterion 3).
|
|
107
|
+
*
|
|
108
|
+
* `git branch --merged` reports a squash-merged branch as UNMERGED, because
|
|
109
|
+
* the squash commit's sha is not the branch tip and the branch tip is not an
|
|
110
|
+
* ancestor of the base. This process squash-merges every phase, so under the
|
|
111
|
+
* naive implementation EVERY delivered branch reads as open forever and the
|
|
112
|
+
* stop condition never goes green.
|
|
113
|
+
*
|
|
114
|
+
* THE CITATION THAT STOOD HERE UNTIL ROUND 1 WAS WRONG, AND IT WAS WRONG IN
|
|
115
|
+
* THE SILENT DIRECTION. It read `delivery/STATE.md:54`, which resolves, which
|
|
116
|
+
* is why no gate reddened; the line it resolves to is about two merge blockers
|
|
117
|
+
* and says nothing about `git branch --merged`. Measured:
|
|
118
|
+
* `grep -n 'branch --merged' delivery/STATE.md` returns nothing, at this head
|
|
119
|
+
* and at the merge base, so the sentence is absent rather than moved. The
|
|
120
|
+
* claim itself is measured in this repository and the measurement is the
|
|
121
|
+
* citation now: witness/captures/next-delivered-elsewhere-git.txt records
|
|
122
|
+
* `git branch --merged main` listing neither the squash-merged branch nor the
|
|
123
|
+
* one delivered through another pull request. The same wrong citation is in
|
|
124
|
+
* the plan at delivery/plan/kernel-plan-m4.md:3239 and in this phase's work
|
|
125
|
+
* history; the work history is corrected, and the plan is an owner-approved
|
|
126
|
+
* document this phase does not edit, so it is raised rather than changed.
|
|
127
|
+
*
|
|
128
|
+
* THREE ARMS, AND THE THIRD IS WHY THIS IS NOT JUST A CALL TO `landedness`.
|
|
129
|
+
*
|
|
130
|
+
* 1. ANCESTOR. An ordinary merge or a fast-forward. `landedness` arm one.
|
|
131
|
+
* 2. CONTENT. Merging the branch into the base changes no tree, so the base
|
|
132
|
+
* already carries everything the branch did however it got there. This is
|
|
133
|
+
* MEMBER A, the squash merge, and it is `landedness` arm two.
|
|
134
|
+
* 3. PATCH EQUIVALENCE. Every commit the branch carries has an equivalent
|
|
135
|
+
* patch already upstream, found by patch id rather than by sha. This is
|
|
136
|
+
* MEMBER B: commits that landed inside ANOTHER branch's pull request, so
|
|
137
|
+
* they appear on the base interleaved with commits the branch never had.
|
|
138
|
+
*
|
|
139
|
+
* ARM 3 IS NOT REDUNDANT WITH ARM 2, and the case that separates them is the
|
|
140
|
+
* ordinary one. Once the base moves on and edits the same region again, a
|
|
141
|
+
* three-way merge of the delivered branch CONFLICTS, so arm 2 answers `open`
|
|
142
|
+
* for a branch whose every commit is demonstrably already upstream. Arm 3
|
|
143
|
+
* answers it by patch id, which survives the later edit.
|
|
144
|
+
*
|
|
145
|
+
* ARM 2 IS NOT REDUNDANT WITH ARM 3 EITHER. A squash collapses N commits into
|
|
146
|
+
* one, whose patch id is the id of the COMBINED diff and therefore equals no
|
|
147
|
+
* individual commit's. For any branch of more than one commit, arm 3 sees only
|
|
148
|
+
* unmatched commits and answers `open`.
|
|
149
|
+
*
|
|
150
|
+
* REUSE, NOT A SECOND IMPLEMENTATION (T-005). Arms 1 and 2 are
|
|
151
|
+
* `landedness` at src/teardown.ts:123, the predicate `tiphys teardown` already
|
|
152
|
+
* refuses on. A second copy here would be a second thing to keep true.
|
|
153
|
+
*
|
|
154
|
+
* INCONCLUSIVE IS NOT `open` AND IT IS NOT `delivered`. A git failure returns
|
|
155
|
+
* `unknown`, which the caller counts as work remaining, because the one answer
|
|
156
|
+
* this predicate must never invent is a confident one.
|
|
157
|
+
*/
|
|
158
|
+
export function deliveredElsewhere(contextDir, branchRef, baseRef) {
|
|
159
|
+
const landed = landedness(contextDir, branchRef, baseRef);
|
|
160
|
+
if (landed.kind === "landed") {
|
|
161
|
+
return { kind: "delivered", how: landed.how };
|
|
162
|
+
}
|
|
163
|
+
if (landed.kind === "inconclusive") {
|
|
164
|
+
return { kind: "unknown", detail: landed.detail };
|
|
165
|
+
}
|
|
166
|
+
return patchEquivalence(contextDir, branchRef, baseRef);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Arm 3. `git cherry <base> <branch>` prints one line per commit the branch
|
|
170
|
+
* carries that the base does not carry BY SHA, marked `-` when an equivalent
|
|
171
|
+
* patch is already upstream and `+` when it is not. Every line `-` means the
|
|
172
|
+
* base already has every patch this branch introduces.
|
|
173
|
+
*/
|
|
174
|
+
function patchEquivalence(contextDir, branchRef, baseRef) {
|
|
175
|
+
const cherry = runGit(contextDir, ["cherry", baseRef, branchRef]);
|
|
176
|
+
if (cherry.status !== 0) {
|
|
177
|
+
return {
|
|
178
|
+
kind: "unknown",
|
|
179
|
+
detail: `git cherry exited ${String(cherry.status)}: ${singleLine(cherry.stderr)}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const marks = cherry.stdout
|
|
183
|
+
.split("\n")
|
|
184
|
+
.map((line) => line.trim())
|
|
185
|
+
.filter((line) => line !== "");
|
|
186
|
+
if (marks.length === 0) {
|
|
187
|
+
// A branch with nothing ahead of the base is an ancestor of it, so arm 1
|
|
188
|
+
// should already have answered. Reaching here means the two readings
|
|
189
|
+
// disagree, which is reported rather than resolved by guessing.
|
|
190
|
+
return {
|
|
191
|
+
kind: "unknown",
|
|
192
|
+
detail: `git cherry reports no commit ahead of ${baseRef} while ` +
|
|
193
|
+
`merge-base --is-ancestor reports ${branchRef} is not an ancestor of it`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return marks.every((line) => line.startsWith("-"))
|
|
197
|
+
? { kind: "delivered", how: "patch-equivalent" }
|
|
198
|
+
: { kind: "open" };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The line a task whose record did not read is reported as. Named rather than
|
|
202
|
+
* inlined so that the one edit which reopens this defect, dropping the push,
|
|
203
|
+
* is a single contiguous statement a witness can mutate without disturbing
|
|
204
|
+
* anything else in the walk.
|
|
205
|
+
*/
|
|
206
|
+
function unreadableTaskRecord(id) {
|
|
207
|
+
return (`task ${id}: tasks/${id}/meta.json did not read as a task record ` +
|
|
208
|
+
`(absent, not a regular file, not parseable, or missing a required ` +
|
|
209
|
+
`field: src/task.ts:381 returns the same value for all four), so this ` +
|
|
210
|
+
`task's status is not established and it is not evidence that the task ` +
|
|
211
|
+
`finished`);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Open tasks: `tasks/<id>/meta.json` carrying status `open`.
|
|
215
|
+
*
|
|
216
|
+
* THE UNREADABLE RECORD IS THE DANGEROUS ONE, NOT THE MISSING DIRECTORY. The
|
|
217
|
+
* first version of this walk recorded an item only when the record read AND
|
|
218
|
+
* said `open`, with no else arm, so a task directory whose meta.json did not
|
|
219
|
+
* read landed in neither `items` nor `unknown`. Measured before the fix, with
|
|
220
|
+
* one task directory and a truncated meta.json: `in flight: 0`, `unknown: 0`,
|
|
221
|
+
* `NOTHING IS IN FLIGHT`, exit 0. A process killed mid-write is the ordinary
|
|
222
|
+
* failure this repository keeps meeting, so that is T-036's own mechanism
|
|
223
|
+
* inside the command written to prevent T-036.
|
|
224
|
+
*
|
|
225
|
+
* FOUR CAUSES COLLAPSE INTO ONE VALUE AND THE MESSAGE SAYS SO. `readTaskMeta`
|
|
226
|
+
* (src/task.ts:381) returns `undefined` for an absent file, a file that is not
|
|
227
|
+
* regular or could not be read, a body that does not parse, and a body that
|
|
228
|
+
* parses and fails the field check. This walk does not re-read the file to
|
|
229
|
+
* tell them apart: a second reader of a task record is a second thing to keep
|
|
230
|
+
* true (T-005), and every one of the four means the same thing here, which is
|
|
231
|
+
* that the task's status is NOT ESTABLISHED. The reported line names all four
|
|
232
|
+
* rather than implying a diagnosis it did not make.
|
|
233
|
+
*/
|
|
234
|
+
function openTasks(fleet, report) {
|
|
235
|
+
let entries;
|
|
236
|
+
try {
|
|
237
|
+
entries = readdirSync(fleet.tasksDir, { withFileTypes: true });
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
report.unknown.push(`tasks/ could not be listed: ${singleLine(String(error))}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
244
|
+
const id = entry.name;
|
|
245
|
+
if (!TASK_ID_PATTERN.test(id)) {
|
|
246
|
+
// A DIRECTORY here is task-shaped storage whose name this command
|
|
247
|
+
// cannot parse, so it is reported rather than dropped. A FILE is not:
|
|
248
|
+
// `tiphys init` itself writes `tasks/.gitkeep`, and a fleet home that
|
|
249
|
+
// reported its own scaffolding as unknown could never exit 0. That is
|
|
250
|
+
// the line between an absence this walk established and one it merely
|
|
251
|
+
// did not look at.
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
report.unknown.push(`tasks/${id}: a directory under tasks/ whose name is not a task id, ` +
|
|
254
|
+
`so this command cannot read a task record from it and its status ` +
|
|
255
|
+
`is not established`);
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const meta = readTaskMeta(fleet, id);
|
|
260
|
+
if (meta === undefined) {
|
|
261
|
+
report.unknown.push(unreadableTaskRecord(id));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (meta.status === "open") {
|
|
265
|
+
report.items.push({ category: "task", what: `${id} (open)` });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Pool entries: a worktree, or an open task whose record did not survive. */
|
|
270
|
+
function poolEntries(fleet, report) {
|
|
271
|
+
let entries;
|
|
272
|
+
try {
|
|
273
|
+
entries = poolList(fleet);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
report.unknown.push(`the pool could not be listed: ${singleLine(String(error))}`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
report.items.push({
|
|
281
|
+
category: "worktree",
|
|
282
|
+
what: `${entry.taskId} (${entry.origin}, head ${entry.headSha})`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The base ref a project clone's branches are judged against: whatever
|
|
288
|
+
* `origin/HEAD` points at, then `origin/main`, then a local `main`. A clone
|
|
289
|
+
* that answers none of them is UNKNOWN, never assumed, because a guessed base
|
|
290
|
+
* sends the delivered-elsewhere predicate at the wrong ref (src/pool.ts:369
|
|
291
|
+
* records the same refusal for the same reason).
|
|
292
|
+
*
|
|
293
|
+
* `origin/HEAD` CAN BE STALE AND THIS FUNCTION CANNOT TELL (round 1). Git
|
|
294
|
+
* writes it once at clone time and never refreshes it, so a default-branch
|
|
295
|
+
* rename upstream leaves it pointing at a branch that is no longer the
|
|
296
|
+
* default, and a clone made while a feature branch was checked out upstream
|
|
297
|
+
* points there from birth. Measured before round 1's fix: a clone whose
|
|
298
|
+
* `origin/HEAD` pointed at `feat-open` judged every branch against
|
|
299
|
+
* `feat-open`, found nothing open, and exited 0 while `git cherry
|
|
300
|
+
* refs/remotes/origin/main refs/remotes/origin/feat-open` printed a commit on
|
|
301
|
+
* no other ref.
|
|
302
|
+
*
|
|
303
|
+
* WHAT ROUND 1 DID AND DID NOT DO ABOUT IT, STATED EXACTLY, BECAUSE THE
|
|
304
|
+
* OBVIOUS FIX DOES NOT WORK AND WAS MEASURED NOT TO.
|
|
305
|
+
*
|
|
306
|
+
* It did not make this function RESOLVE the stale case. I did not find a
|
|
307
|
+
* signal inside a clone that establishes the upstream default branch: the
|
|
308
|
+
* refresh (`git remote set-head origin --auto`) is a fetch, and the local
|
|
309
|
+
* candidates I tried (`remote.origin.fetch`, `branch.<name>.remote`, a
|
|
310
|
+
* preference for `origin/main`) either say nothing about the default or
|
|
311
|
+
* replace an inherited guess with a typed one, which is the refusal
|
|
312
|
+
* src/pool.ts:369 argues against. Whether such a signal exists is an open
|
|
313
|
+
* question here, not a settled impossibility.
|
|
314
|
+
*
|
|
315
|
+
* AND THE BRANCH WALK DOES NOT RESCUE IT EITHER, WHICH I FOUND BY RUNNING IT.
|
|
316
|
+
* The reasoning that it would went: with `refs/remotes/` now walked, the real
|
|
317
|
+
* default branch is judged against the feature branch and comes back open.
|
|
318
|
+
* Measured against a clone whose `origin/HEAD` pointed at `feat-open`, with
|
|
319
|
+
* `feat-open` one commit AHEAD of `main`: `refs/remotes/origin/main` is an
|
|
320
|
+
* ANCESTOR of the base, so it is reported delivered, and the command exited 0
|
|
321
|
+
* with `in flight: 0`. The rescue only happens when the true default has
|
|
322
|
+
* DIVERGED from the mis-pointed base, which is one shape of the fault and not
|
|
323
|
+
* the common one.
|
|
324
|
+
*
|
|
325
|
+
* SO THE FIX IS DISCLOSURE PLUS A RECORDED DOUBT, AND BOTH ARE MECHANICAL.
|
|
326
|
+
* `how` is returned and printed, so the base a judgement used is in the
|
|
327
|
+
* output instead of being invisible. And when `origin/HEAD` points somewhere
|
|
328
|
+
* that is NEITHER `origin/main` NOR `origin/master` WHILE ONE OF THOSE TWO
|
|
329
|
+
* EXISTS IN THE SAME CLONE, two available signals DISAGREE, and the
|
|
330
|
+
* disagreement goes into `unknown`, which holds the exit at
|
|
331
|
+
* `EXIT_WORK_REMAINS`.
|
|
332
|
+
*
|
|
333
|
+
* THE DIFFERENCE BETWEEN THIS AND THE GUESS src/pool.ts:369 FORBIDS IS THE
|
|
334
|
+
* DIRECTION. That refusal is about FILLING an unresolved field from a
|
|
335
|
+
* plausible name, which produces a confident wrong answer. Nothing here is
|
|
336
|
+
* filled: the base stays whatever `origin/HEAD` said, and a name-shaped
|
|
337
|
+
* observation is used only to RAISE A DOUBT, whose worst case is a project
|
|
338
|
+
* whose genuine default is neither of those two names while a stale `main`
|
|
339
|
+
* survives beside it. That project is reported unknown and never exits 0
|
|
340
|
+
* until someone looks, which is a false positive in the one direction this
|
|
341
|
+
* command is built to fail in.
|
|
342
|
+
*/
|
|
343
|
+
const CONVENTIONAL_DEFAULTS = ["refs/remotes/origin/main", "refs/remotes/origin/master"];
|
|
344
|
+
export function baseRefOf(projectDir) {
|
|
345
|
+
const head = runGit(projectDir, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
|
|
346
|
+
const pointed = head.stdout.trim();
|
|
347
|
+
if (head.status === 0 && pointed !== "") {
|
|
348
|
+
if (!CONVENTIONAL_DEFAULTS.includes(pointed)) {
|
|
349
|
+
for (const rival of CONVENTIONAL_DEFAULTS) {
|
|
350
|
+
const verified = runGit(projectDir, ["rev-parse", "--verify", "--quiet", rival]);
|
|
351
|
+
if (verified.status === 0 && verified.stdout.trim() !== "") {
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
ref: pointed,
|
|
355
|
+
how: "origin/HEAD",
|
|
356
|
+
doubt: `origin/HEAD points at ${pointed} while ${rival} also exists in ` +
|
|
357
|
+
`this clone, so two signals disagree about which branch is the ` +
|
|
358
|
+
`default and every delivery judgement below rests on the one ` +
|
|
359
|
+
`git wrote at clone time and never refreshed. Settle it with ` +
|
|
360
|
+
`git remote set-head origin --auto, which needs the network ` +
|
|
361
|
+
`this command does not use`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { ok: true, ref: pointed, how: "origin/HEAD" };
|
|
367
|
+
}
|
|
368
|
+
for (const candidate of ["refs/remotes/origin/main", "refs/heads/main"]) {
|
|
369
|
+
const verified = runGit(projectDir, ["rev-parse", "--verify", "--quiet", candidate]);
|
|
370
|
+
if (verified.status === 0 && verified.stdout.trim() !== "") {
|
|
371
|
+
return { ok: true, ref: candidate, how: `${candidate} resolves` };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: "neither origin/HEAD nor origin/main nor a local main resolves, so there is " +
|
|
377
|
+
"no base ref to judge delivery against",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Undelivered branches in every project clone under `projects/`, LOCAL AND
|
|
382
|
+
* REMOTE-TRACKING ALIKE.
|
|
383
|
+
*
|
|
384
|
+
* WHY REMOTE-TRACKING REFS ARE WALKED (round 1). The first version listed
|
|
385
|
+
* `refs/heads/` only. A clone is what `tiphys init` and `tiphys resume`
|
|
386
|
+
* produce, and in a fresh clone every branch except the checked-out one exists
|
|
387
|
+
* ONLY under `refs/remotes/`, so the normal state of a project was a branch
|
|
388
|
+
* category that was empty by construction and reported as empty by
|
|
389
|
+
* observation. Measured before the fix, against a clone of an upstream
|
|
390
|
+
* carrying one undelivered branch: `in flight: 0`, `unknown: 0`, `NOTHING IS
|
|
391
|
+
* IN FLIGHT`, exit 0.
|
|
392
|
+
*
|
|
393
|
+
* THREE EXCLUSIONS, AND EACH IS AN ESTABLISHED ABSENCE RATHER THAN A DROP.
|
|
394
|
+
*
|
|
395
|
+
* - THE BASE REF ITSELF. Judging the base against the base answers nothing.
|
|
396
|
+
* - A SYMBOLIC REF. `refs/remotes/origin/HEAD` is a POINTER at another ref
|
|
397
|
+
* in the same walk, so its target is judged on its own line and counting
|
|
398
|
+
* it twice would report one branch as two. Detected from
|
|
399
|
+
* `%(symref)` rather than by matching the name `HEAD`, because the name is
|
|
400
|
+
* a convention and the field is the fact.
|
|
401
|
+
* - A REF AT AN OBJECT ALREADY JUDGED. `refs/heads/feat` and
|
|
402
|
+
* `refs/remotes/origin/feat` at the same sha are one piece of work seen
|
|
403
|
+
* twice. Deduplicated by object name, which is what makes them the same
|
|
404
|
+
* commit; when the two disagree they have different object names and both
|
|
405
|
+
* are judged, which is correct, because a local branch ahead of its remote
|
|
406
|
+
* is a different in-flight state from the remote's.
|
|
407
|
+
*/
|
|
408
|
+
function undeliveredBranches(fleet, report) {
|
|
409
|
+
let projects;
|
|
410
|
+
try {
|
|
411
|
+
// A SYMLINK IS A PROJECT AND `Dirent.isDirectory()` SAYS IT IS NOT. The
|
|
412
|
+
// dirent reflects an lstat, so a project clone reached through a symlink
|
|
413
|
+
// under `projects/` was filtered out of the walk entirely, and every
|
|
414
|
+
// branch in it, local or remote, was invisible. Measured before the fix:
|
|
415
|
+
// `projects/demo -> .../elsewhere/demo`, one undelivered branch inside,
|
|
416
|
+
// `in flight: 0`, `unknown: 0`, exit 0. `statSync` FOLLOWS, which is the
|
|
417
|
+
// question being asked, and an entry that resolves to no directory is
|
|
418
|
+
// reported rather than skipped: `projects/` is empty on a fresh fleet
|
|
419
|
+
// home (measured: `tiphys init` creates it with nothing in it), so
|
|
420
|
+
// nothing legitimate lands in `unknown` for it.
|
|
421
|
+
projects = readdirSync(fleet.projectsDir, { withFileTypes: true })
|
|
422
|
+
.map((entry) => entry.name)
|
|
423
|
+
.sort();
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
report.unknown.push(`projects/ could not be listed: ${singleLine(String(error))}`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
for (const project of projects) {
|
|
430
|
+
const dir = join(fleet.projectsDir, project);
|
|
431
|
+
let isDir;
|
|
432
|
+
try {
|
|
433
|
+
isDir = statSync(dir).isDirectory();
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
report.unknown.push(`project ${project}: projects/${project} could not be resolved ` +
|
|
437
|
+
`(${singleLine(String(error))}), so no branch in it can be judged ` +
|
|
438
|
+
`delivered or open`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (!isDir) {
|
|
442
|
+
report.unknown.push(`project ${project}: projects/${project} does not resolve to a ` +
|
|
443
|
+
`directory, so no branch in it can be judged delivered or open`);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
// THE DIRECTORY MUST BE ITS OWN REPOSITORY, AND THIS CHECK IS NOT
|
|
447
|
+
// CEREMONY. A fleet home IS a git repository, so `git -C projects/foo`
|
|
448
|
+
// inside a directory that is not a clone resolves against the FLEET's
|
|
449
|
+
// repository instead of failing: `refs/heads/main` verifies, the branch
|
|
450
|
+
// walk lists the fleet's own branches, and a project nobody could read is
|
|
451
|
+
// reported as having nothing in flight. Measured while writing this
|
|
452
|
+
// phase's own test, which is why it is a refusal rather than a comment.
|
|
453
|
+
// Compared with `pathsIdentifySameObject` because git canonicalises the
|
|
454
|
+
// path it prints and a string comparison answers no for two spellings of
|
|
455
|
+
// one directory (src/path-identity.ts:5).
|
|
456
|
+
const toplevel = runGit(dir, ["rev-parse", "--show-toplevel"]);
|
|
457
|
+
if (toplevel.status !== 0 || !pathsIdentifySameObject(toplevel.stdout.trim(), dir)) {
|
|
458
|
+
report.unknown.push(`project ${project}: not the top level of its own git repository ` +
|
|
459
|
+
`(git rev-parse --show-toplevel exited ${String(toplevel.status)} and ` +
|
|
460
|
+
`reported ${singleLine(toplevel.stdout) || "nothing"}), so no branch ` +
|
|
461
|
+
`in it can be judged delivered or open`);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const base = baseRefOf(dir);
|
|
465
|
+
if (!base.ok) {
|
|
466
|
+
report.unknown.push(`project ${project}: ${base.reason}`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
report.notes.push(`project ${project}: branches judged against ${base.ref} (chosen by ${base.how})`);
|
|
470
|
+
if (base.doubt !== undefined) {
|
|
471
|
+
report.unknown.push(`project ${project}: ${base.doubt}`);
|
|
472
|
+
}
|
|
473
|
+
const listed = runGit(dir, [
|
|
474
|
+
"for-each-ref",
|
|
475
|
+
"--format=%(objectname) %(refname) %(symref)",
|
|
476
|
+
"refs/heads/",
|
|
477
|
+
"refs/remotes/",
|
|
478
|
+
]);
|
|
479
|
+
if (listed.status !== 0) {
|
|
480
|
+
report.unknown.push(`project ${project}: branches could not be listed: ${singleLine(listed.stderr)}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const judgedObjects = new Set();
|
|
484
|
+
for (const line of listed.stdout.split("\n")) {
|
|
485
|
+
const fields = line.trim().split(/\s+/);
|
|
486
|
+
const objectname = fields[0] ?? "";
|
|
487
|
+
const ref = fields[1] ?? "";
|
|
488
|
+
const symref = fields[2] ?? "";
|
|
489
|
+
if (objectname === "" || ref === "") {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (symref !== "" || ref === base.ref || judgedObjects.has(objectname)) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
judgedObjects.add(objectname);
|
|
496
|
+
const delivery = deliveredElsewhere(dir, ref, base.ref);
|
|
497
|
+
if (delivery.kind === "open") {
|
|
498
|
+
report.items.push({ category: "branch", what: `${ref} in project ${project}` });
|
|
499
|
+
}
|
|
500
|
+
else if (delivery.kind === "unknown") {
|
|
501
|
+
report.unknown.push(`project ${project}: ${ref}: ${delivery.detail}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Every in-flight category in a fleet home, in dependency order: the task
|
|
508
|
+
* record, the checkout it owns, and the branch that outlives both.
|
|
509
|
+
*/
|
|
510
|
+
export function collectInFlight(fleet) {
|
|
511
|
+
const report = { items: [], unknown: [], notes: [] };
|
|
512
|
+
openTasks(fleet, report);
|
|
513
|
+
poolEntries(fleet, report);
|
|
514
|
+
undeliveredBranches(fleet, report);
|
|
515
|
+
return report;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* THE ONE NEXT ACTION (criterion 1). Exactly one, chosen by the first
|
|
519
|
+
* non-empty category in the order above, so the answer is derived from the
|
|
520
|
+
* report rather than picked.
|
|
521
|
+
*/
|
|
522
|
+
export function nextAction(report) {
|
|
523
|
+
const unread = report.unknown[0];
|
|
524
|
+
if (unread !== undefined) {
|
|
525
|
+
return `MEASURE the category this command could not read: ${unread}`;
|
|
526
|
+
}
|
|
527
|
+
const task = report.items.find((item) => item.category === "task");
|
|
528
|
+
if (task !== undefined) {
|
|
529
|
+
return `CLOSE OR REPORT task ${task.what}, the first open task in this fleet home`;
|
|
530
|
+
}
|
|
531
|
+
const worktree = report.items.find((item) => item.category === "worktree");
|
|
532
|
+
if (worktree !== undefined) {
|
|
533
|
+
return `TEAR DOWN ${worktree.what}, which still holds a checkout`;
|
|
534
|
+
}
|
|
535
|
+
const branch = report.items.find((item) => item.category === "branch");
|
|
536
|
+
if (branch !== undefined) {
|
|
537
|
+
return `DELIVER OR RETIRE ${branch.what}, whose commits are not on its base by any route`;
|
|
538
|
+
}
|
|
539
|
+
return ("NOTHING IS IN FLIGHT in this fleet home. That is not the same sentence as " +
|
|
540
|
+
"'the work is done'; read the cannot-see list above before concluding it");
|
|
541
|
+
}
|
|
542
|
+
/** The cannot-see block. Unconditional, and it probes nothing to build. */
|
|
543
|
+
export function cannotSeeBlock() {
|
|
544
|
+
const lines = [CANNOT_SEE_HEADING];
|
|
545
|
+
for (const item of CANNOT_SEE) {
|
|
546
|
+
lines.push(` - ${item}`);
|
|
547
|
+
}
|
|
548
|
+
return lines;
|
|
549
|
+
}
|
|
550
|
+
/** The whole report, as lines, with exactly one `next action:` line. */
|
|
551
|
+
export function renderReport(fleet, report) {
|
|
552
|
+
const lines = [`fleet ${fleet.root}`];
|
|
553
|
+
lines.push(`in flight: ${String(report.items.length)}`);
|
|
554
|
+
for (const item of report.items) {
|
|
555
|
+
lines.push(` ${item.category} ${item.what}`);
|
|
556
|
+
}
|
|
557
|
+
lines.push(`unknown: ${String(report.unknown.length)}`);
|
|
558
|
+
for (const detail of report.unknown) {
|
|
559
|
+
lines.push(` ${detail}`);
|
|
560
|
+
}
|
|
561
|
+
// DISCLOSURES, NOT WORK. Printed between the two counted categories and the
|
|
562
|
+
// cannot-see block because that is where a reader decides whether to believe
|
|
563
|
+
// the counts: the base ref a project's branches were judged against is the
|
|
564
|
+
// input the delivered-elsewhere predicate cannot check for itself.
|
|
565
|
+
lines.push(`base refs: ${String(report.notes.length)}`);
|
|
566
|
+
for (const note of report.notes) {
|
|
567
|
+
lines.push(` ${note}`);
|
|
568
|
+
}
|
|
569
|
+
lines.push(...cannotSeeBlock());
|
|
570
|
+
lines.push(`next action: ${nextAction(report)}`);
|
|
571
|
+
return lines;
|
|
572
|
+
}
|
|
573
|
+
/** Work remains whenever anything is in flight OR anything could not be read. */
|
|
574
|
+
export function exitCodeFor(report) {
|
|
575
|
+
return report.items.length + report.unknown.length > 0 ? EXIT_WORK_REMAINS : 0;
|
|
576
|
+
}
|
|
577
|
+
export function cmdNext(args) {
|
|
578
|
+
if (args.length > 0) {
|
|
579
|
+
process.stderr.write(`${USAGE}\n`);
|
|
580
|
+
return EX_USAGE;
|
|
581
|
+
}
|
|
582
|
+
let fleet;
|
|
583
|
+
try {
|
|
584
|
+
fleet = loadFleet(process.cwd());
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
process.stderr.write(`tiphys next: ${singleLine(error.message)}\n`);
|
|
588
|
+
return 1;
|
|
589
|
+
}
|
|
590
|
+
const report = collectInFlight(fleet);
|
|
591
|
+
const code = exitCodeFor(report);
|
|
592
|
+
const lines = renderReport(fleet, report);
|
|
593
|
+
lines.push(`exit ${String(code)} (0 means every in-flight category is empty, ` +
|
|
594
|
+
`${String(EXIT_WORK_REMAINS)} means work remains, 1 means this command failed)`);
|
|
595
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
596
|
+
return code;
|
|
597
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tiphys plan project --phase-id <id> [--plan <file>] [--out <dir>]`
|
|
3
|
+
* (kernel plan M3, M3-P1 step 2; D-M3-18).
|
|
4
|
+
*
|
|
5
|
+
* Emits the M2-P4 scope auditor's phase declaration from the plan, so the
|
|
6
|
+
* auditor's input becomes a GENERATED VIEW of one source instead of a second
|
|
7
|
+
* hand-authored source that can drift.
|
|
8
|
+
*
|
|
9
|
+
* The output path is `<out>/<phase-id-lowercased>.json`, defaulting to
|
|
10
|
+
* `delivery/plan/phase-declarations`, which is the directory
|
|
11
|
+
* `gates.manifest.json` passes to the auditor as `--declarations`.
|
|
12
|
+
*
|
|
13
|
+
* With no `--out` the document is written; with `--stdout` it is printed and
|
|
14
|
+
* nothing is written, which is what a test or a reviewer wants when checking
|
|
15
|
+
* WHAT would be emitted without touching the tree.
|
|
16
|
+
*
|
|
17
|
+
* Every path this command touches is operator-supplied and is classified
|
|
18
|
+
* before it is opened or written (D-M3-27).
|
|
19
|
+
*/
|
|
20
|
+
export declare function cmdPlan(argv: string[]): number;
|