@intentius/chant 0.121.1 → 0.123.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/dist/cli/commands/build.d.ts +12 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +16 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/gate-resume.d.ts +1 -1
- package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/override-approve.d.ts +17 -0
- package/dist/cli/handlers/override-approve.d.ts.map +1 -0
- package/dist/cli/handlers/run-wave.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +4 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/gate-decision.d.ts +157 -0
- package/dist/gate-decision.d.ts.map +1 -0
- package/dist/lifecycle/gate-applied.d.ts +75 -0
- package/dist/lifecycle/gate-applied.d.ts.map +1 -0
- package/dist/lifecycle/gate-audit.d.ts +145 -0
- package/dist/lifecycle/gate-audit.d.ts.map +1 -0
- package/dist/lifecycle/gate-ledger-select.d.ts +54 -0
- package/dist/lifecycle/gate-ledger-select.d.ts.map +1 -0
- package/dist/lifecycle/gate-ledger.d.ts +1 -45
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/override-ledger.d.ts +34 -0
- package/dist/lifecycle/override-ledger.d.ts.map +1 -0
- package/dist/lifecycle/rollback.d.ts.map +1 -1
- package/dist/lifecycle/work-item-id.d.ts +22 -0
- package/dist/lifecycle/work-item-id.d.ts.map +1 -0
- package/dist/lifecycle/work-lease.d.ts +2 -2
- package/dist/lifecycle/work-lease.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +20 -1
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/notify-verify.d.ts +62 -0
- package/dist/notify-verify.d.ts.map +1 -0
- package/dist/notify.d.ts +202 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +4 -4
- package/dist/op/activities/gh-host.d.ts +37 -0
- package/dist/op/activities/gh-host.d.ts.map +1 -0
- package/dist/op/activities/lexicon-upgrade.d.ts +11 -0
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +74 -76
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts +17 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +8 -1
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-resume.d.ts +4 -2
- package/dist/op/gate-resume.d.ts.map +1 -1
- package/dist/op/gate-review.d.ts +2 -1
- package/dist/op/gate-review.d.ts.map +1 -1
- package/dist/op/gate.d.ts +43 -6
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +8 -6
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-waves-notify.d.ts +31 -0
- package/dist/op/op-waves-notify.d.ts.map +1 -0
- package/dist/op/op-waves-run.d.ts +91 -16
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves.d.ts +99 -2
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/operator.d.ts +15 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/override-gate.d.ts +224 -0
- package/dist/override-gate.d.ts.map +1 -0
- package/dist/pr-forge.d.ts +198 -19
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +1 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/trust/gate-seal.d.ts +96 -0
- package/dist/workspace/trust/gate-seal.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +2 -41
- package/dist/workspace/trust/policy.d.ts.map +1 -1
- package/dist/workspace/trust/seal.d.ts +2 -27
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/trust-files.d.ts +57 -0
- package/dist/workspace/trust/trust-files.d.ts.map +1 -0
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/cli/commands/build.ts +37 -18
- package/src/cli/commands/carve-emit-state.test.ts +8 -2
- package/src/cli/commands/lint.test.ts +109 -1
- package/src/cli/commands/lint.ts +165 -1
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
- package/src/cli/handlers/gate-resume.ts +3 -2
- package/src/cli/handlers/operator.test.ts +12 -1
- package/src/cli/handlers/operator.ts +25 -13
- package/src/cli/handlers/override-approve.test.ts +87 -0
- package/src/cli/handlers/override-approve.ts +103 -0
- package/src/cli/handlers/run-wave.ts +10 -2
- package/src/cli/main.test.ts +8 -0
- package/src/cli/main.ts +15 -0
- package/src/cli/registry.ts +4 -0
- package/src/gate-decision-bundle.test.ts +56 -0
- package/src/gate-decision.test.ts +237 -0
- package/src/gate-decision.ts +321 -0
- package/src/lifecycle/gate-applied.ts +115 -0
- package/src/lifecycle/gate-audit-bundle.test.ts +47 -0
- package/src/lifecycle/gate-audit.test.ts +134 -0
- package/src/lifecycle/gate-audit.ts +438 -0
- package/src/lifecycle/gate-ledger-select.ts +113 -0
- package/src/lifecycle/gate-ledger.test.ts +22 -0
- package/src/lifecycle/gate-ledger.ts +9 -104
- package/src/lifecycle/override-ledger.test.ts +63 -0
- package/src/lifecycle/override-ledger.ts +69 -0
- package/src/lifecycle/rollback.ts +5 -1
- package/src/lifecycle/work-item-id.test.ts +71 -0
- package/src/lifecycle/work-item-id.ts +35 -0
- package/src/lifecycle/work-lease.ts +4 -5
- package/src/lint/post-synth.test.ts +29 -0
- package/src/lint/post-synth.ts +21 -2
- package/src/notify-bundle.test.ts +50 -0
- package/src/notify-verify.test.ts +59 -0
- package/src/notify-verify.ts +93 -0
- package/src/notify.schema.json +72 -0
- package/src/notify.test.ts +279 -0
- package/src/notify.ts +398 -0
- package/src/op/activities/gh-host.test.ts +130 -0
- package/src/op/activities/gh-host.ts +79 -0
- package/src/op/activities/lexicon-upgrade.ts +22 -1
- package/src/op/activities/propose-upgrade.ts +5 -1
- package/src/op/activities/reconcile.test.ts +284 -280
- package/src/op/activities/reconcile.ts +128 -131
- package/src/op/gate-approval.test.ts +175 -7
- package/src/op/gate-approval.ts +27 -1
- package/src/op/gate-name.ts +10 -2
- package/src/op/gate-resume.ts +6 -3
- package/src/op/gate-review.ts +4 -7
- package/src/op/gate.ts +117 -16
- package/src/op/index.ts +20 -5
- package/src/op/op-waves-notify.test.ts +127 -0
- package/src/op/op-waves-notify.ts +101 -0
- package/src/op/op-waves-run.ts +527 -104
- package/src/op/op-waves.test.ts +383 -3
- package/src/op/op-waves.ts +187 -6
- package/src/op/operator.test.ts +29 -0
- package/src/op/operator.ts +27 -2
- package/src/op/run-context.test.ts +2 -2
- package/src/op/work-lease-decl.ts +4 -4
- package/src/op/work-lease-run.test.ts +5 -0
- package/src/op/work-lease-run.ts +4 -3
- package/src/override-gate-bundle.test.ts +47 -0
- package/src/override-gate.test.ts +239 -0
- package/src/override-gate.ts +395 -0
- package/src/pr-forge-bundle.test.ts +40 -0
- package/src/pr-forge.test.ts +303 -0
- package/src/pr-forge.ts +502 -47
- package/src/workspace/records-write-contract.test.ts +3 -2
- package/src/workspace/status-gates.ts +7 -4
- package/src/workspace/trust/gate-seal.test.ts +156 -0
- package/src/workspace/trust/gate-seal.ts +254 -0
- package/src/workspace/trust/policy.ts +10 -165
- package/src/workspace/trust/seal.ts +19 -34
- package/src/workspace/trust/trust-files.test.ts +50 -0
- package/src/workspace/trust/trust-files.ts +245 -0
- package/src/workspace/work-cli.ts +5 -3
- package/src/workspace/work-history.test.ts +7 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approvals a wave applied under, and the rule that spends them (#3784).
|
|
3
|
+
*
|
|
4
|
+
* terragucci's wave gate writes one line to `_gates/<op>/applied.jsonl` on
|
|
5
|
+
* `chant/lifecycle` before a wave applies under an approval: the gate, the
|
|
6
|
+
* digest, and the approval's own timestamp. From then on that approval is
|
|
7
|
+
* spent. It does not let the same digest through again, and it no longer
|
|
8
|
+
* stands as the approval of another digest either. Op waves write the same
|
|
9
|
+
* line, in the same file, when their spec sets `approvals.spend`, and
|
|
10
|
+
* `decideGateApproval` (`../gate-decision.ts`) and `evaluateGate`
|
|
11
|
+
* (`../op/gate.ts`) take the lines to leave spent approvals out.
|
|
12
|
+
*
|
|
13
|
+
* The file sits in a directory under `_gates/`, so the readers of
|
|
14
|
+
* `_gates/<op>.jsonl` never see its lines. {@link appliedGateApplies} hands
|
|
15
|
+
* them to `gate-audit` as applies, each tied to the approval it ran under.
|
|
16
|
+
*
|
|
17
|
+
* Pure: no git, no clock, no file. It bundles into `@intentius/chant/gate-decision`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { samePlanDigest } from "./plan-digest";
|
|
21
|
+
import type { GateAuditApply } from "./gate-audit";
|
|
22
|
+
|
|
23
|
+
/** One approval a wave applied under: the approval of `planDigest` made at `approvedAt` was used. */
|
|
24
|
+
export interface AppliedGateRecord {
|
|
25
|
+
version: 1;
|
|
26
|
+
kind: "applied";
|
|
27
|
+
op: string;
|
|
28
|
+
gate: string;
|
|
29
|
+
planDigest: string;
|
|
30
|
+
/** The approval's own timestamp, as the ledger holds it. Every approval of this gate and digest at or before it is spent. */
|
|
31
|
+
approvedAt: string;
|
|
32
|
+
approvedBy: string;
|
|
33
|
+
/** When the wave recorded it, just before it applied. */
|
|
34
|
+
timestamp: string;
|
|
35
|
+
/** The wave, by number and name, for an Op waves record. */
|
|
36
|
+
wave?: { number: number; name: string };
|
|
37
|
+
/** The pull request review the approval came from, under `pr-review`. */
|
|
38
|
+
review?: { pr?: number; head?: string };
|
|
39
|
+
runId?: string;
|
|
40
|
+
commit?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Where the applied lines of `op` live on `chant/lifecycle`: the directory and the file, as `writeBlobToPath` takes them. */
|
|
44
|
+
export function appliedGatePath(op: string): { dir: string; file: string } {
|
|
45
|
+
return { dir: `_gates/${op}`, file: "applied.jsonl" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The applied lines of an `applied.jsonl` file, oldest first. A line that is not one is skipped. */
|
|
49
|
+
export function parseAppliedGateLines(text: string): AppliedGateRecord[] {
|
|
50
|
+
const out: AppliedGateRecord[] = [];
|
|
51
|
+
for (const line of text.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
52
|
+
let r: Record<string, unknown>;
|
|
53
|
+
try {
|
|
54
|
+
r = JSON.parse(line) as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
r?.version === 1 && r.kind === "applied" && typeof r.op === "string" && typeof r.gate === "string" &&
|
|
60
|
+
typeof r.planDigest === "string" && typeof r.approvedAt === "string" && typeof r.timestamp === "string"
|
|
61
|
+
) {
|
|
62
|
+
out.push({ approvedBy: "", ...r } as unknown as AppliedGateRecord);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const time = (iso: string): number => new Date(iso).getTime();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether an approval is spent: a wave applied under an approval of the same
|
|
72
|
+
* gate and digest made at or after it. One that names no digest is never
|
|
73
|
+
* spent. With `review`, an approval that came from a pull request's review
|
|
74
|
+
* is spent too when a wave applied this digest under a review of the same
|
|
75
|
+
* head, so the review cannot be recorded again to pass the digest twice. A
|
|
76
|
+
* review not yet recorded has no `timestamp`.
|
|
77
|
+
*/
|
|
78
|
+
export function isApprovalSpent(
|
|
79
|
+
applied: readonly AppliedGateRecord[],
|
|
80
|
+
approval: { gate: string; planDigest?: string; timestamp?: string; review?: { pr?: number; head?: string } },
|
|
81
|
+
): boolean {
|
|
82
|
+
const digest = approval.planDigest;
|
|
83
|
+
if (digest === undefined) return false;
|
|
84
|
+
return applied.some(
|
|
85
|
+
(a) =>
|
|
86
|
+
a.gate === approval.gate &&
|
|
87
|
+
samePlanDigest(a.planDigest, digest) &&
|
|
88
|
+
((approval.timestamp !== undefined && time(a.approvedAt) >= time(approval.timestamp)) ||
|
|
89
|
+
(approval.review?.head !== undefined && a.review?.head === approval.review.head)),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The applied lines as `gate-audit` applies: each an `applied` entry tied to
|
|
95
|
+
* the approval it ran under. `ref` names the file, `_gates/<op>/applied.jsonl`
|
|
96
|
+
* by default; each line's index is appended so ids stay stable.
|
|
97
|
+
*/
|
|
98
|
+
export function appliedGateApplies(records: readonly AppliedGateRecord[], ref?: string): GateAuditApply[] {
|
|
99
|
+
return records.map((r, i) => ({
|
|
100
|
+
ref: `${ref ?? `_gates/${r.op}/applied.jsonl`}#${i + 1}`,
|
|
101
|
+
at: r.timestamp,
|
|
102
|
+
gate: r.gate,
|
|
103
|
+
op: r.op,
|
|
104
|
+
digest: r.planDigest,
|
|
105
|
+
approval: "required" as const,
|
|
106
|
+
detail: {
|
|
107
|
+
approvedAt: r.approvedAt,
|
|
108
|
+
approvedBy: r.approvedBy,
|
|
109
|
+
...(r.wave ? { wave: r.wave } : {}),
|
|
110
|
+
...(r.review ? { review: r.review } : {}),
|
|
111
|
+
...(r.runId ? { runId: r.runId } : {}),
|
|
112
|
+
...(r.commit ? { commit: r.commit } : {}),
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate audit reader bundles small (#3764, ruling #3421).
|
|
3
|
+
*
|
|
4
|
+
* terragucci and SQL Yodeler derive their audit trail from the gate ledger in
|
|
5
|
+
* one bundled file, with no TypeScript toolchain. So `@intentius/chant/lifecycle/gate-audit`
|
|
6
|
+
* must not reach `typescript`, esbuild, zod, git, or chant's fold, lint and
|
|
7
|
+
* codegen modules. This bundles it the way a product would and reads
|
|
8
|
+
* esbuild's own list of what went in.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { build } from "esbuild";
|
|
13
|
+
import { describe, expect, test } from "vitest";
|
|
14
|
+
import { REPO } from "../workspace/__fixtures__/contract-repo";
|
|
15
|
+
|
|
16
|
+
const BUDGET_BYTES = 32 * 1024;
|
|
17
|
+
|
|
18
|
+
const FORBIDDEN = [/node_modules\//, /\/src\/fold\//, /\/src\/lint\//, /\/src\/codegen\//, /\/src\/workspace\//, /\/lifecycle\/git\.ts$/, /\/lifecycle\/gate-ledger\.ts$/, /\/op\//];
|
|
19
|
+
|
|
20
|
+
describe("the gate audit bundle", () => {
|
|
21
|
+
test("holds the reader, the digest comparison and nothing heavier", async () => {
|
|
22
|
+
const result = await build({
|
|
23
|
+
entryPoints: [join(REPO, "packages", "core", "src", "lifecycle", "gate-audit.ts")],
|
|
24
|
+
bundle: true,
|
|
25
|
+
platform: "node",
|
|
26
|
+
format: "esm",
|
|
27
|
+
write: false,
|
|
28
|
+
metafile: true,
|
|
29
|
+
outdir: "out",
|
|
30
|
+
logLevel: "silent",
|
|
31
|
+
});
|
|
32
|
+
const inputs = Object.keys(result.metafile.inputs);
|
|
33
|
+
for (const pattern of FORBIDDEN) {
|
|
34
|
+
expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
|
|
35
|
+
}
|
|
36
|
+
expect(inputs.map((i) => i.replace(/^.*?packages\//, "packages/")).sort()).toEqual([
|
|
37
|
+
"packages/core/src/content-digest.ts",
|
|
38
|
+
"packages/core/src/declarable.ts",
|
|
39
|
+
"packages/core/src/effect-receipt.ts",
|
|
40
|
+
"packages/core/src/intrinsic.ts",
|
|
41
|
+
"packages/core/src/lifecycle/gate-audit.ts",
|
|
42
|
+
"packages/core/src/lifecycle/plan-digest.ts",
|
|
43
|
+
]);
|
|
44
|
+
const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
|
|
45
|
+
expect(bytes).toBeLessThan(BUDGET_BYTES);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { deriveGateAudit, gateLedgerLines, gateLedgerLinesFromLog, readGateAuditLedgers, isOverrideGate } from "./gate-audit";
|
|
3
|
+
import { memoryGateLedgerPort } from "../op/gate";
|
|
4
|
+
|
|
5
|
+
const D1 = "jcs1-sha256:" + "a".repeat(64);
|
|
6
|
+
const D2 = "jcs1-sha256:" + "b".repeat(64);
|
|
7
|
+
|
|
8
|
+
const pending = (o: Record<string, unknown> = {}) =>
|
|
9
|
+
JSON.stringify({ version: 1, kind: "pending", op: "tf-apply", gate: "wave-1", timestamp: "2026-10-01T10:00:00.000Z", expiresAt: "2026-10-03T10:00:00.000Z", planDigest: D1, runId: "r1", ...o });
|
|
10
|
+
const resolution = (o: Record<string, unknown> = {}) =>
|
|
11
|
+
JSON.stringify({ version: 1, op: "tf-apply", gate: "wave-1", resolvedBy: "alex", timestamp: "2026-10-01T11:00:00.000Z", planDigest: D1, ...o });
|
|
12
|
+
const ledger = (op: string, ...texts: string[]) => ({ op, lines: texts.map((text) => ({ text })) });
|
|
13
|
+
|
|
14
|
+
describe("lifecycle/gate-audit", () => {
|
|
15
|
+
test("a request, then an approval with its seal and approver", () => {
|
|
16
|
+
const seal = { signer: "alex", key: "SHA256:k", signature: "-----BEGIN" };
|
|
17
|
+
const { entries, gaps } = deriveGateAudit({
|
|
18
|
+
ledgers: [ledger("tf-apply", pending(), resolution({ seal, relayedBy: "slack", note: "ok", approver: { kind: "human", roles: ["ops"] }, via: "relay" }))],
|
|
19
|
+
});
|
|
20
|
+
expect(gaps).toEqual([]);
|
|
21
|
+
expect(entries.map((e) => e.kind)).toEqual(["approval-requested", "approved"]);
|
|
22
|
+
expect(entries[0]).toMatchObject({ gate: "wave-1", digest: D1, runId: "r1", expiresAt: "2026-10-03T10:00:00.000Z" });
|
|
23
|
+
expect(entries[1]).toMatchObject({ by: "alex", seal: "sealed", signer: "alex", relayedBy: "slack", note: "ok", approver: { kind: "human", roles: ["ops"] }, detail: { via: "relay" } });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("ids are stable and differ per line", () => {
|
|
27
|
+
const input = { ledgers: [ledger("tf-apply", pending(), resolution())] };
|
|
28
|
+
const a = deriveGateAudit(input).entries;
|
|
29
|
+
const b = deriveGateAudit(input).entries;
|
|
30
|
+
expect(a.map((e) => e.id)).toEqual(b.map((e) => e.id));
|
|
31
|
+
expect(new Set(a.map((e) => e.id)).size).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a pull request review counts as signed; a bare approval is unsigned", () => {
|
|
35
|
+
const review = { pr: 7, head: "abc", approvers: ["bo"] };
|
|
36
|
+
const { entries } = deriveGateAudit({ ledgers: [ledger("tf-apply", pending(), resolution({ review }), resolution({ timestamp: "2026-10-01T12:00:00.000Z" }))] });
|
|
37
|
+
expect(entries.filter((e) => e.kind === "approved").map((e) => e.seal)).toEqual(["review", "unsigned"]);
|
|
38
|
+
expect(entries.find((e) => e.seal === "review")?.review).toEqual(review);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("an approval with no pending fact before it is a gap, also when the digest differs or the fact is later", () => {
|
|
42
|
+
const none = deriveGateAudit({ ledgers: [ledger("tf-apply", resolution())] });
|
|
43
|
+
expect(none.gaps.map((g) => g.kind)).toEqual(["approval-without-request"]);
|
|
44
|
+
const other = deriveGateAudit({ ledgers: [ledger("tf-apply", pending({ planDigest: D2 }), resolution())] });
|
|
45
|
+
expect(other.gaps.map((g) => g.kind)).toEqual(["approval-without-request"]);
|
|
46
|
+
const later = deriveGateAudit({ ledgers: [ledger("tf-apply", resolution({ timestamp: "2026-10-01T09:00:00.000Z" }), pending())] });
|
|
47
|
+
expect(later.gaps.map((g) => g.kind)).toEqual(["approval-without-request"]);
|
|
48
|
+
const old = deriveGateAudit({ ledgers: [ledger("tf-apply", pending({ planDigest: undefined }), resolution())] });
|
|
49
|
+
expect(old.gaps).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the sealed option flags an unsigned approval and not a sealed or reviewed one", () => {
|
|
53
|
+
const { gaps } = deriveGateAudit({
|
|
54
|
+
ledgers: [ledger("tf-apply", pending(), resolution(), resolution({ review: { pr: 1 } }))],
|
|
55
|
+
sealed: (op) => op === "tf-apply",
|
|
56
|
+
});
|
|
57
|
+
expect(gaps.map((g) => g.kind)).toEqual(["unsealed"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("overrides: terragucci's denial and SQL Yodeler's override gate", () => {
|
|
61
|
+
const denial = pending({ op: "policy-override", gate: "root/a", planDigest: "sha256:ov", rules: ["r1", "r2"], members: [{ member: "root/a", planDigest: D1 }] });
|
|
62
|
+
const rec = resolution({ op: "policy-override", gate: "root/a", planDigest: "sha256:ov", note: "freeze exception" });
|
|
63
|
+
const yodel = resolution({ op: "migrate", gate: "override-no-drop", note: "legacy table" });
|
|
64
|
+
const { entries, gaps } = deriveGateAudit({ ledgers: [ledger("policy-override", denial, rec), ledger("migrate", yodel)] });
|
|
65
|
+
expect(gaps).toEqual([]);
|
|
66
|
+
expect(entries.map((e) => e.kind)).toEqual(["override-requested", "override-recorded", "override-recorded"]);
|
|
67
|
+
const recorded = entries.filter((e) => e.kind === "override-recorded");
|
|
68
|
+
expect(recorded[0]).toMatchObject({ note: "freeze exception", rules: ["r1", "r2"] });
|
|
69
|
+
expect(recorded[1]).toMatchObject({ note: "legacy table", rules: ["no-drop"] });
|
|
70
|
+
expect(entries.find((e) => e.kind === "override-requested")?.detail).toMatchObject({ members: [{ member: "root/a", planDigest: D1 }] });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a removed line is a revocation by whoever removed it", () => {
|
|
74
|
+
const log = [
|
|
75
|
+
`\x1ec1\x1falex\x1f2026-10-01T11:00:00+00:00\n\ndiff --git a/x b/x\n+${resolution()}\n`,
|
|
76
|
+
`\x1ec2\x1fbo\x1f2026-10-02T08:00:00+00:00\n\ndiff --git a/x b/x\n-${resolution()}\n`,
|
|
77
|
+
`\x1ec3\x1fcy\x1f2026-10-02T09:00:00+00:00\n\ndiff --git a/x b/x\n-${pending({ gate: "gone" })}\n+${pending({ gate: "gone" })}\n`,
|
|
78
|
+
].join("");
|
|
79
|
+
const lines = gateLedgerLinesFromLog(log);
|
|
80
|
+
expect(lines).toHaveLength(2);
|
|
81
|
+
const { entries } = deriveGateAudit({ ledgers: [{ op: "tf-apply", lines: [{ text: pending() }, ...lines] }] });
|
|
82
|
+
const revoked = entries.find((e) => e.kind === "revoked")!;
|
|
83
|
+
expect(revoked).toMatchObject({ by: "bo", at: "2026-10-02T08:00:00.000Z", subject: "approval", detail: { approvedBy: "alex" } });
|
|
84
|
+
expect(entries.filter((e) => e.kind === "approved")).toHaveLength(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("applies: under an approval, without one, waived, and spending an override", () => {
|
|
88
|
+
const ledgers = [ledger("tf-apply", pending(), resolution()), ledger("policy-override", resolution({ op: "policy-override", gate: "root/a", planDigest: "sha256:ov" }))];
|
|
89
|
+
const ok = { ref: "reports/1", at: "2026-10-01T12:00:00.000Z", gate: "wave-1", digest: D1 };
|
|
90
|
+
const { entries, gaps } = deriveGateAudit({
|
|
91
|
+
ledgers,
|
|
92
|
+
applies: [
|
|
93
|
+
ok,
|
|
94
|
+
{ ...ok, ref: "reports/2", digest: D2 },
|
|
95
|
+
{ ...ok, ref: "reports/3", digest: D2, approval: "not-required" },
|
|
96
|
+
{ ...ok, ref: "reports/4", digest: undefined },
|
|
97
|
+
{ ...ok, ref: "reports/5", at: "2026-10-01T10:30:00.000Z" },
|
|
98
|
+
{ ...ok, ref: "reports/6", overrides: [{ gate: "root/a", digest: "sha256:ov" }, { gate: "root/b" }] },
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
expect(gaps.map((g) => [g.kind, g.detail.split(" ")[0]])).toEqual([
|
|
102
|
+
["approval", "reports/2"],
|
|
103
|
+
["approval", "reports/5"],
|
|
104
|
+
["override", "reports/6"],
|
|
105
|
+
]);
|
|
106
|
+
const approved = entries.find((e) => e.kind === "approved")!;
|
|
107
|
+
expect(entries.filter((e) => e.kind === "applied" && e.under === approved.id)).toHaveLength(2);
|
|
108
|
+
const spent = entries.filter((e) => e.kind === "override-spent");
|
|
109
|
+
expect(spent).toHaveLength(1);
|
|
110
|
+
expect(spent[0]).toMatchObject({ gate: "root/a", by: "alex", at: "2026-10-01T12:00:00.000Z" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("malformed lines are gaps and never entries", () => {
|
|
114
|
+
const { entries, gaps } = deriveGateAudit({ ledgers: [ledger("tf-apply", "{nope", JSON.stringify({ version: 2, gate: "g", timestamp: "t" }), pending())] });
|
|
115
|
+
expect(entries).toHaveLength(1);
|
|
116
|
+
expect(gaps.map((g) => g.kind)).toEqual(["malformed-line", "malformed-line"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the override rule is replaceable", () => {
|
|
120
|
+
const { entries } = deriveGateAudit({ ledgers: [ledger("x", resolution({ op: "x", gate: "ovr/a" }))], isOverride: (_op, gate) => gate.startsWith("ovr/") });
|
|
121
|
+
expect(entries[0]?.kind).toBe("override-recorded");
|
|
122
|
+
expect(isOverrideGate("policy-override", "r")).toBe(true);
|
|
123
|
+
expect(isOverrideGate("a", "b")).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("gateLedgerLines and the port reader feed the same derivation", async () => {
|
|
127
|
+
const text = `${pending()}\n\n${resolution()}\n`;
|
|
128
|
+
const port = memoryGateLedgerPort({ pending: [JSON.parse(pending())], resolutions: [JSON.parse(resolution())] });
|
|
129
|
+
const fromText = deriveGateAudit({ ledgers: [{ op: "tf-apply", lines: gateLedgerLines(text) }] });
|
|
130
|
+
const fromPort = deriveGateAudit({ ledgers: await readGateAuditLedgers(port, ["tf-apply"]) });
|
|
131
|
+
expect(fromPort.entries.map((e) => e.kind)).toEqual(["approval-requested", "approved"]);
|
|
132
|
+
expect(fromPort.entries.map((e) => e.at)).toEqual(fromText.entries.map((e) => e.at));
|
|
133
|
+
});
|
|
134
|
+
});
|