asylumwarden 0.0.0 → 0.1.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/README.md +10 -5
- package/adapters/opencode/index.js +282 -0
- package/bin/asylumwarden.mjs +5 -0
- package/package.json +14 -1
- package/schemas/config.schema.json +93 -0
- package/schemas/ledger.schema.json +21 -0
- package/schemas/manifest.schema.json +49 -0
- package/schemas/receipt.schema.json +102 -0
- package/schemas/registry.schema.json +43 -0
- package/src/cli/main.mjs +342 -0
- package/src/config/bootstrap.mjs +36 -0
- package/src/config/load.mjs +531 -0
- package/src/config/schemas.mjs +58 -0
- package/src/context.mjs +50 -0
- package/src/index.mjs +12 -0
- package/src/kernel/canon.mjs +59 -0
- package/src/kernel/errors.mjs +80 -0
- package/src/kernel/fsutil.mjs +147 -0
- package/src/kernel/glob.mjs +70 -0
- package/src/kernel/lock.mjs +104 -0
- package/src/kernel/schemalite.mjs +153 -0
- package/src/kernel/state.mjs +56 -0
- package/src/lease/lease.mjs +551 -0
- package/src/ledger/anchor.mjs +395 -0
- package/src/ledger/chain.mjs +468 -0
- package/src/manifest/manifest.mjs +199 -0
- package/src/manifest/plan.mjs +142 -0
- package/src/receipt/receipt.mjs +555 -0
- package/src/views/views.mjs +148 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Views — read side: ready / status / verify (CONTRACT §9).
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { fail, WardenError, violation } from "../kernel/errors.mjs";
|
|
7
|
+
import { sha256File } from "../kernel/canon.mjs";
|
|
8
|
+
import { readStateJson } from "../kernel/state.mjs";
|
|
9
|
+
import { anchorSummary } from "../ledger/anchor.mjs";
|
|
10
|
+
|
|
11
|
+
async function listWaveIds(sl) {
|
|
12
|
+
try {
|
|
13
|
+
const entries = await fs.readdir(sl.waves, { withFileTypes: true });
|
|
14
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.code === "ENOENT") return [];
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readWaveCells(ctx, wave) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.stat(ctx.sl.metaFile(wave));
|
|
24
|
+
} catch {
|
|
25
|
+
fail("WAVE-MISSING", `no such wave: ${wave}`, [violation("wave", "wave-missing", wave)]);
|
|
26
|
+
}
|
|
27
|
+
const ids = (await fs.readdir(ctx.sl.cellsDir(wave)))
|
|
28
|
+
.filter((f) => f.endsWith(".json"))
|
|
29
|
+
.map((f) => f.slice(0, -".json".length))
|
|
30
|
+
.sort();
|
|
31
|
+
const cells = [];
|
|
32
|
+
for (const id of ids) cells.push(await readStateJson(ctx.sl.cellFile(wave, id)));
|
|
33
|
+
return cells;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function leaseView(ctx, wave, cell) {
|
|
37
|
+
if (cell.state !== "claimed") return null;
|
|
38
|
+
try {
|
|
39
|
+
return await readStateJson(ctx.sl.ownerFile(wave, cell.id));
|
|
40
|
+
} catch {
|
|
41
|
+
return null; // claimed with a lost lease file is surfaced by verify, not here
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function readyCells(ctx, wave) {
|
|
46
|
+
const cells = await readWaveCells(ctx, wave);
|
|
47
|
+
const done = new Set(cells.filter((c) => c.state === "done").map((c) => c.id));
|
|
48
|
+
return cells.map((cell) => ({
|
|
49
|
+
id: cell.id,
|
|
50
|
+
kind: cell.kind,
|
|
51
|
+
line: cell.line,
|
|
52
|
+
cwd: cell.cwd,
|
|
53
|
+
deps: cell.deps,
|
|
54
|
+
state: cell.state,
|
|
55
|
+
ready: cell.state === "pending" && cell.deps.every((d) => done.has(d)),
|
|
56
|
+
blocked_by: cell.deps.filter((d) => !done.has(d)),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function statusReport(ctx, wave) {
|
|
61
|
+
const waves = wave ? [wave] : await listWaveIds(ctx.sl);
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const id of waves) {
|
|
64
|
+
const cells = await readWaveCells(ctx, id);
|
|
65
|
+
const rows = [];
|
|
66
|
+
for (const cell of cells) {
|
|
67
|
+
const lease = await leaseView(ctx, id, cell);
|
|
68
|
+
let receiptSha = null;
|
|
69
|
+
if (cell.state === "done" || cell.state === "failed") {
|
|
70
|
+
try {
|
|
71
|
+
receiptSha = await sha256File(ctx.sl.receiptFile(id, cell.id));
|
|
72
|
+
} catch {
|
|
73
|
+
receiptSha = "MISSING";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
rows.push({
|
|
77
|
+
id: cell.id,
|
|
78
|
+
kind: cell.kind,
|
|
79
|
+
line: cell.line,
|
|
80
|
+
state: cell.state,
|
|
81
|
+
epoch: cell.epoch,
|
|
82
|
+
holder: lease?.actor ?? null,
|
|
83
|
+
expires_at: lease?.expires_at ?? null,
|
|
84
|
+
receipt_sha256: receiptSha,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
out.push({ wave: id, cells: rows });
|
|
88
|
+
}
|
|
89
|
+
return { waves: out };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function verifyAll(ctx, { wave } = {}) {
|
|
93
|
+
const chain = await ctx.ledger.verify();
|
|
94
|
+
if (chain.torn) {
|
|
95
|
+
fail("LEDGER-TORN", `ledger line ${chain.torn.line} is torn or unparseable`, [
|
|
96
|
+
violation(`ledger:${chain.torn.line}`, "torn", chain.torn.detail),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
if (!chain.chain_ok) {
|
|
100
|
+
fail("LEDGER-BROKEN", "ledger chain verification failed", chain.violations ?? []);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Spot-check: every done/fail event's receipt file must still hash to the
|
|
104
|
+
// recorded sha (evidence preserved, CONTRACT §7).
|
|
105
|
+
const { entries } = chain;
|
|
106
|
+
const expected = new Map();
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if ((entry.type === "done" || entry.type === "fail") && entry.wave && entry.cell) {
|
|
109
|
+
expected.set(`${entry.wave}/${entry.cell}`, entry.payload.receipt_sha256 ?? null);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const mismatches = [];
|
|
113
|
+
let checked = 0;
|
|
114
|
+
for (const [key, sha] of expected) {
|
|
115
|
+
const [w, c] = key.split("/");
|
|
116
|
+
try {
|
|
117
|
+
const actual = await sha256File(ctx.sl.receiptFile(w, c));
|
|
118
|
+
checked += 1;
|
|
119
|
+
if (sha !== null && actual !== sha) {
|
|
120
|
+
mismatches.push({ cell: key, expected: sha, actual });
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code !== "ENOENT") throw error;
|
|
124
|
+
if (!wave || w === wave) mismatches.push({ cell: key, expected: sha, actual: "MISSING" });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (mismatches.length > 0) {
|
|
128
|
+
fail("LEDGER-BROKEN", "receipt spot-check failed", mismatches.map((m) =>
|
|
129
|
+
violation(`receipt:${m.cell}`, "receipt-hash-mismatch", `${m.actual} != ${m.expected}`)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const anchor = await anchorSummary(ctx);
|
|
133
|
+
if (anchor.stale && anchor.closure !== "none") {
|
|
134
|
+
throw new WardenError(
|
|
135
|
+
"ANCHOR-STALE",
|
|
136
|
+
`ledger head seq ${chain.head_seq ?? ""} is beyond last anchor seq ${anchor.last_seq}; audit closure NOT claimed`,
|
|
137
|
+
[violation("anchor", "stale", JSON.stringify(anchor))],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
chain_ok: true,
|
|
142
|
+
entries: entries.length,
|
|
143
|
+
head: chain.head,
|
|
144
|
+
audit_closure: anchor.closure,
|
|
145
|
+
anchor,
|
|
146
|
+
spot_check: { checked, mismatches: [] },
|
|
147
|
+
};
|
|
148
|
+
}
|