@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,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SHARED EXCLUSION REGISTER (kernel plan M4, M4-P21).
|
|
3
|
+
*
|
|
4
|
+
* `src/lock.ts` states its own exclusion domain honestly at src/lock.ts:63:
|
|
5
|
+
* the lease excludes within ONE filesystem and ONE clock. Two environments
|
|
6
|
+
* that clone one fleet remote each get their own `state/orchestrator.lock`
|
|
7
|
+
* and BOTH acquire it, because `state/` is gitignored (src/fleet.ts:28) so
|
|
8
|
+
* the lease artifact never travels. M4-P20 measured that dangerous state and
|
|
9
|
+
* committed the captures; this module is the second exclusion layer that
|
|
10
|
+
* closes it.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THE REGISTER IS. A compare-and-swap register on a dedicated git ref
|
|
13
|
+
* of the fleet's shared remote. The ref's value is a commit whose only file
|
|
14
|
+
* is `lease.json`; a write is `git push --force-with-lease=<ref>:<exact sha>`
|
|
15
|
+
* and the remote decides the race. M4-D-11 was PROTOTYPE-BLOCKED and M4-P20's
|
|
16
|
+
* probe closed it (delivery/verification/cross-environment-exclusion-probe.md:1).
|
|
17
|
+
* Three of that probe's findings are load-bearing here and are implemented
|
|
18
|
+
* rather than remembered:
|
|
19
|
+
*
|
|
20
|
+
* 1. ONLY `refs/heads/*` IS PUSHABLE. Tags, notes and custom namespaces are
|
|
21
|
+
* refused with HTTP 403 (CLAUDE.md standing warning 14, re-measured for
|
|
22
|
+
* this phase). So the "dedicated ref" is a dedicated BRANCH and the
|
|
23
|
+
* default is `refs/heads/tiphys/lease`, which is visible in branch
|
|
24
|
+
* listings and subject to any `refs/heads/**` ruleset.
|
|
25
|
+
* 2. THE EXPECTATION MUST BE AN EXPLICIT SHA. The bare `--force-with-lease`
|
|
26
|
+
* form takes its expectation from the remote-tracking ref, so a routine
|
|
27
|
+
* fetch re-arms it; the probe measured it CLOBBERING a live holder with
|
|
28
|
+
* exit 0. `casWrite` below never emits the bare form.
|
|
29
|
+
* 3. NONZERO DOES NOT MEAN "I LOST". A transport failure exits 1 too, so
|
|
30
|
+
* the result has THREE states and the indeterminate one is resolved by
|
|
31
|
+
* re-reading the register rather than assumed either way.
|
|
32
|
+
*
|
|
33
|
+
* WHY THE STALENESS SIGNAL IS A COUNTER AND NOT A CLOCK (M4-P21 criterion 6).
|
|
34
|
+
* Two environments bring two clocks and `isExpired` compares a lease
|
|
35
|
+
* timestamp against the local one (src/lock.ts:171). Comparing one
|
|
36
|
+
* environment's wall clock against another's is exactly the measurement this
|
|
37
|
+
* layer must not make. So the register carries a MONOTONIC FENCING COUNTER
|
|
38
|
+
* that every write increments, and a holder is judged stale only when that
|
|
39
|
+
* counter has not moved across a duration measured entirely on the OBSERVING
|
|
40
|
+
* environment's own clock (two readings of one clock, never one reading of
|
|
41
|
+
* two). Where the register is reachable the counter decides and the command
|
|
42
|
+
* says `signal=counter`. Where it is not reachable there is no counter to
|
|
43
|
+
* read, only the local clock, and a clock is not a cross-environment signal:
|
|
44
|
+
* the command says `signal=clock` and REFUSES rather than falling back to
|
|
45
|
+
* local-only exclusion, which is the vacuous green this layer exists to
|
|
46
|
+
* prevent (criterion 7).
|
|
47
|
+
*
|
|
48
|
+
* C-2 (binding): nothing here reads a run identifier of a running program,
|
|
49
|
+
* probes liveness, sends a signal, or reads the kernel's virtual filesystem.
|
|
50
|
+
* Environment identity is a random id written once to a TRACKED fleet file
|
|
51
|
+
* (criterion 3), so it survives a reclaim and travels with a clone of the
|
|
52
|
+
* fleet; exclusion is decided by the register's counter and by git's own
|
|
53
|
+
* compare-and-swap verdict, never by anything about a machine.
|
|
54
|
+
*
|
|
55
|
+
* DECLARATION, NOT INFERENCE (criterion 1). The layer is entered only when
|
|
56
|
+
* the fleet home's own `package.json` declares it. With the field absent
|
|
57
|
+
* every function here returns "absent" before any subprocess is spawned, so
|
|
58
|
+
* a fleet that cannot reach a remote is not forced to switch the layer off
|
|
59
|
+
* globally and today's behaviour is unchanged for everyone else.
|
|
60
|
+
*/
|
|
61
|
+
/** The dotted path of the opt-in field inside the fleet home's package.json. */
|
|
62
|
+
export declare const SHARED_EXCLUSION_FIELD = "tiphys.sharedExclusion";
|
|
63
|
+
/** The remote a declaration defaults to. */
|
|
64
|
+
export declare const DEFAULT_SHARED_REMOTE = "origin";
|
|
65
|
+
/**
|
|
66
|
+
* The register ref a declaration defaults to. A BRANCH, deliberately: see
|
|
67
|
+
* finding 1 in this file's header. The plan's prose names `refs/tiphys/lease`
|
|
68
|
+
* and that namespace is refused by the shared remote this kernel is built
|
|
69
|
+
* against, so the default is the pushable form and the ref stays
|
|
70
|
+
* configurable for a remote with different rules.
|
|
71
|
+
*/
|
|
72
|
+
export declare const DEFAULT_SHARED_REF = "refs/heads/tiphys/lease";
|
|
73
|
+
/** The file inside the register commit that carries the lease document. */
|
|
74
|
+
export declare const REGISTER_DOCUMENT_NAME = "lease.json";
|
|
75
|
+
/**
|
|
76
|
+
* The TRACKED fleet file carrying this environment's identity (criterion 3).
|
|
77
|
+
* It sits at the fleet root, outside the gitignored set at src/fleet.ts:28,
|
|
78
|
+
* so an environment that commits it keeps its identity across a reclaim and
|
|
79
|
+
* a clone of that commit reads the same id.
|
|
80
|
+
*/
|
|
81
|
+
export declare const ENVIRONMENT_ID_FILE = "tiphys-environment.json";
|
|
82
|
+
/**
|
|
83
|
+
* Where this environment records what it last saw in the register. It lives
|
|
84
|
+
* under the gitignored `state/` prefix ON PURPOSE: it is a measurement taken
|
|
85
|
+
* on THIS environment's clock and it must never travel, or the duration it
|
|
86
|
+
* carries would be compared against a clock that did not produce it.
|
|
87
|
+
*/
|
|
88
|
+
export declare const OBSERVATION_FILE: string;
|
|
89
|
+
/** How long the counter must stand still before a holder is judged stale. */
|
|
90
|
+
export declare const DEFAULT_STALE_WINDOW_SECONDS = 900;
|
|
91
|
+
export interface SharedExclusionConfig {
|
|
92
|
+
remote: string;
|
|
93
|
+
ref: string;
|
|
94
|
+
staleWindowSeconds: number;
|
|
95
|
+
}
|
|
96
|
+
export type SharedExclusionDeclaration = {
|
|
97
|
+
kind: "absent";
|
|
98
|
+
} | {
|
|
99
|
+
kind: "declared";
|
|
100
|
+
config: SharedExclusionConfig;
|
|
101
|
+
} | {
|
|
102
|
+
kind: "invalid";
|
|
103
|
+
reason: string;
|
|
104
|
+
};
|
|
105
|
+
/** The register's value: one lease document per register commit. */
|
|
106
|
+
export interface SharedLeaseDocument {
|
|
107
|
+
state: "held" | "free";
|
|
108
|
+
envId: string;
|
|
109
|
+
counter: number;
|
|
110
|
+
acquiredAt: string;
|
|
111
|
+
expiresAt: string;
|
|
112
|
+
durationSeconds: number;
|
|
113
|
+
ref: string;
|
|
114
|
+
}
|
|
115
|
+
export type RegisterRead = {
|
|
116
|
+
kind: "absent";
|
|
117
|
+
} | {
|
|
118
|
+
kind: "present";
|
|
119
|
+
sha: string;
|
|
120
|
+
document: SharedLeaseDocument;
|
|
121
|
+
} | {
|
|
122
|
+
kind: "corrupt";
|
|
123
|
+
sha: string;
|
|
124
|
+
reason: string;
|
|
125
|
+
} | {
|
|
126
|
+
kind: "unreachable";
|
|
127
|
+
reason: string;
|
|
128
|
+
};
|
|
129
|
+
export type CasOutcome = {
|
|
130
|
+
kind: "won";
|
|
131
|
+
sha: string;
|
|
132
|
+
} | {
|
|
133
|
+
kind: "lost";
|
|
134
|
+
reason: string;
|
|
135
|
+
} | {
|
|
136
|
+
kind: "indeterminate";
|
|
137
|
+
reason: string;
|
|
138
|
+
};
|
|
139
|
+
/** What this environment last saw, timed on this environment's own clock. */
|
|
140
|
+
export interface RegisterObservation {
|
|
141
|
+
sha: string;
|
|
142
|
+
counter: number;
|
|
143
|
+
firstSeenMs: number;
|
|
144
|
+
}
|
|
145
|
+
/** Which of the two signals decided a shared-exclusion verdict. */
|
|
146
|
+
export type ExclusionSignal = "counter" | "clock";
|
|
147
|
+
export type SharedPreflight = {
|
|
148
|
+
kind: "proceed";
|
|
149
|
+
signal: ExclusionSignal;
|
|
150
|
+
expectedSha: string;
|
|
151
|
+
nextCounter: number;
|
|
152
|
+
takingOver: boolean;
|
|
153
|
+
line: string;
|
|
154
|
+
/** The document the register currently holds, when it holds one. */
|
|
155
|
+
current?: SharedLeaseDocument;
|
|
156
|
+
} | {
|
|
157
|
+
kind: "refused";
|
|
158
|
+
signal: ExclusionSignal;
|
|
159
|
+
line: string;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Read the fleet home's declaration. Absent means the layer is off and no
|
|
163
|
+
* subprocess is spawned anywhere below; `false` is the same answer written
|
|
164
|
+
* explicitly. A field that is present and unreadable is INVALID rather than
|
|
165
|
+
* absent, because silently treating a typo as "off" is the shape that makes
|
|
166
|
+
* a guard green everywhere and protective nowhere.
|
|
167
|
+
*/
|
|
168
|
+
export declare function readSharedExclusion(fleetRoot: string): SharedExclusionDeclaration;
|
|
169
|
+
/** The fleet root a lock path belongs to: <root>/state/orchestrator.lock. */
|
|
170
|
+
export declare function fleetRootForLockPath(lockPath: string): string;
|
|
171
|
+
export interface EnvironmentIdentity {
|
|
172
|
+
envId: string;
|
|
173
|
+
/** True when this call generated the id rather than reading one. */
|
|
174
|
+
generated: boolean;
|
|
175
|
+
path: string;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Read the environment id, or generate one and write it. The id is random
|
|
179
|
+
* and is generated EXACTLY ONCE per fleet home: every later call reads the
|
|
180
|
+
* file. Nothing about the machine enters it.
|
|
181
|
+
*/
|
|
182
|
+
export declare function ensureEnvironmentId(fleetRoot: string): EnvironmentIdentity;
|
|
183
|
+
/**
|
|
184
|
+
* THE COMMAND-SCOPED IDENTITY THE REGISTER COMMIT IS WRITTEN UNDER.
|
|
185
|
+
*
|
|
186
|
+
* `git commit-tree` REFUSES without an author, and CI runners carry no git
|
|
187
|
+
* identity (CLAUDE.md standing warning 5), so a register write that relied
|
|
188
|
+
* on ambient configuration would work on a developer's machine and fail on
|
|
189
|
+
* every runner. These are the same two strings `tiphys init` already uses
|
|
190
|
+
* for the fleet bootstrap commit (EXT-F-02 option B): set as command-scoped
|
|
191
|
+
* environment variables on the invocation only, never written to user or
|
|
192
|
+
* global git configuration. They are repeated here rather than imported
|
|
193
|
+
* because `src/commands/init.ts` imports THIS module for the opt-in field,
|
|
194
|
+
* and a test pins the two copies equal so a drift reddens instead of
|
|
195
|
+
* surfacing as a runner-only failure.
|
|
196
|
+
*/
|
|
197
|
+
export declare const REGISTER_IDENTITY_NAME = "Tiphys Fleet";
|
|
198
|
+
export declare const REGISTER_IDENTITY_EMAIL = "fleet@tiphys.invalid";
|
|
199
|
+
/**
|
|
200
|
+
* The line of a git stderr block carrying git's OWN rejection marker.
|
|
201
|
+
*
|
|
202
|
+
* NOT "the first line", and M4-P20 paid for the difference: with
|
|
203
|
+
* `push.negotiate` true, git 2.43.0 emits a negotiation warning as the first
|
|
204
|
+
* stderr line of EVERY file-transport push, accepted and refused alike, so a
|
|
205
|
+
* signature taken from the first line cannot tell accept from refuse. The
|
|
206
|
+
* marker below is git's own text, never this module's, so the refusal
|
|
207
|
+
* signature stays captured rather than hand-written (T-003).
|
|
208
|
+
*/
|
|
209
|
+
export declare function rejectionLine(stderr: string): string | undefined;
|
|
210
|
+
export declare function renderRegisterDocument(document: SharedLeaseDocument): string;
|
|
211
|
+
/**
|
|
212
|
+
* Read the register. `ls-remote` establishes the value the compare-and-swap
|
|
213
|
+
* will be armed against, then the object is fetched so the document can be
|
|
214
|
+
* read. A failure at either step is UNREACHABLE, never "absent": the two
|
|
215
|
+
* must not be conflated, because "absent" means "nobody holds the lease" and
|
|
216
|
+
* would license an acquire.
|
|
217
|
+
*/
|
|
218
|
+
export declare function readRegister(fleetRoot: string, config: SharedExclusionConfig): RegisterRead;
|
|
219
|
+
/**
|
|
220
|
+
* One compare-and-swap write. `expectedSha` is the value the caller read;
|
|
221
|
+
* the empty string means "I expect the register to be absent". The EXACT-SHA
|
|
222
|
+
* form is the only form emitted (finding 2 in this file's header).
|
|
223
|
+
*/
|
|
224
|
+
export declare function casWrite(fleetRoot: string, config: SharedExclusionConfig, expectedSha: string, document: SharedLeaseDocument): CasOutcome;
|
|
225
|
+
export declare function readObservation(fleetRoot: string): RegisterObservation | undefined;
|
|
226
|
+
export declare function writeObservation(fleetRoot: string, observation: RegisterObservation): void;
|
|
227
|
+
export interface CounterJudgement {
|
|
228
|
+
stale: boolean;
|
|
229
|
+
/** How long the counter has stood still, on THIS environment's clock. */
|
|
230
|
+
unchangedForMs: number;
|
|
231
|
+
observation: RegisterObservation;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Judge a holder by the FENCING COUNTER, using two readings of ONE clock.
|
|
235
|
+
*
|
|
236
|
+
* A holder is stale only when the register's sha and counter are the same
|
|
237
|
+
* ones this environment first saw at `firstSeenMs` and that much of ITS OWN
|
|
238
|
+
* time has passed. Any advance of the counter resets the measurement, which
|
|
239
|
+
* is what makes a renewing holder safe no matter how far its clock is from
|
|
240
|
+
* this one: the renewal is visible as an increment, and an increment is not
|
|
241
|
+
* a timestamp.
|
|
242
|
+
*/
|
|
243
|
+
export declare function judgeByCounter(previous: RegisterObservation | undefined, sha: string, counter: number, nowMs: number, staleWindowMs: number): CounterJudgement;
|
|
244
|
+
export type SharedIntent = "acquire" | "renew" | "release";
|
|
245
|
+
export interface SharedPreflightInput {
|
|
246
|
+
fleetRoot: string;
|
|
247
|
+
config: SharedExclusionConfig;
|
|
248
|
+
envId: string;
|
|
249
|
+
intent: SharedIntent;
|
|
250
|
+
takeover: boolean;
|
|
251
|
+
nowMs: number;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Decide whether the caller may proceed, WITHOUT touching anything. Every
|
|
255
|
+
* refusal here happens before the local lease file is created, which is what
|
|
256
|
+
* makes criterion 7's fail-closed assertion observable: an unreachable
|
|
257
|
+
* register leaves no local lock behind.
|
|
258
|
+
*/
|
|
259
|
+
export declare function preflightShared(input: SharedPreflightInput): SharedPreflight;
|
|
260
|
+
/**
|
|
261
|
+
* The one line a register write that was NOT won reports. It lives here
|
|
262
|
+
* rather than at the call site in `src/lock.ts` because it names the
|
|
263
|
+
* staleness basis, and the C-2 structural inspection over that file
|
|
264
|
+
* (test/lock.test.ts:534) forbids that vocabulary there; keeping the
|
|
265
|
+
* sentence in one place also means a reader sees the same wording whichever
|
|
266
|
+
* mutation lost.
|
|
267
|
+
*/
|
|
268
|
+
export declare function casFailureLine(ref: string, outcome: CasOutcome): string;
|
|
269
|
+
/** Build the document a won preflight should publish. */
|
|
270
|
+
export declare function buildRegisterDocument(input: {
|
|
271
|
+
state: "held" | "free";
|
|
272
|
+
envId: string;
|
|
273
|
+
counter: number;
|
|
274
|
+
nowMs: number;
|
|
275
|
+
durationSeconds: number;
|
|
276
|
+
ref: string;
|
|
277
|
+
acquiredAt?: string;
|
|
278
|
+
}): SharedLeaseDocument;
|
|
279
|
+
/**
|
|
280
|
+
* THE FOUR STATUSES, AS A CLOSED SET (M4-P22 criterion 1).
|
|
281
|
+
*
|
|
282
|
+
* `doctor` prints exactly one of them and a test compares the printed
|
|
283
|
+
* leading token against this array, so a fifth status cannot be added by
|
|
284
|
+
* writing a new sentence somewhere: it has to be added here, where the
|
|
285
|
+
* comparison sees it.
|
|
286
|
+
*
|
|
287
|
+
* THE FOURTH IS NEVER PASS. That is the DR-0038 shape reused rather than
|
|
288
|
+
* reinvented: a check whose question could not be asked reports a third
|
|
289
|
+
* state instead of being forced into a binary, because an unreachable
|
|
290
|
+
* register reported as `free` would license exactly the second live
|
|
291
|
+
* orchestrator this layer exists to refuse
|
|
292
|
+
* (delivery/decisions/DR-0038-the-declared-single-family-review-exception.md:1).
|
|
293
|
+
*/
|
|
294
|
+
export declare const SHARED_LOCK_STATUS_TOKENS: readonly ["not-declared", "free", "held", "unreachable"];
|
|
295
|
+
export type SharedLockStatusToken = (typeof SHARED_LOCK_STATUS_TOKENS)[number];
|
|
296
|
+
export type SharedLockStatus = {
|
|
297
|
+
token: "not-declared";
|
|
298
|
+
text: string;
|
|
299
|
+
} | {
|
|
300
|
+
token: "free";
|
|
301
|
+
text: string;
|
|
302
|
+
} | {
|
|
303
|
+
token: "held";
|
|
304
|
+
envId: string;
|
|
305
|
+
expiresAt: string;
|
|
306
|
+
text: string;
|
|
307
|
+
} | {
|
|
308
|
+
token: "unreachable";
|
|
309
|
+
reason: string;
|
|
310
|
+
text: string;
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* A PURE READ of this environment's identity: it never generates one.
|
|
314
|
+
*
|
|
315
|
+
* `ensureEnvironmentId` above writes a file when none is there, which is
|
|
316
|
+
* right for `lock acquire`, the command that legitimately enters the layer
|
|
317
|
+
* and is about to publish a register document. It is wrong for a GUARD: a
|
|
318
|
+
* refusal must create nothing, and `doctor` must diagnose a fleet without
|
|
319
|
+
* changing it. An absent identity file therefore reads as "this environment
|
|
320
|
+
* is not the one the register names", which is the fail-closed answer.
|
|
321
|
+
*/
|
|
322
|
+
export declare function readEnvironmentId(fleetRoot: string): string | undefined;
|
|
323
|
+
/**
|
|
324
|
+
* The register's state as one of the four statuses, WITHOUT writing an
|
|
325
|
+
* observation and WITHOUT generating an identity.
|
|
326
|
+
*
|
|
327
|
+
* `preflightShared` is deliberately not used here even though it answers a
|
|
328
|
+
* similar question, because it WRITES `state/shared-lease.observed.json` as
|
|
329
|
+
* part of judging staleness. A diagnosis that moves the thing it diagnoses
|
|
330
|
+
* is not a diagnosis, and the staleness judgement is not wanted here anyway:
|
|
331
|
+
* see `guardSharedRegister` below for why a stale holder still refuses.
|
|
332
|
+
*
|
|
333
|
+
* It DOES fetch, because `readRegister` fetches: the register object has to
|
|
334
|
+
* be local before its document can be read. That touches `.git/FETCH_HEAD`
|
|
335
|
+
* in the fleet home and nothing else, and it is stated here rather than left
|
|
336
|
+
* for a reader to discover.
|
|
337
|
+
*
|
|
338
|
+
* TWO CONDITIONS COLLAPSE INTO `unreachable` AND BOTH ARE NAMED IN THE
|
|
339
|
+
* REASON. A declaration that is present and unusable, and a register whose
|
|
340
|
+
* document does not parse, are not `free` and cannot be rendered as
|
|
341
|
+
* `held <envId> until <t>` because neither yields an envId or an expiry. The
|
|
342
|
+
* criterion's set is closed at four, so they take the one status that means
|
|
343
|
+
* "this question could not be answered", and the reason says which of them
|
|
344
|
+
* it was.
|
|
345
|
+
*/
|
|
346
|
+
export declare function sharedLockStatus(fleetRoot: string): SharedLockStatus;
|
|
347
|
+
/**
|
|
348
|
+
* What a task-mutating command is allowed to do, given the register.
|
|
349
|
+
*
|
|
350
|
+
* `off` is the fleet that never opted in, and it is returned before any
|
|
351
|
+
* subprocess is spawned, so nothing about today's behaviour changes for a
|
|
352
|
+
* fleet home with no declaration.
|
|
353
|
+
*/
|
|
354
|
+
export type SharedMutationVerdict = {
|
|
355
|
+
kind: "off";
|
|
356
|
+
} | {
|
|
357
|
+
kind: "allowed";
|
|
358
|
+
status: SharedLockStatus;
|
|
359
|
+
} | {
|
|
360
|
+
kind: "refused";
|
|
361
|
+
reason: string;
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* THE CROSS-ENVIRONMENT HALF OF THE HOLDERSHIP GUARD (M4-P22 criteria 2
|
|
365
|
+
* and 3).
|
|
366
|
+
*
|
|
367
|
+
* `checkHoldership` (src/task.ts:439) answers "does THIS process hold THIS
|
|
368
|
+
* filesystem's lease". That question is answered entirely inside one fleet
|
|
369
|
+
* home, and src/lock.ts:63 says so: the local lease excludes within one
|
|
370
|
+
* filesystem and one clock. So in the state this function exists for, the
|
|
371
|
+
* local lease held by THIS environment and the shared register naming
|
|
372
|
+
* ANOTHER one, the old guard is GREEN and the fleet has two orchestrators
|
|
373
|
+
* mutating one set of tasks. That is the dangerous state, and a test that
|
|
374
|
+
* holds neither lease is green without this function and proves nothing.
|
|
375
|
+
*
|
|
376
|
+
* WHY A STALE HOLDER STILL REFUSES. `judgeByCounter` exists so that a lease
|
|
377
|
+
* whose fencing counter has stood still can be taken over, and that takeover
|
|
378
|
+
* is a LEASE operation: `tiphys lock acquire --take-over` advances the
|
|
379
|
+
* counter under the taking-over environment's id, and only then does the
|
|
380
|
+
* register name this environment. Reading staleness here instead would give
|
|
381
|
+
* `spawn` and `teardown` their own opinion about who holds the fleet, which
|
|
382
|
+
* is a second verdict about one lease from a second place, and it would have
|
|
383
|
+
* to write the observation file to reach it. The refusal names the command
|
|
384
|
+
* that resolves it, so the remedy is reachable rather than merely correct.
|
|
385
|
+
*
|
|
386
|
+
* THE REASON IS ONE LINE, and the callers hand it straight to the same
|
|
387
|
+
* single-reason path every other refusal uses.
|
|
388
|
+
*/
|
|
389
|
+
export declare function guardSharedRegister(fleetRoot: string, command: string): SharedMutationVerdict;
|