@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { runReleaseGate } from "./release.js";
|
|
4
|
+
/**
|
|
5
|
+
* THE MIGRATIONS GATE ENTRY (kernel plan M2, M2-P7 step 4). A thin entry
|
|
6
|
+
* point that names the declared `migrations` verification and calls the one
|
|
7
|
+
* contract module, `src/gates/release.ts`. Deploy verification and
|
|
8
|
+
* migration verification are two INSTANCES of one post-merge verification
|
|
9
|
+
* contract, not two contracts (DR-0014, investigation section 1): the two
|
|
10
|
+
* registry entries exist so that a project can declare one and not the
|
|
11
|
+
* other, and so the evidence bundle shows two lines rather than one
|
|
12
|
+
* aggregate that hides which half was skipped.
|
|
13
|
+
*
|
|
14
|
+
* The two inventories are not symmetric (plan criterion 8): a declared
|
|
15
|
+
* REPOSITORY migrations location containing zero migrations is
|
|
16
|
+
* not-applicable, but a non-empty repository inventory with an EMPTY
|
|
17
|
+
* applied inventory is red, never not-applicable, because that is the
|
|
18
|
+
* recorded incident exactly ("migrations skipped by a flake while the code
|
|
19
|
+
* deployed anyway"). The asymmetry lives in the migrations-command adapter;
|
|
20
|
+
* this entry is shape-identical to deploy's.
|
|
21
|
+
*/
|
|
22
|
+
function isMain() {
|
|
23
|
+
const argv1 = process.argv[1];
|
|
24
|
+
if (argv1 === undefined) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(import.meta.url));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (isMain()) {
|
|
35
|
+
process.exitCode = await runReleaseGate("migrations", process.argv.slice(2));
|
|
36
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RUN PINNING (kernel plan M2, M2-P1 step 3; M2-C-5; M2-D-06).
|
|
3
|
+
*
|
|
4
|
+
* T-004 lesson 3: "a test run is only evidence if you can prove what it
|
|
5
|
+
* ran". In that incident no artifact recorded the content of the source
|
|
6
|
+
* files during a failing run, so a forensic reconstruction was the only way
|
|
7
|
+
* to establish that the tree had been rewritten underneath the run, and the
|
|
8
|
+
* root cause was never awarded.
|
|
9
|
+
*
|
|
10
|
+
* A pin is that artifact: the file set of the tree that was actually
|
|
11
|
+
* executed, with sha256, size and mtimeMs per file, taken at the start and
|
|
12
|
+
* at the end of the run. ANY difference in ANY of the four fields (the
|
|
13
|
+
* path's presence included) is a difference and makes the run's record
|
|
14
|
+
* `error`.
|
|
15
|
+
*
|
|
16
|
+
* WHY ctimeMs IS PINNED TOO (CR-809, added in fix round 1). mtime is
|
|
17
|
+
* SETTABLE FROM USERSPACE. `cp -p`, `rsync -a` and `tar -x` all restore it
|
|
18
|
+
* through `utimensat` at nanosecond precision, so a rewrite performed by any
|
|
19
|
+
* of those three ordinary commands passes a pin of {sha256, size, mtimeMs}
|
|
20
|
+
* without a mark, and T-004's incident is a rewrite. ctime is the inode
|
|
21
|
+
* change time: no userspace call sets it, and `utimensat` BUMPS it, so the
|
|
22
|
+
* very act of restoring mtime is what makes the change visible. It also
|
|
23
|
+
* catches replace-by-rename, which changes the inode.
|
|
24
|
+
*
|
|
25
|
+
* This is a strict ADDITION to M2-C-5's four fields, never a substitution:
|
|
26
|
+
* the constraint says any difference in file set, sha256, size or mtime makes
|
|
27
|
+
* the record `error`, and pinning a fifth field can only make more runs
|
|
28
|
+
* `error`, never fewer. M2-P2 criterion 7 and M2-P3 criterion 8 are pin
|
|
29
|
+
* witnesses and should be read against five fields, not four.
|
|
30
|
+
*
|
|
31
|
+
* The residue, stated rather than left to be discovered: ctime is not
|
|
32
|
+
* forgeable from userspace, but it is not a cryptographic seal either. A
|
|
33
|
+
* privileged actor with raw device access can write any inode field it
|
|
34
|
+
* likes. That is outside anything this kernel can measure.
|
|
35
|
+
*
|
|
36
|
+
* WHY mtimeMs IS PINNED AND A CONTENT HASH IS NOT ENOUGH. T-004's forensics
|
|
37
|
+
* describe a BYTE-IDENTICAL rewrite: `src/lock.ts` was replaced with
|
|
38
|
+
* pristine content 42.8 seconds into the failing run. A content-only pin
|
|
39
|
+
* passes that unchanged, and the whole value of the pin would be lost at
|
|
40
|
+
* precisely the shape the incident actually had. M2-P2 criterion 7 turns
|
|
41
|
+
* that into a witness rather than an assertion.
|
|
42
|
+
*
|
|
43
|
+
* M2-C-6 IS OBEYED BY REUSE, NOT BY REIMPLEMENTATION. The walk reads paths
|
|
44
|
+
* it did not create, so every open is preceded by the DELIVERED
|
|
45
|
+
* `classifyEntry` from src/task.ts. There is no second copy of "may this
|
|
46
|
+
* path be opened" here: T-005 records that this project paid for that class
|
|
47
|
+
* twice because the second component reimplemented the mechanism. The one
|
|
48
|
+
* `readFileSync` in this module sits on the line after the probe, and it is
|
|
49
|
+
* the only read of an externally supplied path anywhere in `src/gates/` that
|
|
50
|
+
* is not `readRegularFileIfPresent`, which returns UTF-8 text and therefore
|
|
51
|
+
* cannot hash a binary file honestly.
|
|
52
|
+
*
|
|
53
|
+
* `lstatSync` and `statSync` here are PROBES, not opens: they are the same
|
|
54
|
+
* two syscalls `classifyEntry` itself performs, they answer questions about
|
|
55
|
+
* the directory entry, and neither can block on a named pipe. They are used
|
|
56
|
+
* only to decide directory-ness and to read size and mtime, never to decide
|
|
57
|
+
* whether a path may be opened, which stays the delivered helper's job.
|
|
58
|
+
*
|
|
59
|
+
* FAIL CLOSED ON ANYTHING THAT IS NOT A REGULAR FILE OR A REAL DIRECTORY.
|
|
60
|
+
* A named pipe, socket or device node inside a pinned root makes `takePin`
|
|
61
|
+
* THROW with a reason naming the path and the observed type. It never opens
|
|
62
|
+
* the path, so it never blocks, and the caller turns the throw into an
|
|
63
|
+
* `error` record. Directory symlinks are deliberately not followed: a pin
|
|
64
|
+
* that follows them can be made to walk outside its declared roots or to
|
|
65
|
+
* loop, and neither is a measurement.
|
|
66
|
+
*/
|
|
67
|
+
export interface PinFile {
|
|
68
|
+
/** `join(root, ...)` for the root exactly as the caller supplied it. */
|
|
69
|
+
path: string;
|
|
70
|
+
sha256: string;
|
|
71
|
+
size: number;
|
|
72
|
+
mtimeMs: number;
|
|
73
|
+
/** Inode change time. Not settable from userspace; see the header. */
|
|
74
|
+
ctimeMs: number;
|
|
75
|
+
}
|
|
76
|
+
export interface Pin {
|
|
77
|
+
roots: string[];
|
|
78
|
+
takenAt: string;
|
|
79
|
+
/**
|
|
80
|
+
* `files.length`, carried explicitly so a vacuous pin is visible in the
|
|
81
|
+
* RECORD and not only to a caller who thinks to check (CR-804). Nothing in
|
|
82
|
+
* the shape used to distinguish "the tree did not change" from "no tree was
|
|
83
|
+
* measured", at the module two other phases consume as primary evidence.
|
|
84
|
+
*/
|
|
85
|
+
fileCount: number;
|
|
86
|
+
files: PinFile[];
|
|
87
|
+
}
|
|
88
|
+
export type PinFieldName = "sha256" | "size" | "mtimeMs" | "ctimeMs";
|
|
89
|
+
export type PinDifference = {
|
|
90
|
+
path: string;
|
|
91
|
+
kind: "added";
|
|
92
|
+
} | {
|
|
93
|
+
path: string;
|
|
94
|
+
kind: "removed";
|
|
95
|
+
} | {
|
|
96
|
+
path: string;
|
|
97
|
+
kind: "changed";
|
|
98
|
+
fields: PinFieldName[];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Pin the given roots. Throws with a reason naming the path and the observed
|
|
102
|
+
* type when a root, or anything under it, is neither a regular file nor a
|
|
103
|
+
* real directory (M2-C-6, fail closed). Callers wrap this with `runStep`
|
|
104
|
+
* from src/task.ts and report `error`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function takePin(roots: string[]): Pin;
|
|
107
|
+
/**
|
|
108
|
+
* Every difference between two pins, in a deterministic order (by path).
|
|
109
|
+
* A byte-identical rewrite that changes only mtimeMs is a difference, which
|
|
110
|
+
* is the whole reason mtimeMs is in the record.
|
|
111
|
+
*/
|
|
112
|
+
export declare function comparePins(a: Pin, b: Pin): PinDifference[];
|
|
113
|
+
/** One line per difference, for a record's detail text. */
|
|
114
|
+
export declare function describePinDifference(difference: PinDifference): string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { classifyEntry } from "../task.js";
|
|
5
|
+
function hashFile(path) {
|
|
6
|
+
// THE PROBE, then the open. The order is the point (MECHANISMS.md, CR-520).
|
|
7
|
+
const entry = classifyEntry(path);
|
|
8
|
+
if (entry.kind !== "regular") {
|
|
9
|
+
throw new Error(entry.kind === "absent" || entry.kind === "dangling"
|
|
10
|
+
? `${path} vanished while the pin was being taken`
|
|
11
|
+
: entry.reason);
|
|
12
|
+
}
|
|
13
|
+
const stats = statSync(path);
|
|
14
|
+
const body = readFileSync(path);
|
|
15
|
+
return {
|
|
16
|
+
path,
|
|
17
|
+
sha256: createHash("sha256").update(body).digest("hex"),
|
|
18
|
+
size: stats.size,
|
|
19
|
+
mtimeMs: stats.mtimeMs,
|
|
20
|
+
ctimeMs: stats.ctimeMs,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True only for a REAL directory, never for a symlink that resolves to one.
|
|
25
|
+
* lstat answers about the link itself, so a symlinked directory falls
|
|
26
|
+
* through to the regular-file probe and is refused there.
|
|
27
|
+
*/
|
|
28
|
+
function isRealDirectory(path) {
|
|
29
|
+
try {
|
|
30
|
+
return lstatSync(path).isDirectory();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function walk(root, dir, into) {
|
|
37
|
+
const names = readdirSync(dir).sort();
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const path = join(dir, name);
|
|
40
|
+
if (isRealDirectory(path)) {
|
|
41
|
+
walk(root, path, into);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const entry = classifyEntry(path);
|
|
45
|
+
if (entry.kind === "absent" || entry.kind === "dangling") {
|
|
46
|
+
// Raced away, or a dangling symlink: nothing to hash, and nothing
|
|
47
|
+
// hidden either, because a later pin reports it as added.
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (entry.kind !== "regular") {
|
|
51
|
+
throw new Error(`${entry.reason}; refusing to pin ${root}`);
|
|
52
|
+
}
|
|
53
|
+
into.push(hashFile(path));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pin the given roots. Throws with a reason naming the path and the observed
|
|
58
|
+
* type when a root, or anything under it, is neither a regular file nor a
|
|
59
|
+
* real directory (M2-C-6, fail closed). Callers wrap this with `runStep`
|
|
60
|
+
* from src/task.ts and report `error`.
|
|
61
|
+
*/
|
|
62
|
+
export function takePin(roots) {
|
|
63
|
+
const files = [];
|
|
64
|
+
if (roots.length === 0) {
|
|
65
|
+
throw new Error("a pin over no roots measures nothing (M2-C-5)");
|
|
66
|
+
}
|
|
67
|
+
for (const root of roots) {
|
|
68
|
+
const before = files.length;
|
|
69
|
+
if (isRealDirectory(root)) {
|
|
70
|
+
walk(root, root, files);
|
|
71
|
+
// THE VACUITY FLOOR (CR-804). A root a caller DECLARED that holds no
|
|
72
|
+
// files is a configuration error, not a measurement of an unchanged
|
|
73
|
+
// tree, and the two are indistinguishable in the result. M2-P2 pins
|
|
74
|
+
// "the clone's source and test roots"; if a scratch clone puts sources
|
|
75
|
+
// one directory deeper than the computed root, a silent empty pin
|
|
76
|
+
// reports no difference and the gate goes green on evidence nobody
|
|
77
|
+
// took. The rule lives HERE, in the module that carries the mechanism,
|
|
78
|
+
// rather than in each consumer (T-005).
|
|
79
|
+
if (files.length === before) {
|
|
80
|
+
throw new Error(`pin root ${root} contributed no files; a declared root that holds nothing is a configuration error, not an unchanged tree (M2-C-5)`);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const entry = classifyEntry(root);
|
|
85
|
+
if (entry.kind === "absent" || entry.kind === "dangling") {
|
|
86
|
+
throw new Error(`pin root ${root} does not exist`);
|
|
87
|
+
}
|
|
88
|
+
if (entry.kind === "regular") {
|
|
89
|
+
files.push(hashFile(root));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`${entry.reason}; refusing to pin ${root}`);
|
|
93
|
+
}
|
|
94
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
95
|
+
return {
|
|
96
|
+
roots: [...roots],
|
|
97
|
+
takenAt: new Date().toISOString(),
|
|
98
|
+
fileCount: files.length,
|
|
99
|
+
files,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Every difference between two pins, in a deterministic order (by path).
|
|
104
|
+
* A byte-identical rewrite that changes only mtimeMs is a difference, which
|
|
105
|
+
* is the whole reason mtimeMs is in the record.
|
|
106
|
+
*/
|
|
107
|
+
export function comparePins(a, b) {
|
|
108
|
+
const before = new Map(a.files.map((f) => [f.path, f]));
|
|
109
|
+
const after = new Map(b.files.map((f) => [f.path, f]));
|
|
110
|
+
const paths = [...new Set([...before.keys(), ...after.keys()])].sort();
|
|
111
|
+
const differences = [];
|
|
112
|
+
for (const path of paths) {
|
|
113
|
+
const start = before.get(path);
|
|
114
|
+
const end = after.get(path);
|
|
115
|
+
if (start === undefined && end !== undefined) {
|
|
116
|
+
differences.push({ path, kind: "added" });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (start !== undefined && end === undefined) {
|
|
120
|
+
differences.push({ path, kind: "removed" });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (start === undefined || end === undefined) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const fields = [];
|
|
127
|
+
if (start.sha256 !== end.sha256) {
|
|
128
|
+
fields.push("sha256");
|
|
129
|
+
}
|
|
130
|
+
if (start.size !== end.size) {
|
|
131
|
+
fields.push("size");
|
|
132
|
+
}
|
|
133
|
+
if (start.mtimeMs !== end.mtimeMs) {
|
|
134
|
+
fields.push("mtimeMs");
|
|
135
|
+
}
|
|
136
|
+
if (start.ctimeMs !== end.ctimeMs) {
|
|
137
|
+
fields.push("ctimeMs");
|
|
138
|
+
}
|
|
139
|
+
if (fields.length > 0) {
|
|
140
|
+
differences.push({ path, kind: "changed", fields });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return differences;
|
|
144
|
+
}
|
|
145
|
+
/** One line per difference, for a record's detail text. */
|
|
146
|
+
export function describePinDifference(difference) {
|
|
147
|
+
if (difference.kind === "added") {
|
|
148
|
+
return `${difference.path} was added during the run`;
|
|
149
|
+
}
|
|
150
|
+
if (difference.kind === "removed") {
|
|
151
|
+
return `${difference.path} was removed during the run`;
|
|
152
|
+
}
|
|
153
|
+
return `${difference.path} changed during the run (${difference.fields.join(", ")})`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GateResult } from "./result.ts";
|
|
2
|
+
import type { WitnessEvaluation, WitnessHooks } from "../witness/run.ts";
|
|
3
|
+
export interface RedWitnessOutcome {
|
|
4
|
+
result: GateResult;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
evaluations: WitnessEvaluation[];
|
|
7
|
+
/** Wall-clock cost of the stored-witness re-evaluation, milliseconds. */
|
|
8
|
+
reEvaluationMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RedWitnessRun {
|
|
11
|
+
repoRoot: string;
|
|
12
|
+
base: string;
|
|
13
|
+
head?: string;
|
|
14
|
+
baseline?: string;
|
|
15
|
+
evidenceDir?: string;
|
|
16
|
+
hooks?: WitnessHooks;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run the red-witness gate against a repository. Exported so tests can
|
|
20
|
+
* drive it without a subprocess; the CLI below is a thin shell over it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runRedWitnessGate(run: RedWitnessRun): RedWitnessOutcome;
|