@peterxiaoyang/superspec 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 +47 -0
- package/adapters/codex/agents/architect.toml +157 -0
- package/adapters/codex/agents/code-reviewer.toml +175 -0
- package/adapters/codex/agents/critic.toml +114 -0
- package/adapters/codex/agents/test-engineer.toml +163 -0
- package/adapters/codex/agents/verifier.toml +119 -0
- package/adapters/codex/install-map.json +81 -0
- package/bin/launch.js +37 -0
- package/bin/superspec-guard.js +4 -0
- package/bin/superspec-init.js +4 -0
- package/bin/superspec.js +4 -0
- package/dist/src/archive.d.ts +23 -0
- package/dist/src/archive.js +428 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +20 -0
- package/dist/src/cli_args.d.ts +12 -0
- package/dist/src/cli_args.js +146 -0
- package/dist/src/core.d.ts +19 -0
- package/dist/src/core.js +357 -0
- package/dist/src/disclosure.d.ts +35 -0
- package/dist/src/disclosure.js +671 -0
- package/dist/src/evidence.d.ts +28 -0
- package/dist/src/evidence.js +849 -0
- package/dist/src/gates.d.ts +16 -0
- package/dist/src/gates.js +1470 -0
- package/dist/src/git.d.ts +8 -0
- package/dist/src/git.js +112 -0
- package/dist/src/init_cli.d.ts +2 -0
- package/dist/src/init_cli.js +145 -0
- package/dist/src/install_engine.d.ts +54 -0
- package/dist/src/install_engine.js +351 -0
- package/dist/src/invariants.d.ts +16 -0
- package/dist/src/invariants.js +363 -0
- package/dist/src/openspec.d.ts +18 -0
- package/dist/src/openspec.js +157 -0
- package/dist/src/paths.d.ts +22 -0
- package/dist/src/paths.js +203 -0
- package/dist/src/project_init.d.ts +4 -0
- package/dist/src/project_init.js +161 -0
- package/dist/src/state.d.ts +37 -0
- package/dist/src/state.js +464 -0
- package/dist/src/tasks.d.ts +23 -0
- package/dist/src/tasks.js +225 -0
- package/dist/src/util.d.ts +120 -0
- package/dist/src/util.js +442 -0
- package/dist/superspec.d.ts +4 -0
- package/dist/superspec.js +57 -0
- package/dist/superspec_guard.d.ts +4 -0
- package/dist/superspec_guard.js +19 -0
- package/dist/superspec_init.d.ts +2 -0
- package/dist/superspec_init.js +17 -0
- package/package.json +63 -0
- package/schemas/install-manifest.schema.json +80 -0
- package/templates/sidecar/archive-preservation.json +11 -0
- package/templates/sidecar/business-invariants.md +38 -0
- package/templates/sidecar/config.yaml +13 -0
- package/templates/sidecar/discovery.md +24 -0
- package/templates/sidecar/test-contract.md +26 -0
- package/templates/workflow/prompts/architect.md +113 -0
- package/templates/workflow/prompts/code-reviewer.md +141 -0
- package/templates/workflow/prompts/critic.md +80 -0
- package/templates/workflow/prompts/test-engineer.md +130 -0
- package/templates/workflow/prompts/verifier.md +85 -0
- package/templates/workflow/skills/superspec-apply/SKILL.md +72 -0
- package/templates/workflow/skills/superspec-archive/SKILL.md +41 -0
- package/templates/workflow/skills/superspec-explore/SKILL.md +70 -0
- package/templates/workflow/skills/superspec-propose/SKILL.md +79 -0
- package/templates/workflow/skills/superspec-review/SKILL.md +237 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { GUARD_VERSION, GuardError, SCHEMA_VERSION, STATE_FILENAME, STATE_LOCK_FILENAME, deepEqual, isObject, now, reason, renderList, fingerprint_obj, runtime, sha256_file, } from "./util.js";
|
|
5
|
+
import { artifact_status_map, effective_route_phase, get_repo_root, normalize_route_phase, openspec_version, status_fingerprint } from "./openspec.js";
|
|
6
|
+
import { config_file, ensure_state_layout, find_forbidden_aliases, project_config_file, superspec_dir, sidecar_business_invariants_path, sidecar_discovery_path, sidecar_test_contract_path, } from "./paths.js";
|
|
7
|
+
import { evidence_fingerprint } from "./evidence.js";
|
|
8
|
+
export function state_file(changeRoot) {
|
|
9
|
+
return join(superspec_dir(changeRoot), STATE_FILENAME);
|
|
10
|
+
}
|
|
11
|
+
export function ledger_file(changeRoot) {
|
|
12
|
+
return join(superspec_dir(changeRoot), "ledger.jsonl");
|
|
13
|
+
}
|
|
14
|
+
export function load_state(changeRoot) {
|
|
15
|
+
const filePath = state_file(changeRoot);
|
|
16
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// FIX-1 (audit B-2): a missing state file is a legal "first run / explicit delete",
|
|
26
|
+
// but an existing-yet-unparseable file means an unexpected write happened and must fail closed.
|
|
27
|
+
export function state_file_corrupt(changeRoot) {
|
|
28
|
+
const filePath = state_file(changeRoot);
|
|
29
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return !isObject(JSON.parse(readFileSync(filePath, "utf8")));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function state_corrupt_reasons(changeRoot) {
|
|
39
|
+
if (!state_file_corrupt(changeRoot))
|
|
40
|
+
return [];
|
|
41
|
+
return [reason("state_corrupt", `.superspec/${STATE_FILENAME} exists but is not a parseable JSON object; guard state is corrupt (unexpected write, crash truncation, or manual edit). Inspect the file first, then rerun \`recompute --change <change> --rebuild-corrupt\` to explicitly rebuild guard-owned state; the rebuild is recorded in ledger.jsonl`)];
|
|
42
|
+
}
|
|
43
|
+
export function read_ledger_text(changeRoot) {
|
|
44
|
+
ensure_state_layout(changeRoot);
|
|
45
|
+
const filePath = ledger_file(changeRoot);
|
|
46
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile())
|
|
47
|
+
return "";
|
|
48
|
+
return readFileSync(filePath, "utf8");
|
|
49
|
+
}
|
|
50
|
+
export function compute_fingerprints(changeRoot, status) {
|
|
51
|
+
const repoRoot = get_repo_root(status);
|
|
52
|
+
return {
|
|
53
|
+
openspec_status_fingerprint: status_fingerprint(status),
|
|
54
|
+
project_config_fingerprint: sha256_file(project_config_file(repoRoot)),
|
|
55
|
+
change_config_fingerprint: sha256_file(config_file(changeRoot)),
|
|
56
|
+
forbidden_aliases_fingerprint: fingerprint_obj(find_forbidden_aliases(repoRoot, changeRoot).sort()),
|
|
57
|
+
evidence_fingerprint: evidence_fingerprint(changeRoot),
|
|
58
|
+
tasks_fingerprint: sha256_file(join(changeRoot, "tasks.md")),
|
|
59
|
+
discovery_fingerprint: sha256_file(sidecar_discovery_path(changeRoot)),
|
|
60
|
+
design_fingerprint: sha256_file(join(changeRoot, "design.md")),
|
|
61
|
+
business_invariants_fingerprint: sha256_file(sidecar_business_invariants_path(changeRoot)),
|
|
62
|
+
test_contract_fingerprint: sha256_file(sidecar_test_contract_path(changeRoot)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function state_stale_reasons(changeRoot, status) {
|
|
66
|
+
const prev = load_state(changeRoot);
|
|
67
|
+
if (prev === null)
|
|
68
|
+
return [];
|
|
69
|
+
const current = compute_fingerprints(changeRoot, status);
|
|
70
|
+
const previous = isObject(prev.computed_from) ? prev.computed_from : {};
|
|
71
|
+
const stale = Object.entries(current).filter(([key, value]) => previous[key] !== value).map(([key]) => key).sort();
|
|
72
|
+
if (stale.length === 0)
|
|
73
|
+
return [];
|
|
74
|
+
return [reason("state_fingerprint_stale", `superspec-state fingerprints are stale: ${renderList(stale)}`)];
|
|
75
|
+
}
|
|
76
|
+
function fsyncDir(dirPath) {
|
|
77
|
+
let fd = null;
|
|
78
|
+
try {
|
|
79
|
+
fd = openSync(dirPath, "r");
|
|
80
|
+
fsyncSync(fd);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Directory fsync is best-effort across platforms.
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (fd !== null)
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function materialize_ledger_event(event) {
|
|
91
|
+
return { ...event, event_id: `EVT-${Date.now()}-${++ledgerEventSequence}`, created_at: now() };
|
|
92
|
+
}
|
|
93
|
+
let ledgerEventSequence = 0;
|
|
94
|
+
export function ledger_event_line(event) {
|
|
95
|
+
return `${JSON.stringify(event)}\n`;
|
|
96
|
+
}
|
|
97
|
+
function appendLedgerLineUnlocked(base, line) {
|
|
98
|
+
const ledger = join(base, "ledger.jsonl");
|
|
99
|
+
const fd = openSync(ledger, "a");
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(fd, line, "utf8");
|
|
102
|
+
fsyncSync(fd);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
closeSync(fd);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function removeNonFilePath(path) {
|
|
109
|
+
if (!existsSync(path))
|
|
110
|
+
return;
|
|
111
|
+
rmSync(path, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
function currentStateFingerprints(changeRoot) {
|
|
114
|
+
const current = load_state(changeRoot);
|
|
115
|
+
if (current === null)
|
|
116
|
+
return null;
|
|
117
|
+
return isObject(current.computed_from) ? current.computed_from : {};
|
|
118
|
+
}
|
|
119
|
+
function casStateFingerprints(changeRoot, expected) {
|
|
120
|
+
if (expected === undefined || expected === null)
|
|
121
|
+
return;
|
|
122
|
+
const current = currentStateFingerprints(changeRoot);
|
|
123
|
+
if (current === null && Object.keys(expected).length === 0)
|
|
124
|
+
return;
|
|
125
|
+
if (!deepEqual(current, expected))
|
|
126
|
+
throw new GuardError("state_concurrent_update: superspec-state fingerprints changed during write");
|
|
127
|
+
}
|
|
128
|
+
export function with_state_lock(changeRoot, fn) {
|
|
129
|
+
ensure_state_layout(changeRoot);
|
|
130
|
+
const base = superspec_dir(changeRoot);
|
|
131
|
+
const lock = join(base, STATE_LOCK_FILENAME);
|
|
132
|
+
let fd = null;
|
|
133
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
134
|
+
try {
|
|
135
|
+
fd = openSync(lock, "wx");
|
|
136
|
+
writeFileSync(fd, `${JSON.stringify(stateLockInfo())}\n`, "utf8");
|
|
137
|
+
fsyncSync(fd);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err?.code !== "EEXIST")
|
|
142
|
+
throw err;
|
|
143
|
+
const stale = reclaim_stale_state_lock(lock);
|
|
144
|
+
if (stale)
|
|
145
|
+
continue;
|
|
146
|
+
throw new GuardError(`state_concurrent_update: ${describe_state_lock(lock)}; if no guard process is active, remove ${STATE_LOCK_FILENAME} or rerun recompute --force-unlock`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (fd === null) {
|
|
150
|
+
throw new GuardError(`state_concurrent_update: ${STATE_LOCK_FILENAME} could not be acquired after stale lock recovery`);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return fn();
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
if (fd !== null)
|
|
157
|
+
closeSync(fd);
|
|
158
|
+
try {
|
|
159
|
+
unlinkSync(lock);
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function stateLockStaleMs() {
|
|
165
|
+
const configured = Number(process.env.SUPERSPEC_STATE_LOCK_STALE_MS);
|
|
166
|
+
return Number.isFinite(configured) && configured > 0 ? configured : 5 * 60 * 1000;
|
|
167
|
+
}
|
|
168
|
+
function stateLockInfo() {
|
|
169
|
+
return {
|
|
170
|
+
pid: process.pid,
|
|
171
|
+
hostname: hostname(),
|
|
172
|
+
created_at: now(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function read_state_lock_info(lock) {
|
|
176
|
+
try {
|
|
177
|
+
const text = readFileSync(lock, "utf8").trim();
|
|
178
|
+
if (!text)
|
|
179
|
+
return null;
|
|
180
|
+
const parsed = JSON.parse(text);
|
|
181
|
+
return isObject(parsed) ? parsed : null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function pid_is_alive(pid) {
|
|
188
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
189
|
+
return false;
|
|
190
|
+
try {
|
|
191
|
+
process.kill(pid, 0);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (err?.code === "ESRCH")
|
|
196
|
+
return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function lock_age_ms(lock, info) {
|
|
201
|
+
const parsed = typeof info?.created_at === "string" ? Date.parse(info.created_at) : NaN;
|
|
202
|
+
if (Number.isFinite(parsed))
|
|
203
|
+
return Date.now() - parsed;
|
|
204
|
+
try {
|
|
205
|
+
return Date.now() - statSync(lock).mtimeMs;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function state_lock_is_stale(lock, info) {
|
|
212
|
+
const ageMs = lock_age_ms(lock, info);
|
|
213
|
+
if (!info)
|
|
214
|
+
return false;
|
|
215
|
+
const pid = Number(info.pid);
|
|
216
|
+
const host = String(info.hostname ?? "");
|
|
217
|
+
if (host === hostname() && pid_is_alive(pid))
|
|
218
|
+
return false;
|
|
219
|
+
if (host === hostname() && !pid_is_alive(pid))
|
|
220
|
+
return true;
|
|
221
|
+
return ageMs > stateLockStaleMs();
|
|
222
|
+
}
|
|
223
|
+
function reclaim_stale_state_lock(lock) {
|
|
224
|
+
const info = read_state_lock_info(lock);
|
|
225
|
+
if (!state_lock_is_stale(lock, info))
|
|
226
|
+
return false;
|
|
227
|
+
const reclaimed = `${lock}.stale-${process.pid}-${Date.now()}`;
|
|
228
|
+
try {
|
|
229
|
+
renameSync(lock, reclaimed);
|
|
230
|
+
rmSync(reclaimed, { force: true });
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function describe_state_lock(lock) {
|
|
238
|
+
const info = read_state_lock_info(lock);
|
|
239
|
+
if (!info)
|
|
240
|
+
return `${STATE_LOCK_FILENAME} is held by an unstructured or fresh legacy lock`;
|
|
241
|
+
const details = [
|
|
242
|
+
`pid=${String(info.pid ?? "unknown")}`,
|
|
243
|
+
`hostname=${String(info.hostname ?? "unknown")}`,
|
|
244
|
+
`created_at=${String(info.created_at ?? "unknown")}`,
|
|
245
|
+
];
|
|
246
|
+
return `${STATE_LOCK_FILENAME} is held (${details.join(", ")})`;
|
|
247
|
+
}
|
|
248
|
+
export function force_unlock_state(changeRoot) {
|
|
249
|
+
ensure_state_layout(changeRoot);
|
|
250
|
+
const lock = join(superspec_dir(changeRoot), STATE_LOCK_FILENAME);
|
|
251
|
+
if (!existsSync(lock))
|
|
252
|
+
return false;
|
|
253
|
+
const info = read_state_lock_info(lock);
|
|
254
|
+
const pid = Number(info?.pid);
|
|
255
|
+
const host = String(info?.hostname ?? "");
|
|
256
|
+
if (info && host === hostname() && pid_is_alive(pid)) {
|
|
257
|
+
throw new GuardError(`state_concurrent_update: refusing --force-unlock for live guard process pid=${pid}`);
|
|
258
|
+
}
|
|
259
|
+
rmSync(lock, { force: true });
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
function build_recomputed_state(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts = {}) {
|
|
263
|
+
// FIX-5 (audit B-3): reuse decision-time fingerprints when provided so a file mutated between
|
|
264
|
+
// decision and write can never be absorbed as "fresh fingerprint + stale decision".
|
|
265
|
+
const fps = opts.fingerprints ?? compute_fingerprints(changeRoot, status);
|
|
266
|
+
const prev = load_state(changeRoot);
|
|
267
|
+
let freshness = "fresh";
|
|
268
|
+
if (prev !== null) {
|
|
269
|
+
const prevFps = isObject(prev.computed_from) ? prev.computed_from : {};
|
|
270
|
+
if (Object.entries(fps).some(([key, value]) => prevFps[key] !== value))
|
|
271
|
+
freshness = "recomputed";
|
|
272
|
+
}
|
|
273
|
+
const amap = artifact_status_map(status);
|
|
274
|
+
const effectiveRoute = effective_route_phase(status, guardRoutePhase, decision);
|
|
275
|
+
return {
|
|
276
|
+
schema_version: SCHEMA_VERSION,
|
|
277
|
+
change_id: change,
|
|
278
|
+
guard_version: GUARD_VERSION,
|
|
279
|
+
updated_at: now(),
|
|
280
|
+
openspec: {
|
|
281
|
+
version: openspec_version(),
|
|
282
|
+
status_fingerprint: fps.openspec_status_fingerprint,
|
|
283
|
+
status_summary: amap,
|
|
284
|
+
},
|
|
285
|
+
superspec: {
|
|
286
|
+
guard_route_phase: effectiveRoute,
|
|
287
|
+
requested_route_phase: normalize_route_phase(guardRoutePhase),
|
|
288
|
+
active_gate: activeGate,
|
|
289
|
+
preset: (opts.config ?? {}).preset,
|
|
290
|
+
preset_upgrade_required: Boolean(opts.preset_upgrade_required),
|
|
291
|
+
state_freshness: freshness,
|
|
292
|
+
last_guard_decision: decision.decision,
|
|
293
|
+
last_block_reasons: (decision.block_reasons ?? []).map((item) => item.code),
|
|
294
|
+
},
|
|
295
|
+
computed_from: {
|
|
296
|
+
openspec_status_command: `openspec status --change ${change} --json`,
|
|
297
|
+
...fps,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
export function prepare_recomputed_state_write(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts = {}) {
|
|
302
|
+
const prev = load_state(changeRoot);
|
|
303
|
+
const expectedFps = prev !== null && isObject(prev.computed_from) ? prev.computed_from : {};
|
|
304
|
+
const state = build_recomputed_state(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts);
|
|
305
|
+
const ledgerEvent = materialize_ledger_event({ change_id: change, kind: "guard_decision", gate: activeGate, decision: decision.decision });
|
|
306
|
+
const ledgerLine = ledger_event_line(ledgerEvent);
|
|
307
|
+
return {
|
|
308
|
+
state,
|
|
309
|
+
state_text: `${JSON.stringify(state, null, 2)}\n`,
|
|
310
|
+
expected_state_fingerprints: expectedFps,
|
|
311
|
+
ledger_event: ledgerEvent,
|
|
312
|
+
ledger_line: ledgerLine,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
export function write_prepared_state_locked(changeRoot, prepared) {
|
|
316
|
+
ensure_state_layout(changeRoot);
|
|
317
|
+
const base = superspec_dir(changeRoot);
|
|
318
|
+
casStateFingerprints(changeRoot, isObject(prepared.expected_state_fingerprints) ? prepared.expected_state_fingerprints : {});
|
|
319
|
+
const stateText = typeof prepared.state_text === "string" ? prepared.state_text : `${JSON.stringify(prepared.state ?? {}, null, 2)}\n`;
|
|
320
|
+
const ledgerText = typeof prepared.ledger_text === "string"
|
|
321
|
+
? prepared.ledger_text
|
|
322
|
+
: `${read_ledger_text(changeRoot)}${typeof prepared.ledger_line === "string" ? prepared.ledger_line : ""}`;
|
|
323
|
+
const ledgerTmp = join(base, "ledger.tmp");
|
|
324
|
+
removeNonFilePath(ledgerTmp);
|
|
325
|
+
const ledgerFd = openSync(ledgerTmp, "w");
|
|
326
|
+
try {
|
|
327
|
+
writeFileSync(ledgerFd, ledgerText, "utf8");
|
|
328
|
+
fsyncSync(ledgerFd);
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
closeSync(ledgerFd);
|
|
332
|
+
}
|
|
333
|
+
renameSync(ledgerTmp, ledger_file(changeRoot));
|
|
334
|
+
fsyncDir(base);
|
|
335
|
+
const tmp = join(base, "superspec-state.tmp");
|
|
336
|
+
removeNonFilePath(tmp);
|
|
337
|
+
const tmpFd = openSync(tmp, "w");
|
|
338
|
+
try {
|
|
339
|
+
writeFileSync(tmpFd, stateText, "utf8");
|
|
340
|
+
fsyncSync(tmpFd);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
closeSync(tmpFd);
|
|
344
|
+
}
|
|
345
|
+
renameSync(tmp, state_file(changeRoot));
|
|
346
|
+
fsyncDir(base);
|
|
347
|
+
}
|
|
348
|
+
export function restore_state_snapshot_locked(changeRoot, snapshot) {
|
|
349
|
+
ensure_state_layout(changeRoot);
|
|
350
|
+
const base = superspec_dir(changeRoot);
|
|
351
|
+
const ledgerTmp = join(base, "ledger.tmp");
|
|
352
|
+
removeNonFilePath(ledgerTmp);
|
|
353
|
+
const ledgerFd = openSync(ledgerTmp, "w");
|
|
354
|
+
try {
|
|
355
|
+
writeFileSync(ledgerFd, snapshot.ledger_text, "utf8");
|
|
356
|
+
fsyncSync(ledgerFd);
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
closeSync(ledgerFd);
|
|
360
|
+
}
|
|
361
|
+
renameSync(ledgerTmp, ledger_file(changeRoot));
|
|
362
|
+
fsyncDir(base);
|
|
363
|
+
if (snapshot.state_text === null) {
|
|
364
|
+
if (existsSync(state_file(changeRoot)))
|
|
365
|
+
rmSync(state_file(changeRoot), { force: true });
|
|
366
|
+
fsyncDir(base);
|
|
367
|
+
if (typeof runtime.on_state_snapshot_restored === "function") {
|
|
368
|
+
runtime.on_state_snapshot_restored({ change_root: changeRoot, state_text: null, ledger_text: snapshot.ledger_text });
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const tmp = join(base, "superspec-state.tmp");
|
|
373
|
+
removeNonFilePath(tmp);
|
|
374
|
+
const tmpFd = openSync(tmp, "w");
|
|
375
|
+
try {
|
|
376
|
+
writeFileSync(tmpFd, snapshot.state_text, "utf8");
|
|
377
|
+
fsyncSync(tmpFd);
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
closeSync(tmpFd);
|
|
381
|
+
}
|
|
382
|
+
renameSync(tmp, state_file(changeRoot));
|
|
383
|
+
fsyncDir(base);
|
|
384
|
+
if (typeof runtime.on_state_snapshot_restored === "function") {
|
|
385
|
+
runtime.on_state_snapshot_restored({ change_root: changeRoot, state_text: snapshot.state_text, ledger_text: snapshot.ledger_text });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export function write_state_atomic(changeRoot, state, opts = {}) {
|
|
389
|
+
const ledgerEvent = opts.ledger_event ? materialize_ledger_event(opts.ledger_event) : null;
|
|
390
|
+
const prepared = {
|
|
391
|
+
state,
|
|
392
|
+
state_text: `${JSON.stringify(state, null, 2)}\n`,
|
|
393
|
+
expected_state_fingerprints: opts.expected_state_fingerprints ?? {},
|
|
394
|
+
ledger_event: ledgerEvent,
|
|
395
|
+
ledger_line: ledgerEvent ? ledger_event_line(ledgerEvent) : "",
|
|
396
|
+
};
|
|
397
|
+
with_state_lock(changeRoot, () => {
|
|
398
|
+
write_prepared_state_locked(changeRoot, prepared);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
export function append_ledger(changeRoot, event) {
|
|
402
|
+
const fullEvent = materialize_ledger_event(event);
|
|
403
|
+
const line = ledger_event_line(fullEvent);
|
|
404
|
+
with_state_lock(changeRoot, () => {
|
|
405
|
+
appendLedgerLineUnlocked(superspec_dir(changeRoot), line);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
// FIX-6 (audit C-2): the fact "evidence X was superseded by Y" must itself be auditable.
|
|
409
|
+
// Every observed supersede pair is appended to the ledger exactly once (dedup inside the
|
|
410
|
+
// state lock), independent of whether the surrounding guard decision allows or blocks.
|
|
411
|
+
export function record_supersede_ledger_events(change, changeRoot, evidences) {
|
|
412
|
+
const observed = evidences.filter((ev) => !ev._invalid
|
|
413
|
+
&& ev.status === "superseded"
|
|
414
|
+
&& typeof ev.supersedes === "string" && ev.supersedes
|
|
415
|
+
&& typeof ev.evidence_id === "string" && ev.evidence_id);
|
|
416
|
+
if (observed.length === 0)
|
|
417
|
+
return;
|
|
418
|
+
with_state_lock(changeRoot, () => {
|
|
419
|
+
const seen = new Set();
|
|
420
|
+
for (const line of read_ledger_text(changeRoot).split("\n")) {
|
|
421
|
+
if (!line.trim())
|
|
422
|
+
continue;
|
|
423
|
+
let event;
|
|
424
|
+
try {
|
|
425
|
+
event = JSON.parse(line);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (!isObject(event) || event.kind !== "evidence_superseded")
|
|
431
|
+
continue;
|
|
432
|
+
seen.add(`${event.superseded_by}\u0000${event.supersedes}`);
|
|
433
|
+
}
|
|
434
|
+
for (const ev of observed) {
|
|
435
|
+
const key = `${ev.evidence_id}\u0000${ev.supersedes}`;
|
|
436
|
+
if (seen.has(key))
|
|
437
|
+
continue;
|
|
438
|
+
seen.add(key);
|
|
439
|
+
const event = materialize_ledger_event({
|
|
440
|
+
change_id: change,
|
|
441
|
+
kind: "evidence_superseded",
|
|
442
|
+
superseded_by: ev.evidence_id,
|
|
443
|
+
supersedes: ev.supersedes,
|
|
444
|
+
gate: ev.gate ?? null,
|
|
445
|
+
supersede_reason: typeof ev.supersede_reason === "string" ? ev.supersede_reason : null,
|
|
446
|
+
});
|
|
447
|
+
appendLedgerLineUnlocked(superspec_dir(changeRoot), ledger_event_line(event));
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
export function recompute_and_write_state_locked(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts = {}) {
|
|
452
|
+
const prepared = prepare_recomputed_state_write(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts);
|
|
453
|
+
write_prepared_state_locked(changeRoot, prepared);
|
|
454
|
+
return prepared.state;
|
|
455
|
+
}
|
|
456
|
+
export function recompute_and_write_state(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts = {}) {
|
|
457
|
+
let state = {};
|
|
458
|
+
with_state_lock(changeRoot, () => {
|
|
459
|
+
const prepared = prepare_recomputed_state_write(change, changeRoot, status, guardRoutePhase, activeGate, decision, opts);
|
|
460
|
+
write_prepared_state_locked(changeRoot, prepared);
|
|
461
|
+
state = prepared.state;
|
|
462
|
+
});
|
|
463
|
+
return state;
|
|
464
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { JsonMap, Reason, TaskInfo } from "./util.ts";
|
|
2
|
+
export type TestContractRecord = {
|
|
3
|
+
row: number;
|
|
4
|
+
test_id: string;
|
|
5
|
+
scenario_ref: string;
|
|
6
|
+
invariant_refs: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function parse_tasks(changeRoot: string): Record<string, TaskInfo>;
|
|
9
|
+
export declare function tasks_structure_hash(changeRoot: string): string | null;
|
|
10
|
+
export declare function splitList(value: string): string[];
|
|
11
|
+
export declare function parse_test_contract_records(changeRoot: string): TestContractRecord[];
|
|
12
|
+
export declare function parse_test_contract_ids(changeRoot: string): Set<string>;
|
|
13
|
+
export declare function test_contract_text(changeRoot: string): string;
|
|
14
|
+
export declare function parse_spec_scenarios(changeRoot: string): string[];
|
|
15
|
+
export declare function test_contract_covers_scenario(changeRoot: string, scenario: string): boolean;
|
|
16
|
+
export declare function test_contract_invariant_refs_by_test(changeRoot: string): Map<string, Set<string>>;
|
|
17
|
+
export declare function task_test_refs(tasks: Record<string, TaskInfo>): Set<string>;
|
|
18
|
+
export declare function write_scope_conflict_reasons(tasks: Record<string, TaskInfo>): Reason[];
|
|
19
|
+
export declare function red_green_test_ids(evidences: JsonMap[]): Set<string>;
|
|
20
|
+
export declare function task_test_evidence(evidences: JsonMap[], taskId: string, semanticStatus: string, gate?: string | null): JsonMap[];
|
|
21
|
+
export declare function task_alternative_verification(evidences: JsonMap[], taskId: string): JsonMap[];
|
|
22
|
+
export declare function evidence_test_id_reasons(evidences: JsonMap[], taskId: string, declared: Set<string>, contractIds: Set<string>, semanticStatus: string): Reason[];
|
|
23
|
+
export declare function declared_test_evidence_reasons(evidences: JsonMap[], taskId: string, declared: Set<string>, semanticStatus: string): Reason[];
|