@namewta/speculo 1.0.5 → 1.0.7

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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/src/kernel.d.ts +23 -2
  3. package/dist/src/kernel.js +9 -0
  4. package/dist/src/kernel.js.map +1 -1
  5. package/dist/src/ops-resources.d.ts +3 -0
  6. package/dist/src/ops-resources.js +188 -0
  7. package/dist/src/ops-resources.js.map +1 -0
  8. package/dist/src/refresh.js +20 -2
  9. package/dist/src/refresh.js.map +1 -1
  10. package/dist/src/structured.js +7 -0
  11. package/dist/src/structured.js.map +1 -1
  12. package/dist/src/workflows.js +1 -1
  13. package/dist/src/workflows.js.map +1 -1
  14. package/package.json +1 -1
  15. package/template/.speculo/README.md +3 -3
  16. package/template/.speculo/kernel/README.md +3 -0
  17. package/template/.speculo/kernel/checkpoint.schema.json +138 -4
  18. package/template/commands/archive-and-consolidate.md +5 -1
  19. package/template/commands/status.md +2 -2
  20. package/template/workflows/ops/D-project-deploy/D-project-deploy.md +34 -0
  21. package/template/workflows/ops/H-host-manage/H-host-manage.md +30 -0
  22. package/template/workflows/ops/I-initialize/I-initialize.md +28 -0
  23. package/template/workflows/ops/INDEX.md +15 -17
  24. package/template/workflows/ops/README.md +59 -93
  25. package/template/workflows/ops/_state/status.json +15 -3
  26. package/template/workflows/ops/common/CAPABILITIES.md +23 -0
  27. package/template/workflows/ops/common/USAGE.md +112 -0
  28. package/template/workflows/ops/common/examples/README.md +7 -0
  29. package/template/workflows/ops/common/examples/binding.example.json +12 -0
  30. package/template/workflows/ops/common/examples/compose-app.example.json +56 -0
  31. package/template/workflows/ops/common/examples/credential.example.json +9 -0
  32. package/template/workflows/ops/common/examples/environment-request.example.json +7 -0
  33. package/template/workflows/ops/common/examples/register.example.json +44 -0
  34. package/template/workflows/ops/common/examples/shared-allocation.example.json +37 -0
  35. package/template/workflows/ops/common/rules/activation-and-memory.md +5 -20
  36. package/template/workflows/ops/common/rules/persistence-and-secrets.md +27 -0
  37. package/template/workflows/ops/common/rules/recovery.md +15 -0
  38. package/template/workflows/ops/common/rules/shared-services.md +13 -0
  39. package/template/workflows/ops/common/schemas/allocation.schema.json +92 -0
  40. package/template/workflows/ops/common/schemas/approval.schema.json +41 -21
  41. package/template/workflows/ops/common/schemas/binding.schema.json +73 -0
  42. package/template/workflows/ops/common/schemas/deployment.schema.json +244 -0
  43. package/template/workflows/ops/common/schemas/host.schema.json +88 -0
  44. package/template/workflows/ops/common/schemas/plan.schema.json +1573 -0
  45. package/template/workflows/ops/common/schemas/project.schema.json +55 -11
  46. package/template/workflows/ops/common/schemas/spec.schema.json +1032 -0
  47. package/template/workflows/ops/common/schemas/status.schema.json +758 -17
  48. package/template/workflows/ops/common/service-profiles/custom.md +5 -0
  49. package/template/workflows/ops/common/service-profiles/docker-engine.md +11 -0
  50. package/template/workflows/ops/common/service-profiles/minio.md +7 -0
  51. package/template/workflows/ops/common/service-profiles/mysql.md +7 -0
  52. package/template/workflows/ops/common/service-profiles/redis.md +7 -0
  53. package/template/workflows/ops/common/templates/CONTROLLER-RECORD.md +13 -0
  54. package/template/workflows/ops/common/templates/EXECUTION-PLAN.md +15 -0
  55. package/template/workflows/ops/common/templates/HOST-README.md +11 -0
  56. package/template/workflows/ops/common/templates/PROJECT-README.md +15 -0
  57. package/template/workflows/ops/common/tests/test_ops.mjs +752 -0
  58. package/template/workflows/ops/common/tools/bootstrap.ps1 +18 -0
  59. package/template/workflows/ops/common/tools/bootstrap.sh +27 -0
  60. package/template/workflows/ops/common/tools/demo-local.mjs +101 -0
  61. package/template/workflows/ops/common/tools/ops.mjs +4 -0
  62. package/template/workflows/ops/common/tools/opslib/agent.mjs +873 -0
  63. package/template/workflows/ops/common/tools/opslib/cli.mjs +311 -0
  64. package/template/workflows/ops/common/tools/opslib/core.mjs +393 -0
  65. package/template/workflows/ops/common/tools/opslib/docs.mjs +252 -0
  66. package/template/workflows/ops/common/tools/opslib/execution.mjs +378 -0
  67. package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +109 -0
  68. package/template/workflows/ops/common/tools/opslib/model.mjs +272 -0
  69. package/template/workflows/ops/common/tools/opslib/native_windows.mjs +36 -0
  70. package/template/workflows/ops/common/tools/opslib/planner.mjs +687 -0
  71. package/template/workflows/ops/common/tools/opslib/services.mjs +76 -0
  72. package/template/workflows/ops/common/tools/opslib/sources.mjs +56 -0
  73. package/template/workflows/ops/common/tools/opslib/transport.mjs +127 -0
  74. package/template/workflows/ops/common/tools/validate-ops.mjs +43 -1012
  75. package/template/workflows/ops/manifest.json +60 -1
  76. package/template/workflows/ops/runtime-contract.json +1 -6
  77. package/template/workflows/ops/A-archive-and-learn/A-archive-and-learn.md +0 -73
  78. package/template/workflows/ops/A-archive-and-learn/promotion-plan-template.md +0 -41
  79. package/template/workflows/ops/A-archive-and-learn/retrospective-template.md +0 -53
  80. package/template/workflows/ops/E-execute-and-stabilize/E-execute-and-stabilize.md +0 -82
  81. package/template/workflows/ops/E-execute-and-stabilize/attempt-summary-template.md +0 -44
  82. package/template/workflows/ops/E-execute-and-stabilize/diagnosis-template.md +0 -24
  83. package/template/workflows/ops/E-execute-and-stabilize/handoff-template.md +0 -32
  84. package/template/workflows/ops/E-execute-and-stabilize/rollback-template.md +0 -26
  85. package/template/workflows/ops/E-execute-and-stabilize/verification-state-template.json +0 -30
  86. package/template/workflows/ops/E-execute-and-stabilize/verification-template.md +0 -55
  87. package/template/workflows/ops/H-computer-hygiene/H-computer-hygiene.md +0 -105
  88. package/template/workflows/ops/H-computer-hygiene/examples/README.md +0 -9
  89. package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.environment.md +0 -45
  90. package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.json +0 -970
  91. package/template/workflows/ops/H-computer-hygiene/examples/linux/2026-09-14.md +0 -216
  92. package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.environment.md +0 -45
  93. package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.json +0 -970
  94. package/template/workflows/ops/H-computer-hygiene/examples/macos/2026-09-14.md +0 -216
  95. package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.environment.md +0 -45
  96. package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.json +0 -969
  97. package/template/workflows/ops/H-computer-hygiene/examples/windows/2026-09-14.md +0 -216
  98. package/template/workflows/ops/H-computer-hygiene/references/00-sources.md +0 -58
  99. package/template/workflows/ops/H-computer-hygiene/references/01-safety.md +0 -52
  100. package/template/workflows/ops/H-computer-hygiene/references/10-windows.md +0 -47
  101. package/template/workflows/ops/H-computer-hygiene/references/11-macos.md +0 -46
  102. package/template/workflows/ops/H-computer-hygiene/references/12-linux.md +0 -13
  103. package/template/workflows/ops/H-computer-hygiene/references/20-toolchains.md +0 -83
  104. package/template/workflows/ops/H-computer-hygiene/references/21-path-migration.md +0 -64
  105. package/template/workflows/ops/H-computer-hygiene/references/22-repositories.md +0 -35
  106. package/template/workflows/ops/H-computer-hygiene/references/30-software-removal.md +0 -41
  107. package/template/workflows/ops/H-computer-hygiene/references/40-report-spec.md +0 -54
  108. package/template/workflows/ops/H-computer-hygiene/references/50-speculo-integration.md +0 -26
  109. package/template/workflows/ops/H-computer-hygiene/references/source-register.json +0 -406
  110. package/template/workflows/ops/H-computer-hygiene/rules/catalog.json +0 -191
  111. package/template/workflows/ops/H-computer-hygiene/scripts/hygiene.py +0 -1348
  112. package/template/workflows/ops/H-computer-hygiene/scripts/run.ps1 +0 -7
  113. package/template/workflows/ops/H-computer-hygiene/scripts/run.sh +0 -5
  114. package/template/workflows/ops/H-computer-hygiene/templates/config.example.json +0 -13
  115. package/template/workflows/ops/H-computer-hygiene/templates/maven-settings.fragment.xml +0 -6
  116. package/template/workflows/ops/H-computer-hygiene/templates/native-evidence.example.json +0 -19
  117. package/template/workflows/ops/H-computer-hygiene/tests/cli_smoke.py +0 -65
  118. package/template/workflows/ops/H-computer-hygiene/tests/make_examples.py +0 -114
  119. package/template/workflows/ops/H-computer-hygiene/tests/test_hygiene.py +0 -486
  120. package/template/workflows/ops/I-intake-and-assess/I-intake-and-assess.md +0 -73
  121. package/template/workflows/ops/I-intake-and-assess/change-status-template.json +0 -26
  122. package/template/workflows/ops/I-intake-and-assess/collector-catalog.md +0 -28
  123. package/template/workflows/ops/I-intake-and-assess/deployment-dossier-template.md +0 -62
  124. package/template/workflows/ops/I-intake-and-assess/global-change-status-template.json +0 -26
  125. package/template/workflows/ops/I-intake-and-assess/project-detection.md +0 -38
  126. package/template/workflows/ops/I-intake-and-assess/request-template.md +0 -44
  127. package/template/workflows/ops/I-intake-and-assess/system-report-template.md +0 -34
  128. package/template/workflows/ops/I-intake-and-assess/target-profile-template.json +0 -24
  129. package/template/workflows/ops/P-plan-and-approve/P-plan-and-approve.md +0 -72
  130. package/template/workflows/ops/P-plan-and-approve/plan-review-template.md +0 -78
  131. package/template/workflows/ops/_state/archive/.gitkeep +0 -1
  132. package/template/workflows/ops/_state/changes/.gitkeep +0 -1
  133. package/template/workflows/ops/common/rules/artifact-contract.md +0 -38
  134. package/template/workflows/ops/common/rules/closure-and-learning.md +0 -27
  135. package/template/workflows/ops/common/rules/evidence-and-redaction.md +0 -22
  136. package/template/workflows/ops/common/rules/execution-loop.md +0 -27
  137. package/template/workflows/ops/common/rules/path-and-scope-contract.md +0 -21
  138. package/template/workflows/ops/common/rules/plan-and-approval.md +0 -25
  139. package/template/workflows/ops/common/rules/project-and-change-scope.md +0 -20
  140. package/template/workflows/ops/common/rules/target-profile-and-release-gates.md +0 -44
  141. package/template/workflows/ops/common/schemas/attempt.schema.json +0 -42
  142. package/template/workflows/ops/common/schemas/change-status.schema.json +0 -43
  143. package/template/workflows/ops/common/schemas/deployment-model.schema.json +0 -26
  144. package/template/workflows/ops/common/schemas/implementation-plan.schema.json +0 -221
  145. package/template/workflows/ops/common/schemas/inventory-snapshot.schema.json +0 -31
  146. package/template/workflows/ops/common/schemas/journal-event.schema.json +0 -22
  147. package/template/workflows/ops/common/schemas/promotion-approval.schema.json +0 -20
  148. package/template/workflows/ops/common/schemas/promotion-manifest.schema.json +0 -22
  149. package/template/workflows/ops/common/schemas/target-profile.schema.json +0 -32
  150. package/template/workflows/ops/common/schemas/verification-state.schema.json +0 -30
  151. package/template/workflows/ops/common/tools/close-change.mjs +0 -177
@@ -0,0 +1,393 @@
1
+ /** OPS resource runtime primitives. Node standard library only. */
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import {
4
+ chmodSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
5
+ openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync,
6
+ } from "node:fs";
7
+ import { hostname } from "node:os";
8
+ import { dirname, join, posix, win32 } from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+
11
+ export const VERSION = "2.2.0";
12
+ const ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13
+ export const SECRET_RE = /\{\{credential:([a-z0-9-]+)@([1-9][0-9]*):([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
14
+
15
+ export class OpsError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "OpsError";
19
+ }
20
+ }
21
+
22
+ export class UnknownResult extends OpsError {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "UnknownResult";
26
+ }
27
+ }
28
+
29
+ export function now() {
30
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
31
+ }
32
+
33
+ function sortKeys(value) {
34
+ if (Array.isArray(value)) return value.map(sortKeys);
35
+ if (value && typeof value === "object") {
36
+ return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
37
+ }
38
+ return value;
39
+ }
40
+
41
+ export function assertJsonSafe(value) {
42
+ if (typeof value === "number" && !Number.isFinite(value)) {
43
+ throw new TypeError("Out of range float values are not JSON compliant");
44
+ }
45
+ if (Array.isArray(value)) value.forEach(assertJsonSafe);
46
+ else if (value && typeof value === "object") Object.values(value).forEach(assertJsonSafe);
47
+ }
48
+
49
+ export function canonical(value) {
50
+ assertJsonSafe(value);
51
+ return Buffer.from(JSON.stringify(sortKeys(value)), "utf8");
52
+ }
53
+
54
+ export function digest(value) {
55
+ const data = Buffer.isBuffer(value) ? value : canonical(value);
56
+ return createHash("sha256").update(data).digest("hex");
57
+ }
58
+
59
+ export function newId(prefix) {
60
+ const d = new Date();
61
+ const ts = [
62
+ d.getUTCFullYear(),
63
+ String(d.getUTCMonth() + 1).padStart(2, "0"),
64
+ String(d.getUTCDate()).padStart(2, "0"),
65
+ String(d.getUTCHours()).padStart(2, "0"),
66
+ String(d.getUTCMinutes()).padStart(2, "0"),
67
+ String(d.getUTCSeconds()).padStart(2, "0"),
68
+ ].join("");
69
+ return `${prefix}-${ts}-${randomBytes(4).toString("hex")}`;
70
+ }
71
+
72
+ export function identifier(value, label = "id") {
73
+ if (typeof value !== "string" || !ID_RE.test(value) || value.length > 80) {
74
+ throw new OpsError(`${label}: expected lowercase kebab id (1..80 characters)`);
75
+ }
76
+ if (/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/.test(value)) {
77
+ throw new OpsError(`${label}: Windows reserved name`);
78
+ }
79
+ return value;
80
+ }
81
+
82
+ function pyList(items) {
83
+ return `[${items.map((x) => `'${x}'`).join(", ")}]`;
84
+ }
85
+
86
+ export function exact(value, allowed, required, label) {
87
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
88
+ throw new OpsError(`${label}: expected object`);
89
+ }
90
+ const unknown = Object.keys(value).filter((k) => !allowed.has(k)).sort();
91
+ if (unknown.length) throw new OpsError(`${label}: unknown fields: ${pyList(unknown)}`);
92
+ const missing = [...required].filter((k) => !(k in value)).sort();
93
+ if (missing.length) throw new OpsError(`${label}: missing fields: ${pyList(missing)}`);
94
+ }
95
+
96
+ function pyRepr(value) {
97
+ if (typeof value !== "string") return String(value);
98
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
99
+ }
100
+
101
+ export function relative(value) {
102
+ if (typeof value !== "string" || !value || value.includes("\\") || value.includes("\x00") || value.includes(":")) {
103
+ throw new OpsError(`unsafe relative path: ${pyRepr(value)}`);
104
+ }
105
+ if (value.startsWith("/") || value.split("/").some((x) => x === "" || x === "." || x === "..")) {
106
+ throw new OpsError(`unsafe relative path: ${pyRepr(value)}`);
107
+ }
108
+ return value;
109
+ }
110
+
111
+ function adapter(platform) {
112
+ return platform === "windows" ? win32 : posix;
113
+ }
114
+
115
+ export function rootPath(value, platform) {
116
+ const p = adapter(platform);
117
+ if (typeof value !== "string" || value.includes("\x00") || !p.isAbsolute(value) || value.split(/[\\/]/).includes("..")) {
118
+ throw new OpsError("host root must be an absolute, non-traversing path");
119
+ }
120
+ const n = p.normalize(value);
121
+ if (platform === "windows") {
122
+ if (n.startsWith("\\\\") || p.parse(n).root === n || n.slice(2).includes(":")) {
123
+ throw new OpsError("UNC, drive root and ADS paths are not host roots");
124
+ }
125
+ const parts = n.split(/[\\/]/).filter(Boolean);
126
+ if (parts.length < 2) throw new OpsError("UNC, drive root and ADS paths are not host roots");
127
+ const lower = n.toLowerCase().replace(/\\+$/, "");
128
+ if (["c:\\windows", "c:\\program files", "c:\\users", "c:\\programdata"].includes(lower)) {
129
+ throw new OpsError("a system directory cannot be host_root");
130
+ }
131
+ return n;
132
+ }
133
+ if (["/", "/etc", "/usr", "/var", "/home", "/root", "/tmp", "/srv", "/opt", "/mnt"].includes(n)) {
134
+ throw new OpsError("host_root must be an OPS-specific child directory");
135
+ }
136
+ return n;
137
+ }
138
+
139
+ export function targetJoin(host, ...parts) {
140
+ const p = adapter(host.platform);
141
+ let cur = host.root;
142
+ for (const part of parts) cur = p.join(cur, ...relative(part).split("/"));
143
+ return cur;
144
+ }
145
+
146
+ export function within(path, root, platform) {
147
+ const p = adapter(platform);
148
+ if (!p.isAbsolute(path) || path.split(/[\\/]/).includes("..")) return false;
149
+ const rel = p.relative(root, path);
150
+ if (!rel || rel.startsWith("..") || p.isAbsolute(rel)) return false;
151
+ return true;
152
+ }
153
+
154
+ export function noSymlinks(path, { allowMissing = true } = {}) {
155
+ const chain = [];
156
+ let cur = path;
157
+ while (true) {
158
+ chain.unshift(cur);
159
+ const parent = dirname(cur);
160
+ if (parent === cur) break;
161
+ cur = parent;
162
+ }
163
+ for (const q of chain) {
164
+ let st;
165
+ try {
166
+ st = lstatSync(q);
167
+ } catch (error) {
168
+ if (error.code === "ENOENT") {
169
+ if (allowMissing) continue;
170
+ throw new OpsError(`missing path: ${q}`);
171
+ }
172
+ throw error;
173
+ }
174
+ if (st.isSymbolicLink()) throw new OpsError(`symlink/reparse point rejected: ${q}`);
175
+ }
176
+ }
177
+
178
+ export function secure(path, directory = false) {
179
+ if (process.platform !== "win32") {
180
+ chmodSync(path, directory ? 0o700 : 0o600);
181
+ return;
182
+ }
183
+ const who = spawnSync("whoami", ["/user", "/fo", "csv", "/nh"], { encoding: "utf8" });
184
+ if (who.status !== 0) throw new Error(who.stderr);
185
+ const sid = who.stdout.trim().split(",").at(-1).replaceAll('"', "");
186
+ const flags = directory ? "(OI)(CI)F" : "F";
187
+ const ic = spawnSync("icacls", [path, "/inheritance:r", "/grant:r", `*${sid}:${flags}`, "*S-1-5-18:F"], { encoding: "utf8" });
188
+ if (ic.status !== 0) throw new Error(ic.stderr);
189
+ }
190
+
191
+ export function privateDir(path) {
192
+ noSymlinks(path);
193
+ const missing = [];
194
+ let p = path;
195
+ while (!existsSync(p)) {
196
+ missing.push(p);
197
+ const next = dirname(p);
198
+ if (next === p) break;
199
+ p = next;
200
+ }
201
+ for (const d of missing.reverse()) {
202
+ mkdirSync(d, { mode: 0o700 });
203
+ secure(d, true);
204
+ }
205
+ if (existsSync(path)) secure(path, true);
206
+ }
207
+
208
+ export function atomicWrite(path, data, mode = 0o600, { exclusive = false } = {}) {
209
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(String(data), "utf8");
210
+ noSymlinks(path);
211
+ privateDir(dirname(path));
212
+ if (exclusive && existsSync(path)) throw new OpsError(`immutable artifact already exists: ${path}`);
213
+ const tmp = join(dirname(path), `.ops-write-${randomBytes(6).toString("hex")}`);
214
+ const fd = openSync(tmp, "w");
215
+ try {
216
+ writeSync(fd, buf);
217
+ fsyncSync(fd);
218
+ } finally {
219
+ closeSync(fd);
220
+ }
221
+ try {
222
+ if (process.platform === "win32") secure(tmp);
223
+ else chmodSync(tmp, mode);
224
+ if (exclusive) {
225
+ try {
226
+ linkSync(tmp, path);
227
+ } catch (error) {
228
+ if (error.code === "EEXIST") throw new OpsError(`immutable artifact already exists: ${path}`);
229
+ throw error;
230
+ }
231
+ unlinkSync(tmp);
232
+ } else {
233
+ try {
234
+ renameSync(tmp, path);
235
+ } catch (error) {
236
+ if (process.platform === "win32" && (error.code === "EEXIST" || error.code === "EPERM" || error.code === "EACCES")) {
237
+ unlinkSync(path);
238
+ renameSync(tmp, path);
239
+ } else {
240
+ throw error;
241
+ }
242
+ }
243
+ }
244
+ if (process.platform !== "win32") chmodSync(path, mode);
245
+ } catch (error) {
246
+ try { unlinkSync(tmp); } catch {}
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ export function writeJson(path, value, { exclusive = false } = {}) {
252
+ assertJsonSafe(value);
253
+ atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`, 0o600, { exclusive });
254
+ }
255
+
256
+ function assertNoDuplicateKeys(text) {
257
+ let i = 0;
258
+ const n = text.length;
259
+ const skip = () => { while (i < n && /\s/.test(text[i])) i++; };
260
+ function parseString() {
261
+ i++;
262
+ while (i < n) {
263
+ if (text[i] === "\\") { i += 2; continue; }
264
+ if (text[i] === '"') { i++; return; }
265
+ i++;
266
+ }
267
+ throw new Error("Unterminated string");
268
+ }
269
+ function parseValue() {
270
+ skip();
271
+ const c = text[i];
272
+ if (c === "{") return parseObject();
273
+ if (c === "[") {
274
+ i++; skip();
275
+ if (text[i] === "]") { i++; return; }
276
+ while (true) {
277
+ parseValue(); skip();
278
+ if (text[i] === ",") { i++; continue; }
279
+ if (text[i] === "]") { i++; return; }
280
+ throw new Error("Invalid array");
281
+ }
282
+ }
283
+ if (c === '"') return parseString();
284
+ if (c === "t" || c === "f" || c === "n") { while (i < n && /[a-z]/.test(text[i])) i++; return; }
285
+ if (c === "-" || (c >= "0" && c <= "9")) { while (i < n && /[0-9eE+.\-]/.test(text[i])) i++; return; }
286
+ throw new Error("Invalid JSON");
287
+ }
288
+ function parseObject() {
289
+ i++; skip();
290
+ const seen = new Set();
291
+ if (text[i] === "}") { i++; return; }
292
+ while (true) {
293
+ skip();
294
+ if (text[i] !== '"') throw new Error("Expected key");
295
+ const start = i;
296
+ parseString();
297
+ const key = JSON.parse(text.slice(start, i));
298
+ if (seen.has(key)) throw new Error(`duplicate JSON key: ${key}`);
299
+ seen.add(key);
300
+ skip();
301
+ if (text[i] !== ":") throw new Error("Expected colon");
302
+ i++;
303
+ parseValue();
304
+ skip();
305
+ if (text[i] === ",") { i++; continue; }
306
+ if (text[i] === "}") { i++; return; }
307
+ throw new Error("Expected comma");
308
+ }
309
+ }
310
+ parseValue();
311
+ }
312
+
313
+ export function readJson(path) {
314
+ noSymlinks(path, { allowMissing: false });
315
+ const text = readFileSync(path, "utf8");
316
+ try {
317
+ if (/\bNaN\b|\bInfinity\b/.test(text.replace(/"(?:\\.|[^"\\])*"/g, '""'))) {
318
+ throw new Error("nonfinite");
319
+ }
320
+ assertNoDuplicateKeys(text);
321
+ return JSON.parse(text);
322
+ } catch (error) {
323
+ throw new OpsError(`invalid JSON: ${path}: ${error.message}`);
324
+ }
325
+ }
326
+
327
+ export function withLock(path, owner, fn) {
328
+ privateDir(dirname(path));
329
+ try {
330
+ mkdirSync(path, { mode: 0o700 });
331
+ } catch (error) {
332
+ if (error.code === "EEXIST") throw new OpsError(`lock-held: ${path}; inspect owner, never auto-break`);
333
+ throw error;
334
+ }
335
+ writeJson(join(path, "owner.json"), { ...owner, pid: process.pid, machine: hostname(), at: now() });
336
+ try {
337
+ return fn();
338
+ } finally {
339
+ try { unlinkSync(join(path, "owner.json")); } catch {}
340
+ try { rmSync(path, { recursive: true, force: true }); } catch {}
341
+ }
342
+ }
343
+
344
+ export function redact(text, secrets) {
345
+ for (const value of [...new Set(secrets)].sort((a, b) => b.length - a.length)) {
346
+ if (value) text = text.split(value).join("[REDACTED]");
347
+ }
348
+ return text.replace(/(password|passwd|token|secret|access_key)(\s*[=:]\s*)[^\s,;]+/gi, "$1$2[REDACTED]");
349
+ }
350
+
351
+ export function credentialsIn(value) {
352
+ const found = new Set();
353
+ const re = new RegExp(SECRET_RE.source, "g");
354
+ let m;
355
+ const blob = JSON.stringify(value);
356
+ while ((m = re.exec(blob))) found.add(`${m[1]}@${m[2]}`);
357
+ return found;
358
+ }
359
+
360
+ export function resolveSecrets(value, ledger) {
361
+ if (typeof value === "string") {
362
+ return value.replace(new RegExp(SECRET_RE.source, "g"), (_w, cid, version, field) => {
363
+ const result = ledger?.entries?.[cid]?.[version]?.values?.[field];
364
+ if (result === undefined) throw new OpsError(`missing credential: ${cid}@${version}:${field}`);
365
+ if (typeof result !== "string") throw new OpsError("credential values must be strings");
366
+ return result;
367
+ });
368
+ }
369
+ if (Array.isArray(value)) return value.map((v) => resolveSecrets(v, ledger));
370
+ if (value && typeof value === "object") {
371
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveSecrets(v, ledger)]));
372
+ }
373
+ return value;
374
+ }
375
+
376
+ export function emptyStatus() {
377
+ return {
378
+ schema_version: 3,
379
+ workflow: "ops",
380
+ revision: 0,
381
+ controller: null,
382
+ hosts: {},
383
+ projects: {},
384
+ deployments: {},
385
+ allocations: {},
386
+ bindings: {},
387
+ releases: {},
388
+ policies: { server_readme_credentials: false, server_operations: true, strict_docker_root: true },
389
+ updated_at: null,
390
+ };
391
+ }
392
+
393
+ export { withLock as lock };
@@ -0,0 +1,252 @@
1
+ /** Deterministic server/controller documentation and protected plaintext delivery bundles. */
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { digest, now, OpsError, targetJoin } from "./core.mjs";
5
+
6
+ export const STANDARD = `# OPS 部署与持久化规范
7
+
8
+ APP 和公共服务同级:host_root/project_id。明确启用多实例时使用 project_id/instances/environment/instance。
9
+
10
+ 每个项目的 compose 或 service、env、config、data、logs、backups、releases 和 README 聚合在该项目目录。业务数据必须位于 data/component/purpose;不能回退到临时目录、代码目录、用户默认目录、匿名卷或命名卷。Docker 全局 data-root 另登记在 host_root/_runtime/docker;既有引擎迁移必须单独审批。
11
+
12
+ README 记录实际版本、来源、主机、时间、路径、依赖、启停和备份恢复。它默认不含密码。OPERATIONS.md 和部署机明文账本按策略保存真实账号密码,权限必须限制;不得进入 Git、镜像构建上下文、Web 静态目录或普通日志。
13
+
14
+ 共享服务拥有物理数据;APP 只拥有获批的逻辑数据库、桶、命名空间和应用账号。复制 APP 目录不是共享依赖完整备份。卸载 APP 不删除公共服务、共享网络、逻辑资源或任何数据。单 APP 回滚不允许恢复整个共享实例。
15
+
16
+ 旧环境默认值恢复和健康验证失败不得清理原环境。缓存隔离不等于释放空间;禁止把 data/env/backups 或数据库持久日志当成垃圾。
17
+
18
+ 计划 → 明确批准 → 执行 → 实际验证 → 双边文档回执完成。远程断线意味着结果未知,不能重跑迁移或重新生成密码。
19
+ `;
20
+
21
+ function htmlEscape(s) {
22
+ return String(s).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
23
+ }
24
+
25
+ export function code(value) {
26
+ return "<code>" + htmlEscape(value).replaceAll("\n", "<br>") + "</code>";
27
+ }
28
+
29
+ export function block(value, language = "json") {
30
+ const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
31
+ const ticks = "`".repeat(Math.max(3, Math.max(0, ...(text.match(/`+/g) || []).map((x) => x.length)) + 1));
32
+ return ticks + language + "\n" + text + "\n" + ticks + "\n";
33
+ }
34
+
35
+ export function pathFor(status, dep, relativeName) {
36
+ return targetJoin({ ...status.hosts[dep.host_id], root: dep.root }, relativeName);
37
+ }
38
+
39
+ export function credentialRefs(status, dep) {
40
+ const refs = new Set(dep.credential_refs);
41
+ for (const b of Object.values(status.bindings)) {
42
+ if (b.status === "active" && b.consumer_deployment_id === dep.deployment_id) refs.add(b.credential_ref);
43
+ }
44
+ for (const a of Object.values(status.allocations)) {
45
+ if (a.provider_deployment_id === dep.deployment_id && a.status !== "retired") refs.add(a.credential_ref);
46
+ }
47
+ return [...refs].sort();
48
+ }
49
+
50
+ export function credentialsText(refs, ledger) {
51
+ const out = ["## 明文账号密码\n", "本文件是受限明文交付,不是脱敏报告。只向获授权管理员及对应运行账户开放。\n"];
52
+ if (!refs.length) return [...out, "本部署没有登记密码凭据。密钥认证本身不存在登录密码;未知旧密码不能伪造。\n"].join("\n");
53
+ for (const ref of refs) {
54
+ const [cid, v] = ref.split("@");
55
+ const item = ledger.entries?.[cid]?.[v];
56
+ if (!item) throw new OpsError("document delivery blocked by missing credential: " + ref);
57
+ out.push("### " + ref + "\n", code(item.purpose) + "\n");
58
+ for (const [key, value] of Object.entries(item.values)) out.push("**" + key + "**\n", block(value, "text"));
59
+ }
60
+ return out.join("\n");
61
+ }
62
+
63
+ export function dependencies(status, dep) {
64
+ const lines = ["## 依赖与数据归属\n"];
65
+ const bindings = Object.values(status.bindings).filter((b) => b.consumer_deployment_id === dep.deployment_id && b.status === "active");
66
+ if (!bindings.length) lines.push("未登记共享或外部服务依赖;专用组件的数据仍归本项目目录。\n");
67
+ for (const b of bindings) {
68
+ lines.push("### " + b.component + " / " + b.binding_id + "\n", `模式:${b.mode};端点:${code(b.endpoint)};网络:${code(b.network)}。\n`);
69
+ if (b.mode === "shared") {
70
+ const a = status.allocations[b.allocation_id];
71
+ const provider = status.deployments[b.provider_deployment_id];
72
+ lines.push(`提供者:${provider.project_id} / ${provider.deployment_id},主机 ${provider.host_id};服务目录 ${code(provider.root)}。\n`);
73
+ lines.push(`逻辑资源:${code(a.resource_name)}(${a.resource_kind});数据组 ${a.data_group};账号版本 ${a.credential_ref};恢复粒度 ${a.recovery_scope}。\n`);
74
+ for (const storage of provider.storage) lines.push(`提供者物理路径:${code(storage.path)}(${storage.component}/${storage.purpose})。\n`);
75
+ lines.push("公共服务的物理数据不复制到 APP 目录;单 APP 恢复不得覆盖其他消费者。\n");
76
+ }
77
+ }
78
+ const consumers = Object.values(status.bindings).filter((b) => b.provider_deployment_id === dep.deployment_id && b.status === "active");
79
+ if (consumers.length) {
80
+ lines.push("## 公共服务消费者\n\n| APP 实例 | 所在主机 | 分配 | 凭据版本 |\n|---|---|---|---|\n");
81
+ for (const b of consumers) {
82
+ const consumer = status.deployments[b.consumer_deployment_id];
83
+ lines.push(`| ${consumer.deployment_id} | ${consumer.host_id} | ${b.allocation_id} | ${b.credential_ref} |\n`);
84
+ }
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+
89
+ export function deploymentReadme(status, dep, runId, generatedAt, { ledger = null, includeCredentials = false, controller = false } = {}) {
90
+ const host = status.hosts[dep.host_id];
91
+ const project = status.projects[dep.project_id];
92
+ const out = [
93
+ `# ${project.display_name} — ${dep.deployment_id}\n`,
94
+ `文档代次:${runId};生成时间(UTC):${generatedAt}。部署验证与双边交付以部署机对应运行记录和 docs-receipt.json 为准。\n`,
95
+ "## 部署事实\n\n| 项目 | 值 |\n|---|---|",
96
+ `| 主机 / 账户 | ${host.host_id} / ${code(host.connection.username ?? "本机执行账户")} |`,
97
+ `| 连接 | ${code(host.connection.hostname ?? "local")},SSH 端口 ${host.connection.port ?? "不适用"} |`,
98
+ `| 类型 / 方式 | ${project.kind} / ${dep.method} |`,
99
+ `| 生命周期状态 | ${dep.status} |`,
100
+ `| 环境 / 实例 | ${dep.environment} / ${dep.instance} |`,
101
+ `| 计划版本 | ${code(dep.version)} |`,
102
+ `| 最后运行验证版本 | ${code(dep.observed_version || "尚未验证")} |`,
103
+ `| 首次部署时间(UTC) | ${dep.installed_at || "尚未完成"} |`,
104
+ `| 最近部署验证时间(UTC) | ${dep.updated_at || "尚未验证"} |`,
105
+ `| 项目根目录 | ${code(dep.root)} |`,
106
+ `| 来源 | ${code(dep.source.location)} |`,
107
+ `| 固定来源版本 | ${code(dep.source.revision)} |\n`,
108
+ "## 目录与持久化\n",
109
+ `部署定义:${code(dep.method === "compose" ? pathFor(status, dep, "compose/compose.yaml") : pathFor(status, dep, "service"))}。\n`,
110
+ `环境文件:${code(pathFor(status, dep, "env"))};配置:${code(pathFor(status, dep, "config"))}。\n`,
111
+ `日志:${code(pathFor(status, dep, "logs"))};发布历史:${code(pathFor(status, dep, "releases"))};备份:${code(pathFor(status, dep, "backups"))}。\n`,
112
+ "| 组件 | 用途 | 自有持久化路径 |\n|---|---|---|",
113
+ ];
114
+ for (const p of dep.storage) out.push(`| ${p.component} | ${p.purpose} | ${code(p.path)} |`);
115
+ if (!dep.storage.length) out.push("| — | 无登记的业务持久化内容 | 不应生成匿名数据路径 |");
116
+ out.push("\n" + dependencies(status, dep), "## 日常操作\n");
117
+ for (const [title, key] of [["启动/运行", "start"], ["停止(保留数据)", "stop"], ["查看与验证", "verify"]]) {
118
+ out.push("### " + title + "\n");
119
+ if (!dep.commands[key].length) out.push("不适用或由已批准运行计划执行。\n");
120
+ for (const argv of dep.commands[key]) out.push(block(argv));
121
+ }
122
+ out.push(
123
+ "## 更新步骤\n",
124
+ "先盘点主机与依赖;固定版本;检查备份及恢复能力;生成新计划并确认;分批执行;验证运行与数据;核对服务端和部署机文档回执。不得直接删除 data/env/backups,也不得用旧批准授权新迁移。\n",
125
+ "## 备份\n", dep.backup + "\n", "## 恢复与回滚\n", dep.recovery + "\n",
126
+ "切换旧代码不能自动恢复数据库结构。共享服务整实例恢复需要全部消费者维护审批。\n",
127
+ );
128
+ if (dep.notes.length) out.push("## 限制与备注\n", dep.notes.join("\n\n") + "\n");
129
+ const replicas = Object.values(status.deployments).filter((d) => d.project_id === dep.project_id);
130
+ out.push("## 同项目部署位置\n", replicas.sort((a, b) => a.deployment_id < b.deployment_id ? -1 : 1).map((d) => `- ${d.host_id} / ${d.deployment_id}:${code(d.root)}`).join("\n") + "\n");
131
+ if (controller) out.push("\n本目录是部署机的配置、凭据、运行证据和远端文档记录;不声称自动复制了远端业务数据。数据备份需要独立的备份策略和回执。\n");
132
+ if (includeCredentials) out.push(credentialsText(credentialRefs(status, dep), ledger));
133
+ else out.push("\n## 凭据记录\n\n本 README 默认不写密码。获授权管理员读取本项目 OPERATIONS.md(启用时)或部署机明文手册;密码未知时必须补齐,不能编造。\n");
134
+ return out.join("\n") + "\n";
135
+ }
136
+
137
+ export function hostReadme(status, hid, runId, at, { ledger = null, full = false, controller = false } = {}) {
138
+ const host = status.hosts[hid];
139
+ const out = [
140
+ `# 主机 ${host.display_name} / ${hid}\n`,
141
+ `主机持久化根:${code(host.root)};更新:${at};运行:${runId}。\n`,
142
+ "APP 与公共服务同级。通用规范见 docs/standards/DEPLOYMENT-STANDARD.md。DEPLOYMENTS.md 为本机部署手册。\n",
143
+ "| APP / 服务 | 实例 | 版本(最近验证) | 目录 | 最近验证时间 |\n|---|---|---|---|---|",
144
+ ];
145
+ const deps = Object.values(status.deployments).filter((d) => d.host_id === hid).sort((a, b) => a.deployment_id < b.deployment_id ? -1 : 1);
146
+ for (const d of deps) out.push(`| ${d.project_id} | ${d.deployment_id} | ${code(d.observed_version || "未验证")} | ${code(d.root)} | ${d.updated_at || "未验证"} |`);
147
+ out.push("\n公共服务数据归提供者;其他主机使用的服务通过依赖绑定记录,不在本机创建假的空服务目录。\n");
148
+ if (full) {
149
+ for (const d of deps) out.push(deploymentReadme(status, d, runId, at, { ledger, includeCredentials: controller || status.policies.server_operations }));
150
+ }
151
+ return out.join("\n") + "\n";
152
+ }
153
+
154
+ export function remotePaths(status, dep) {
155
+ const names = ["README.md", "project.yaml", "run/release-state.json"];
156
+ if (status.policies.server_operations) names.push("OPERATIONS.md");
157
+ if (status.projects[dep.project_id].kind === "shared-service") names.push("consumers.md");
158
+ if (dep.layout === "instances") {
159
+ return names.map((n) => pathFor(status, dep, n)).concat([targetJoin(status.hosts[dep.host_id], dep.project_id + "/README.md")]);
160
+ }
161
+ return names.map((n) => pathFor(status, dep, n));
162
+ }
163
+
164
+ export function planReport(plan) {
165
+ const out = [
166
+ `# OPS 执行计划 ${plan.run_id}\n`,
167
+ `Worker:${plan.worker};操作:${plan.operation};风险:${plan.risk}。\n`,
168
+ `计划摘要:\`${digest(plan)}\`\n\n创建:${plan.created_at};批准有效截止:${plan.expires_at}。\n`,
169
+ "## 目标与理由\n", plan.reason + "\n",
170
+ ];
171
+ for (const [hid, h] of Object.entries(plan.hosts)) out.push(`主机 ${hid}:${h.transport} / ${h.platform} / 根 ${code(h.root)} / 身份 ${h.identity}。\n`);
172
+ out.push("## 受影响消费者\n", plan.affected_consumers.join(", ") || "没有登记的既有消费者受影响。", "\n## 完整动作集合\n");
173
+ for (const op of plan.operations) {
174
+ out.push(`### ${op.step_id} — ${op.host_id} / ${op.deployment_id || "host"} / ${op.kind}\n`);
175
+ const display = Object.fromEntries(Object.entries(op).filter(([k]) => k !== "content_b64"));
176
+ if ("content_b64" in op) display.payload_sha256 = digest(Buffer.from(op.content_b64, "base64"));
177
+ out.push(block(display));
178
+ if (op.secret_argv_acknowledged) out.push("**该 MinIO 管理动作的应用密码会短暂出现在受特权用户可见的 mc 子进程参数中。批准包含此风险。**\n");
179
+ }
180
+ out.push(
181
+ "## 明文文件与双边交付\n",
182
+ "服务器 README 默认不含密码;受限 OPERATIONS.md 和部署机手册按已批准策略写真实明文。env 和本地配置副本按 0600/受限 ACL 写入。目标与部署机文件均回读校验;缺一不可标记 completed。\n",
183
+ block({ document_targets: plan.document_targets, credential_versions: Object.keys(plan.credential_versions), policies: plan.registry_after.policies }),
184
+ "## 数据保护和恢复\n", plan.rollback_note + "\n",
185
+ "没有默认删除数据、逻辑资源、公共服务、卷或备份的动作。失败停止,断线标记 unknown;同一计划不能盲目重跑迁移。\n",
186
+ "## 批准\n", "必须由用户确认上述目标、完整写入集合、凭据版本、影响集合和恢复限制。approve 必须携带本报告对应的完整摘要;编辑计划会使旧批准失效。\n",
187
+ );
188
+ return out.join("\n");
189
+ }
190
+
191
+ export function deliveryBundle(state, plan, status, ledger, verifiedIds) {
192
+ const at = now();
193
+ const rid = plan.run_id;
194
+ const remote = new Map();
195
+ const local = {};
196
+ const addRemote = (hid, path, text) => {
197
+ const expected = plan.document_preconditions[hid]?.[path];
198
+ if (expected === undefined) throw new OpsError("document write not included in approved preconditions: " + path);
199
+ remote.set(hid + "\0" + path, { host_id: hid, path, content: text, sha256: digest(Buffer.from(text, "utf8")), expected });
200
+ };
201
+ for (const did of [...new Set(verifiedIds)].sort()) {
202
+ const d = status.deployments[did];
203
+ const hid = d.host_id;
204
+ const readme = deploymentReadme(status, d, rid, at, { ledger, includeCredentials: status.policies.server_readme_credentials });
205
+ addRemote(hid, pathFor(status, d, "README.md"), readme);
206
+ const operations = deploymentReadme(status, d, rid, at, { ledger, includeCredentials: true });
207
+ if (status.policies.server_operations) addRemote(hid, pathFor(status, d, "OPERATIONS.md"), operations);
208
+ addRemote(hid, pathFor(status, d, "project.yaml"), JSON.stringify(d, null, 2) + "\n");
209
+ addRemote(hid, pathFor(status, d, "run/release-state.json"), JSON.stringify({
210
+ run_id: rid, version: d.version, observed_version: d.observed_version, verified_at: d.updated_at, document_generation: at,
211
+ }, null, 2) + "\n");
212
+ if (status.projects[d.project_id].kind === "shared-service") addRemote(hid, pathFor(status, d, "consumers.md"), dependencies(status, d));
213
+ if (d.layout === "instances") {
214
+ const projectroot = targetJoin(status.hosts[hid], d.project_id);
215
+ const peers = Object.values(status.deployments).filter((x) => x.host_id === hid && x.project_id === d.project_id);
216
+ const text = "# " + d.project_id + " 实例索引\n\n" + peers.map((x) => `- ${x.environment}/${x.instance}:${code(x.root)},版本 ${code(x.version)}`).join("\n") + "\n";
217
+ addRemote(hid, targetJoin({ ...status.hosts[hid], root: projectroot }, "README.md"), text);
218
+ }
219
+ const prefix = `hosts/${hid}/deployments/${did}`;
220
+ local[prefix + "/README.md"] = deploymentReadme(status, d, rid, at, { ledger, includeCredentials: true, controller: true });
221
+ local[prefix + "/OPERATIONS.md"] = operations;
222
+ local[prefix + "/deployment.json"] = JSON.stringify(d, null, 2) + "\n";
223
+ local[prefix + "/server/README.md"] = readme;
224
+ }
225
+ const touchedHosts = [...new Set([...verifiedIds.map((d) => status.deployments[d].host_id), ...Object.keys(plan.hosts)])].sort();
226
+ for (const hid of touchedHosts) {
227
+ const h = status.hosts[hid];
228
+ addRemote(hid, targetJoin(h, "README.md"), hostReadme(status, hid, rid, at));
229
+ addRemote(hid, targetJoin(h, "DEPLOYMENTS.md"), hostReadme(status, hid, rid, at, { ledger, full: true }));
230
+ addRemote(hid, targetJoin(h, "docs/standards/DEPLOYMENT-STANDARD.md"), STANDARD);
231
+ const knowledgePath = targetJoin(h, "knowledge/INDEX.md");
232
+ if ((plan.document_preconditions[hid]?.[knowledgePath] ?? { kind: "file" }).kind === "absent") {
233
+ addRemote(hid, knowledgePath, "# 共享知识索引\n\n通用规范见 ../docs/standards/DEPLOYMENT-STANDARD.md。只收录经用户确认、带来源和最后验证时间的知识,不存密码。\n");
234
+ }
235
+ local[`hosts/${hid}/README.md`] = hostReadme(status, hid, rid, at);
236
+ local[`hosts/${hid}/DEPLOYMENTS.md`] = hostReadme(status, hid, rid, at, { ledger, full: true, controller: true });
237
+ }
238
+ const globalText = ["# 全域部署总册(明文)\n", `最近文档代次:${rid};生成时间:${at}。\n`, "本地账本和配置副本不等于远端业务数据备份。\n"];
239
+ for (const hid of Object.keys(status.hosts).sort()) globalText.push(hostReadme(status, hid, rid, at, { ledger, full: true, controller: true }));
240
+ local["FLEET-DEPLOYMENTS.md"] = globalText.join("\n");
241
+ local["README.md"] = "# OPS 控制端\n\n主机记录位于 hosts/<host_id>/;项目关联位于 status.json;项目部署记录位于 hosts/<host_id>/deployments/<deployment_id>/;完整明文总册位于 FLEET-DEPLOYMENTS.md;凭据账本位于 private/credentials.json;执行证据按 host runs / release 保存。\n\n不得清理此运行态目录来替换静态 workflow。双边文档完成由每次运行 docs-receipt.json 证明。\n";
242
+ local["docs/standards/DEPLOYMENT-STANDARD.md"] = STANDARD;
243
+ if (!existsSync(join(state, "knowledge", "INDEX.md"))) {
244
+ local["knowledge/INDEX.md"] = "# 共享知识索引\n\n只收录经用户批准、带来源与最后验证日期的通用知识。运行时环境和密码不自动提升为共享知识。\n";
245
+ }
246
+ return {
247
+ schema_version: 1, run_id: rid, plan_digest: digest(plan), generated_at: at,
248
+ remote: [...remote.values()],
249
+ local: Object.entries(local).map(([p, text]) => ({ path: p, content: text, sha256: digest(Buffer.from(text, "utf8")) })),
250
+ acks: {},
251
+ };
252
+ }