@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,351 @@
|
|
|
1
|
+
// Manifest-driven install/update/uninstall engine (audit G-1, Phase 5 decision D4, 2026-06-10).
|
|
2
|
+
// Single engine behind `superspec_init` / `--update` / `--uninstall`, per DISTRIBUTION.md §5-§6:
|
|
3
|
+
// - install-map (adapters/codex/install-map.json) is the only source of what gets installed;
|
|
4
|
+
// - install-manifest (.codex/superspec/install-manifest.json) is the only basis for update/uninstall;
|
|
5
|
+
// - manifest sha256 is the managed BASELINE: mismatch means the user edited the file, which the
|
|
6
|
+
// engine must never overwrite or delete (dpkg-style *.new / skip + warn).
|
|
7
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { SCHEMA_VERSION, isObject, sha256_file } from "./util.js";
|
|
11
|
+
function find_package_root(moduleUrl) {
|
|
12
|
+
const moduleDir = dirname(fileURLToPath(moduleUrl));
|
|
13
|
+
for (const candidate of [resolve(moduleDir, ".."), resolve(moduleDir, "..", "..")]) {
|
|
14
|
+
if (existsSync(join(candidate, "package.json")) && existsSync(join(candidate, INSTALL_MAP_REL)))
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
return resolve(moduleDir, "..");
|
|
18
|
+
}
|
|
19
|
+
export const INSTALL_MAP_REL = join("adapters", "codex", "install-map.json");
|
|
20
|
+
export const PACKAGE_ROOT = find_package_root(import.meta.url);
|
|
21
|
+
export const PROJECT_INSTALL_MANIFEST_REL = join(".codex", "superspec", "install-manifest.json");
|
|
22
|
+
export const USER_INSTALL_MANIFEST_REL = join("superspec", "install-manifest.json");
|
|
23
|
+
export const INSTALL_MANIFEST_REL = PROJECT_INSTALL_MANIFEST_REL;
|
|
24
|
+
function package_version(packageRoot) {
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
27
|
+
return typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function load_install_map(packageRoot = PACKAGE_ROOT) {
|
|
34
|
+
const mapPath = join(packageRoot, INSTALL_MAP_REL);
|
|
35
|
+
if (!existsSync(mapPath))
|
|
36
|
+
return { mappings: [], problems: [`install map missing: ${mapPath}`] };
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = JSON.parse(readFileSync(mapPath, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return { mappings: [], problems: [`install map unparsable: ${err.message}`] };
|
|
43
|
+
}
|
|
44
|
+
if (!isObject(raw) || !Array.isArray(raw.mappings))
|
|
45
|
+
return { mappings: [], problems: ["install map must contain a mappings list"] };
|
|
46
|
+
const mappings = [];
|
|
47
|
+
const problems = [];
|
|
48
|
+
raw.mappings.forEach((item, idx) => {
|
|
49
|
+
if (!isObject(item) || typeof item.kind !== "string" || typeof item.source !== "string" || !item.source
|
|
50
|
+
|| typeof item.target !== "string" || !item.target) {
|
|
51
|
+
problems.push(`install map mappings[${idx}] is malformed`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!existsSync(join(packageRoot, item.source))) {
|
|
55
|
+
problems.push(`install map source missing: ${item.source}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
mappings.push({ kind: item.kind, source: item.source, target: item.target });
|
|
59
|
+
});
|
|
60
|
+
return { mappings, problems };
|
|
61
|
+
}
|
|
62
|
+
export function manifest_shape_problems(manifest) {
|
|
63
|
+
// Lightweight mirror of schemas/install-manifest.schema.json (no runtime deps allowed).
|
|
64
|
+
const problems = [];
|
|
65
|
+
if (!isObject(manifest))
|
|
66
|
+
return ["manifest is not an object"];
|
|
67
|
+
if (typeof manifest.superspecVersion !== "string" || !manifest.superspecVersion)
|
|
68
|
+
problems.push("superspecVersion missing");
|
|
69
|
+
if (typeof manifest.installedAt !== "string" || Number.isNaN(Date.parse(manifest.installedAt)))
|
|
70
|
+
problems.push("installedAt missing or not a date-time");
|
|
71
|
+
if (!Number.isInteger(manifest.guardSchemaVersion) || manifest.guardSchemaVersion < 1)
|
|
72
|
+
problems.push("guardSchemaVersion missing");
|
|
73
|
+
if (!["global-bin", "npm-bin", "repo-wrapper"].includes(manifest.guardWiring))
|
|
74
|
+
problems.push("guardWiring must be global-bin, npm-bin, or repo-wrapper");
|
|
75
|
+
if (manifest.installScope !== undefined && !["project", "user"].includes(manifest.installScope))
|
|
76
|
+
problems.push("installScope must be project or user");
|
|
77
|
+
if (!Array.isArray(manifest.createdDirs) || manifest.createdDirs.some((item) => typeof item !== "string" || !item))
|
|
78
|
+
problems.push("createdDirs malformed");
|
|
79
|
+
if (!Array.isArray(manifest.dataGlobs) || manifest.dataGlobs.some((item) => typeof item !== "string" || !item))
|
|
80
|
+
problems.push("dataGlobs malformed");
|
|
81
|
+
if (!Array.isArray(manifest.files)) {
|
|
82
|
+
problems.push("files missing");
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
manifest.files.forEach((entry, idx) => {
|
|
86
|
+
if (!isObject(entry) || typeof entry.path !== "string" || !entry.path
|
|
87
|
+
|| typeof entry.sha256 !== "string" || !/^sha256:[0-9a-f]{64}$/.test(entry.sha256)
|
|
88
|
+
|| typeof entry.managed !== "boolean" || typeof entry.preexisting !== "boolean") {
|
|
89
|
+
problems.push(`files[${idx}] malformed`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return problems;
|
|
94
|
+
}
|
|
95
|
+
export function read_install_manifest(repoRoot, opts = {}) {
|
|
96
|
+
const manifestPath = join(repoRoot, install_manifest_rel(opts.scope ?? "project"));
|
|
97
|
+
if (!existsSync(manifestPath))
|
|
98
|
+
return { manifest: null, problems: [] };
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return { manifest: null, problems: [`install manifest unparsable: ${err.message}`] };
|
|
105
|
+
}
|
|
106
|
+
const shapeProblems = manifest_shape_problems(parsed);
|
|
107
|
+
if (shapeProblems.length > 0)
|
|
108
|
+
return { manifest: null, problems: shapeProblems.map((item) => `install manifest invalid: ${item}`) };
|
|
109
|
+
return { manifest: parsed, problems: [] };
|
|
110
|
+
}
|
|
111
|
+
export function install_manifest_rel(scope = "project") {
|
|
112
|
+
return scope === "user" ? USER_INSTALL_MANIFEST_REL : PROJECT_INSTALL_MANIFEST_REL;
|
|
113
|
+
}
|
|
114
|
+
function scoped_mapping(mapping, scope) {
|
|
115
|
+
if (scope === "project")
|
|
116
|
+
return mapping;
|
|
117
|
+
if (mapping.kind === "wrapper")
|
|
118
|
+
return null;
|
|
119
|
+
const prefix = ".codex/";
|
|
120
|
+
return {
|
|
121
|
+
...mapping,
|
|
122
|
+
target: mapping.target.startsWith(prefix) ? mapping.target.slice(prefix.length) : mapping.target,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function scoped_mappings(mappings, scope) {
|
|
126
|
+
return mappings.flatMap((mapping) => {
|
|
127
|
+
const scoped = scoped_mapping(mapping, scope);
|
|
128
|
+
return scoped === null ? [] : [scoped];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope) {
|
|
132
|
+
const manifest = {
|
|
133
|
+
superspecVersion: package_version(packageRoot),
|
|
134
|
+
installedAt: new Date().toISOString(),
|
|
135
|
+
guardSchemaVersion: SCHEMA_VERSION,
|
|
136
|
+
guardWiring: "global-bin",
|
|
137
|
+
installScope: scope,
|
|
138
|
+
files,
|
|
139
|
+
createdDirs: [...new Set(createdDirs)].sort(),
|
|
140
|
+
dataGlobs: ["**/.superspec"],
|
|
141
|
+
};
|
|
142
|
+
const manifestPath = join(repoRoot, install_manifest_rel(scope));
|
|
143
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
144
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
145
|
+
return manifest;
|
|
146
|
+
}
|
|
147
|
+
function ensure_parent_dirs(repoRoot, targetAbs, createdDirs) {
|
|
148
|
+
const parent = dirname(targetAbs);
|
|
149
|
+
if (existsSync(parent))
|
|
150
|
+
return;
|
|
151
|
+
// Record every directory level this install creates so uninstall can remove exactly those.
|
|
152
|
+
let probe = parent;
|
|
153
|
+
const missing = [];
|
|
154
|
+
while (!existsSync(probe) && probe.startsWith(repoRoot) && probe !== repoRoot) {
|
|
155
|
+
missing.push(probe);
|
|
156
|
+
probe = dirname(probe);
|
|
157
|
+
}
|
|
158
|
+
mkdirSync(parent, { recursive: true });
|
|
159
|
+
for (const dir of missing)
|
|
160
|
+
createdDirs.push(dir.slice(repoRoot.length + 1));
|
|
161
|
+
}
|
|
162
|
+
function install_one(repoRoot, packageRoot, mapping, createdDirs) {
|
|
163
|
+
const sourceAbs = join(packageRoot, mapping.source);
|
|
164
|
+
const targetAbs = join(repoRoot, mapping.target);
|
|
165
|
+
const sourceSha = sha256_file(sourceAbs);
|
|
166
|
+
ensure_parent_dirs(repoRoot, targetAbs, createdDirs);
|
|
167
|
+
copyFileSync(sourceAbs, targetAbs);
|
|
168
|
+
if (mapping.kind === "wrapper")
|
|
169
|
+
chmodSync(targetAbs, 0o755);
|
|
170
|
+
return {
|
|
171
|
+
entry: { path: mapping.target, sha256: sourceSha, managed: true, preexisting: false },
|
|
172
|
+
action: { action: `install ${mapping.target}`, status: "created" },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function install_workflow(repoRoot, opts = {}) {
|
|
176
|
+
const packageRoot = opts.packageRoot ?? PACKAGE_ROOT;
|
|
177
|
+
const scope = opts.scope ?? "project";
|
|
178
|
+
const loaded = load_install_map(packageRoot);
|
|
179
|
+
const mappings = scoped_mappings(loaded.mappings, scope);
|
|
180
|
+
const problems = loaded.problems;
|
|
181
|
+
const actions = [];
|
|
182
|
+
if (problems.length > 0)
|
|
183
|
+
return { actions, problems, manifest: null };
|
|
184
|
+
const files = [];
|
|
185
|
+
const createdDirs = [];
|
|
186
|
+
for (const mapping of mappings) {
|
|
187
|
+
const sourceAbs = join(packageRoot, mapping.source);
|
|
188
|
+
const targetAbs = join(repoRoot, mapping.target);
|
|
189
|
+
const sourceSha = sha256_file(sourceAbs);
|
|
190
|
+
const targetSha = sha256_file(targetAbs);
|
|
191
|
+
if (targetSha === null) {
|
|
192
|
+
const result = install_one(repoRoot, packageRoot, mapping, createdDirs);
|
|
193
|
+
files.push(result.entry);
|
|
194
|
+
actions.push(result.action);
|
|
195
|
+
}
|
|
196
|
+
else if (targetSha === sourceSha) {
|
|
197
|
+
// Identical content: adopt as managed (idempotent re-init).
|
|
198
|
+
files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
|
|
199
|
+
actions.push({ action: `install ${mapping.target}`, status: "ok" });
|
|
200
|
+
}
|
|
201
|
+
else if (opts.force) {
|
|
202
|
+
copyFileSync(targetAbs, `${targetAbs}.bak`);
|
|
203
|
+
copyFileSync(sourceAbs, targetAbs);
|
|
204
|
+
if (mapping.kind === "wrapper")
|
|
205
|
+
chmodSync(targetAbs, 0o755);
|
|
206
|
+
files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
|
|
207
|
+
actions.push({ action: `install ${mapping.target}`, status: "updated", detail: `existing file backed up to ${mapping.target}.bak` });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Pre-existing different file: never overwrite, never delete (DISTRIBUTION §5 red line).
|
|
211
|
+
files.push({ path: mapping.target, sha256: targetSha, managed: false, preexisting: true });
|
|
212
|
+
actions.push({ action: `install ${mapping.target}`, status: "skipped", detail: "pre-existing file with different content kept; rerun with --force to overwrite (backs up *.bak)" });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const manifest = write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope);
|
|
216
|
+
return { actions, problems: [], manifest };
|
|
217
|
+
}
|
|
218
|
+
export function update_workflow(repoRoot, opts = {}) {
|
|
219
|
+
const packageRoot = opts.packageRoot ?? PACKAGE_ROOT;
|
|
220
|
+
const scope = opts.scope ?? "project";
|
|
221
|
+
const actions = [];
|
|
222
|
+
const { manifest: previous, problems: manifestProblems } = read_install_manifest(repoRoot, { scope });
|
|
223
|
+
if (manifestProblems.length > 0)
|
|
224
|
+
return { actions, problems: manifestProblems, manifest: null };
|
|
225
|
+
if (previous === null)
|
|
226
|
+
return { actions, problems: ["install manifest missing; run superspec init before --update"], manifest: null };
|
|
227
|
+
const loaded = load_install_map(packageRoot);
|
|
228
|
+
const mappings = scoped_mappings(loaded.mappings, scope);
|
|
229
|
+
const problems = loaded.problems;
|
|
230
|
+
if (problems.length > 0)
|
|
231
|
+
return { actions, problems, manifest: null };
|
|
232
|
+
const prevByPath = new Map(previous.files.map((entry) => [entry.path, entry]));
|
|
233
|
+
const files = [];
|
|
234
|
+
const createdDirs = [...previous.createdDirs];
|
|
235
|
+
const mappedTargets = new Set();
|
|
236
|
+
for (const mapping of mappings) {
|
|
237
|
+
mappedTargets.add(mapping.target);
|
|
238
|
+
const sourceAbs = join(packageRoot, mapping.source);
|
|
239
|
+
const targetAbs = join(repoRoot, mapping.target);
|
|
240
|
+
const sourceSha = sha256_file(sourceAbs);
|
|
241
|
+
const targetSha = sha256_file(targetAbs);
|
|
242
|
+
const prev = prevByPath.get(mapping.target);
|
|
243
|
+
if (prev?.preexisting) {
|
|
244
|
+
// Was the user's file before we arrived: keep hands off forever.
|
|
245
|
+
files.push({ ...prev, sha256: targetSha ?? prev.sha256 });
|
|
246
|
+
actions.push({ action: `update ${mapping.target}`, status: "skipped", detail: "preexisting file is never touched" });
|
|
247
|
+
}
|
|
248
|
+
else if (targetSha === null) {
|
|
249
|
+
const result = install_one(repoRoot, packageRoot, mapping, createdDirs);
|
|
250
|
+
files.push(result.entry);
|
|
251
|
+
actions.push({ ...result.action, action: `update ${mapping.target}` });
|
|
252
|
+
}
|
|
253
|
+
else if (targetSha === sourceSha) {
|
|
254
|
+
files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
|
|
255
|
+
actions.push({ action: `update ${mapping.target}`, status: "ok" });
|
|
256
|
+
}
|
|
257
|
+
else if (prev && targetSha === prev.sha256) {
|
|
258
|
+
// Managed and unmodified since install: safe to roll forward.
|
|
259
|
+
copyFileSync(sourceAbs, targetAbs);
|
|
260
|
+
if (mapping.kind === "wrapper")
|
|
261
|
+
chmodSync(targetAbs, 0o755);
|
|
262
|
+
files.push({ path: mapping.target, sha256: sourceSha, managed: true, preexisting: false });
|
|
263
|
+
actions.push({ action: `update ${mapping.target}`, status: "updated" });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// User-modified managed file: keep the user's version, ship ours as *.new (dpkg style).
|
|
267
|
+
// Manifest keeps the OLD baseline sha so uninstall still detects the modification.
|
|
268
|
+
copyFileSync(sourceAbs, `${targetAbs}.new`);
|
|
269
|
+
files.push(prev ?? { path: mapping.target, sha256: targetSha, managed: false, preexisting: true });
|
|
270
|
+
actions.push({ action: `update ${mapping.target}`, status: "skipped", detail: `user-modified file kept; new version written to ${mapping.target}.new` });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Mappings removed from the new install map: delete the old managed file only when unmodified.
|
|
274
|
+
for (const prev of prevByPath.values()) {
|
|
275
|
+
if (mappedTargets.has(prev.path) || !prev.managed || prev.preexisting)
|
|
276
|
+
continue;
|
|
277
|
+
const targetAbs = join(repoRoot, prev.path);
|
|
278
|
+
const targetSha = sha256_file(targetAbs);
|
|
279
|
+
if (targetSha === null)
|
|
280
|
+
continue;
|
|
281
|
+
if (targetSha === prev.sha256) {
|
|
282
|
+
unlinkSync(targetAbs);
|
|
283
|
+
actions.push({ action: `update ${prev.path}`, status: "removed", detail: "managed file no longer shipped" });
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
files.push(prev);
|
|
287
|
+
actions.push({ action: `update ${prev.path}`, status: "skipped", detail: "no longer shipped but user-modified; kept" });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const manifest = write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope);
|
|
291
|
+
return { actions, problems: [], manifest };
|
|
292
|
+
}
|
|
293
|
+
function remove_empty_created_dirs(repoRoot, createdDirs, actions) {
|
|
294
|
+
const byDepth = [...new Set(createdDirs)].sort((a, b) => b.split("/").length - a.split("/").length);
|
|
295
|
+
for (const rel of byDepth) {
|
|
296
|
+
const abs = join(repoRoot, rel);
|
|
297
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
298
|
+
continue;
|
|
299
|
+
if (readdirSync(abs).length > 0)
|
|
300
|
+
continue;
|
|
301
|
+
rmdirSync(abs);
|
|
302
|
+
actions.push({ action: `uninstall rmdir ${rel}`, status: "removed" });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function uninstall_workflow(repoRoot, opts = {}) {
|
|
306
|
+
const scope = opts.scope ?? "project";
|
|
307
|
+
const actions = [];
|
|
308
|
+
const { manifest, problems: manifestProblems } = read_install_manifest(repoRoot, { scope });
|
|
309
|
+
if (manifestProblems.length > 0)
|
|
310
|
+
return { actions, problems: manifestProblems, manifest: null };
|
|
311
|
+
if (manifest === null)
|
|
312
|
+
return { actions, problems: ["install manifest missing; nothing to uninstall (manifest is the only removal authority)"], manifest: null };
|
|
313
|
+
for (const entry of manifest.files) {
|
|
314
|
+
const targetAbs = join(repoRoot, entry.path);
|
|
315
|
+
if (!entry.managed || entry.preexisting) {
|
|
316
|
+
actions.push({ action: `uninstall ${entry.path}`, status: "skipped", detail: "preexisting/unmanaged file is never touched" });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const targetSha = sha256_file(targetAbs);
|
|
320
|
+
if (targetSha === null) {
|
|
321
|
+
actions.push({ action: `uninstall ${entry.path}`, status: "ok", detail: "already absent" });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (targetSha !== entry.sha256) {
|
|
325
|
+
actions.push({ action: `uninstall ${entry.path}`, status: "skipped", detail: "user-modified since install; kept" });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (opts.dryRun) {
|
|
329
|
+
actions.push({ action: `uninstall ${entry.path}`, status: "would_remove" });
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
unlinkSync(targetAbs);
|
|
333
|
+
actions.push({ action: `uninstall ${entry.path}`, status: "removed" });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!opts.dryRun) {
|
|
337
|
+
remove_empty_created_dirs(repoRoot, manifest.createdDirs, actions);
|
|
338
|
+
const manifestRel = install_manifest_rel(scope);
|
|
339
|
+
const manifestPath = join(repoRoot, manifestRel);
|
|
340
|
+
if (existsSync(manifestPath)) {
|
|
341
|
+
unlinkSync(manifestPath);
|
|
342
|
+
actions.push({ action: `uninstall ${manifestRel}`, status: "removed" });
|
|
343
|
+
}
|
|
344
|
+
const manifestDir = dirname(manifestPath);
|
|
345
|
+
if (existsSync(manifestDir) && readdirSync(manifestDir).length === 0)
|
|
346
|
+
rmdirSync(manifestDir);
|
|
347
|
+
}
|
|
348
|
+
// .superspec runtime data (dataGlobs) is intentionally untouched: default uninstall keeps all
|
|
349
|
+
// evidence/state; a --purge with preservation bundle is future work (DISTRIBUTION §6.4).
|
|
350
|
+
return { actions, problems: [], manifest };
|
|
351
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { JsonMap, Reason, TaskInfo } from "./util.ts";
|
|
2
|
+
export declare function business_invariants_text(changeRoot: string): string;
|
|
3
|
+
export declare function parse_business_invariant_records(changeRoot: string): JsonMap[];
|
|
4
|
+
export declare function business_invariant_ids(changeRoot: string): Set<string>;
|
|
5
|
+
export declare function hard_business_invariant_ids(changeRoot: string): Set<string>;
|
|
6
|
+
export declare function automated_hard_business_invariant_ids(changeRoot: string): Set<string>;
|
|
7
|
+
export declare function post_implementation_business_invariant_ids(changeRoot: string): Set<string>;
|
|
8
|
+
export declare function human_confirmation_business_invariant_ids(changeRoot: string): Set<string>;
|
|
9
|
+
export declare function business_invariant_validation_reasons(changeRoot: string): Reason[];
|
|
10
|
+
export declare function test_contract_invariant_ids(changeRoot: string): Set<string>;
|
|
11
|
+
export declare function task_invariant_refs(tasks: Record<string, TaskInfo>): Set<string>;
|
|
12
|
+
export declare function evidence_invariant_refs(ev: JsonMap): Set<string>;
|
|
13
|
+
export declare function red_green_invariant_ids(evidences: JsonMap[]): Set<string>;
|
|
14
|
+
export declare function evidence_invariant_ref_reasons(evidences: JsonMap[], taskId: string, declared: Set<string>, validIds: Set<string>, semanticStatus: string): Reason[];
|
|
15
|
+
export declare function evidence_test_contract_invariant_reasons(evidences: JsonMap[], taskId: string, byTest: Map<string, Set<string>>, semanticStatus: string): Reason[];
|
|
16
|
+
export declare function invariant_matrix_coverage_reasons(changeRoot: string, ev: JsonMap, evidences: JsonMap[]): Reason[];
|