cli-archguard 7.0.19 → 7.0.28

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 CHANGED
@@ -1,9 +1,37 @@
1
- # cli-archguard
1
+ # 架构守卫 / ArchGuard / Архитектурный страж
2
2
 
3
- Official ArchGuard installer for CLI.Tax.
3
+ CLI.Tax 架构守卫官方安装包:在真实代码写入期间锁定架构合同,逐块检查技术栈漂移、规则违规与复杂度超限,并记录可审计的回滚证据。
4
+
5
+ Official CLI.Tax ArchGuard installer: lock an architecture contract while code is written, check every block for stack drift, rule violations, and complexity overages, and retain auditable rollback evidence.
6
+
7
+ Официальный установщик ArchGuard для CLI.Tax: фиксирует архитектурный контракт во время записи кода, проверяет каждый блок на дрейф стека, нарушения и превышение сложности и сохраняет проверяемые доказательства отката.
4
8
 
5
9
  ```bash
6
10
  npx cli-archguard@latest install
7
11
  ```
8
12
 
9
- The installed skill creates and validates architecture contracts, checks each code block, reports drift, and records auditable checkpoint evidence. Runtime and release source: https://github.com/88208555/archguard-clitax.git
13
+ 安装后的技能可创建并校验架构合同、检查每个代码块、报告漂移并记录可审计的检查点证据。The installed skill creates and validates architecture contracts, checks each code block, reports drift, and records auditable checkpoint evidence. Установленный навык создаёт и проверяет архитектурные контракты, контролирует каждый блок, сообщает о дрейфе и ведёт аудируемый журнал. Runtime and release source: https://github.com/88208555/archguard-clitax.git
14
+
15
+ The npm package includes the runtime and trusted local runner. Remote JSON cannot submit AST findings; AST contracts must use these local commands:
16
+
17
+ ```bash
18
+ cli-archguard ledger-init . .archguard/ledger.json
19
+ cli-archguard snapshot . src/example.ts .archguard/example.snapshot.json
20
+ cli-archguard checkpoint . arch.contract.yaml src/example.ts .archguard/example.snapshot.json .archguard/ledger.json block-1
21
+ ```
22
+
23
+ 快照和台账路径必须使用仓库内 `.archguard/` 相对路径;绝对路径、父级穿越、符号链接/目录联接和覆盖非受管文件都会被拒绝。 Snapshot and ledger paths must be relative paths under the repository's `.archguard/` directory; absolute paths, parent traversal, symlinks/junctions, and overwriting unmanaged files are rejected. Пути снимков и журнала должны быть относительными и находиться в `.archguard/` внутри репозитория; абсолютные пути, выход к родителю, символические ссылки/соединения каталогов и перезапись неуправляемых файлов отклоняются.
24
+
25
+ `checkpoint` 允许写入时退出码为 `0`;发现阻断并完成回滚时退出码为 `2`;执行错误退出码为 `1`。调用方必须同时读取 JSON 证据,禁止只凭输出文本判定成功。
26
+
27
+ ## 受限调用与自动评价 / Restricted invocation and automatic evaluation / Ограниченный вызов и автооценка
28
+
29
+ IDE 通过 `invoke` 或 JSON-stdin `broker` 调用。broker 本身只需要 Brain Client HTTPS、受限身份文件和显式传入路径,不需要完整磁盘访问。要保证 IDE 看不到令牌,必须把 broker 作为独立低权限账户或沙箱服务运行并只暴露受限 IPC;同一账户下的 `0600` 不能隔离 IDE 与 broker。服务端在同一次 runtime 请求中事务提交权威评价并返回回执,broker 只验证回执,不发起第二次评价写入。
30
+
31
+ Use `npx cli-archguard@latest invoke <operation> '<JSON object>'`, or send JSON stdin to `npx cli-archguard@latest broker`. The broker itself needs only Brain Client HTTPS, its restricted identity file, and explicitly supplied paths; it does not need full-disk access. To keep the token inaccessible to the IDE, run the broker under a separate least-privilege account or sandbox service and expose only restricted IPC. Mode `0600` does not isolate two processes running as the same account.
32
+
33
+ IDE вызывает пакет через `invoke` или JSON-stdin `broker`. Самому broker нужны только HTTPS Brain Client, ограниченный файл идентификации и явно переданные пути; полный доступ к диску не нужен. Чтобы IDE не мог прочитать токен, broker должен работать под отдельной малопривилегированной учётной записью или в sandbox-сервисе с ограниченным IPC. Режим `0600` не изолирует процессы одной учётной записи.
34
+
35
+ The Brain Client server binds the real response and atomically persists the authoritative score and comment within the same runtime request, then returns a committed receipt. The broker verifies `feedbackReceiptId`, `feedbackInvocationId`, and the authoritative digest; it makes no second evaluation write and never creates a score or comment. Not-reported or incomplete validation, P0/P1 findings, blocked, and failed results cannot be positive. Missing credentials or receipts, digest mismatches, invalid responses, and HTTP failures fail explicitly.
36
+
37
+ The local CLI has no command for manually submitting a score or evaluation comment. Humans cannot choose a skill score or write skill evaluation content. Daily chat is outside the evaluation protocol.
@@ -0,0 +1,46 @@
1
+ import { appendFile, lstat } from 'node:fs/promises'
2
+ import { isAbsolute, relative, resolve } from 'node:path'
3
+
4
+ const INVALIDATION_SCHEMA = 'contextbase.invalidation/1.0'
5
+
6
+ function safeRelativePath(value) {
7
+ if (typeof value !== 'string' || !value || value !== value.normalize('NFC')
8
+ || isAbsolute(value) || /^[A-Za-z]:/.test(value) || value.includes('\\')
9
+ || /[\u0000-\u001f\u007f]/.test(value)
10
+ || value.split('/').some((part) => !part || part === '.' || part === '..')) {
11
+ throw new Error('ContextBase invalidation path is unsafe')
12
+ }
13
+ return value
14
+ }
15
+
16
+ async function recordContextBaseInvalidation(repositoryRoot, targetPath, outcome) {
17
+ const root = resolve(repositoryRoot)
18
+ const directory = resolve(root, '.contextbase')
19
+ const path = relative(root, directory)
20
+ if (path !== '.contextbase') throw new Error('ContextBase managed path escapes repository')
21
+ let status
22
+ try {
23
+ status = await lstat(directory)
24
+ } catch (error) {
25
+ if (error instanceof Error && error.code === 'ENOENT') {
26
+ return { recorded: false, reason: 'contextbase-not-initialized' }
27
+ }
28
+ throw error
29
+ }
30
+ if (status.isSymbolicLink() || !status.isDirectory()) {
31
+ throw new Error('.contextbase must be a real directory')
32
+ }
33
+ const event = {
34
+ schemaVersion: INVALIDATION_SCHEMA,
35
+ at: new Date().toISOString(),
36
+ path: safeRelativePath(targetPath),
37
+ source: 'archguard-checkpoint',
38
+ outcome,
39
+ }
40
+ await appendFile(resolve(directory, 'invalidation.jsonl'), `${JSON.stringify(event)}\n`, {
41
+ mode: 0o600,
42
+ })
43
+ return { recorded: true, event }
44
+ }
45
+
46
+ export { INVALIDATION_SCHEMA, recordContextBaseInvalidation }
@@ -0,0 +1,492 @@
1
+ import { createRequire } from "node:module";
2
+ import * as import_archguard_runtime from "./archguard-runtime.mjs";
3
+ import { verifyMutationPassFile } from "cli-aimlock/local-runner";
4
+ import { recordContextBaseInvalidation } from "./archguard-contextbase-hook.mjs";
5
+ const require = createRequire(import.meta.url);
6
+ const import_node_crypto = require("node:crypto");
7
+ const import_node_fs = require("node:fs");
8
+ const import_promises = require("node:fs/promises");
9
+ const import_node_path = require("node:path");
10
+ const import_parser = require("@babel/parser");
11
+ const import_yaml = require("yaml");
12
+ const __name = (target, value) =>
13
+ Object.defineProperty(target, "name", { value, configurable: true });
14
+ const SNAPSHOT_VERSION = "archguard.block-snapshot/1.0";
15
+ const LEDGER_VERSION = "archguard.checkpoint-ledger/1.0";
16
+ const MANAGED_DIRECTORY = ".archguard";
17
+ const MANAGED_OWNER = "cli-archguard";
18
+ const MISSING_SHA256 = (0, import_node_crypto.createHash)("sha256")
19
+ .update("archguard.missing-file/1.0")
20
+ .digest("hex");
21
+ function sha256(value) {
22
+ return (0, import_node_crypto.createHash)("sha256")
23
+ .update(value)
24
+ .digest("hex");
25
+ }
26
+ __name(sha256, "sha256");
27
+ function object(value, context) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value))
29
+ throw new Error(`${context} must be an object`);
30
+ return value;
31
+ }
32
+ __name(object, "object");
33
+ function safeRelativePath(value, context) {
34
+ if (
35
+ typeof value !== "string" ||
36
+ value !== value.normalize("NFC") ||
37
+ (0, import_node_path.isAbsolute)(value) ||
38
+ /^[A-Za-z]:/.test(value) ||
39
+ value.includes("\\") ||
40
+ /[\u0000-\u001f\u007f]/.test(value) ||
41
+ value.split("/").some((part) => !part || part === "." || part === "..")
42
+ ) {
43
+ throw new Error(`${context} is unsafe`);
44
+ }
45
+ return value;
46
+ }
47
+ __name(safeRelativePath, "safeRelativePath");
48
+ function managedRelativePath(value, context) {
49
+ const path = safeRelativePath(value, context);
50
+ const segments = path.split("/");
51
+ if (segments[0] !== MANAGED_DIRECTORY || segments.length < 2) {
52
+ throw new Error(`${context} must be inside ${MANAGED_DIRECTORY}`);
53
+ }
54
+ return { path, segments };
55
+ }
56
+ __name(managedRelativePath, "managedRelativePath");
57
+ function assertInside(root, target, context) {
58
+ const path = (0, import_node_path.relative)(root, target);
59
+ if (
60
+ path === "" ||
61
+ (!(0, import_node_path.isAbsolute)(path) &&
62
+ path !== ".." &&
63
+ !path.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`))
64
+ )
65
+ return;
66
+ throw new Error(`${context} escapes the repository`);
67
+ }
68
+ __name(assertInside, "assertInside");
69
+ function missing(error) {
70
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
71
+ }
72
+ __name(missing, "missing");
73
+ function alreadyExists(error) {
74
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
75
+ }
76
+ __name(alreadyExists, "alreadyExists");
77
+ async function repositoryRoot(value) {
78
+ if (typeof value !== "string" || value !== value.trim() || !value)
79
+ throw new Error("repositoryRoot is required");
80
+ const explicit = (0, import_node_path.resolve)(value);
81
+ const status = await (0, import_promises.lstat)(explicit);
82
+ if (status.isSymbolicLink() || !status.isDirectory())
83
+ throw new Error("repositoryRoot must be a real directory");
84
+ return (0, import_promises.realpath)(explicit);
85
+ }
86
+ __name(repositoryRoot, "repositoryRoot");
87
+ async function managedTarget(root, value, context, createParents) {
88
+ const managed = managedRelativePath(value, context);
89
+ let current = root;
90
+ for (const segment of managed.segments.slice(0, -1)) {
91
+ current = (0, import_node_path.resolve)(current, segment);
92
+ if (createParents) {
93
+ try {
94
+ await (0, import_promises.mkdir)(current, { mode: 448 });
95
+ } catch (error) {
96
+ if (!alreadyExists(error)) throw error;
97
+ }
98
+ }
99
+ const status = await (0, import_promises.lstat)(current);
100
+ if (status.isSymbolicLink() || !status.isDirectory()) {
101
+ throw new Error(`${context} parent cannot be a symlink or junction`);
102
+ }
103
+ const real = await (0, import_promises.realpath)(current);
104
+ assertInside(root, real, `${context} parent`);
105
+ if (real !== current) {
106
+ throw new Error(`${context} parent must resolve inside the real repository`);
107
+ }
108
+ }
109
+ const target = (0, import_node_path.resolve)(root, ...managed.segments);
110
+ const managedRoot = (0, import_node_path.resolve)(root, MANAGED_DIRECTORY);
111
+ assertInside(managedRoot, target, context);
112
+ return { target, relativePath: managed.path };
113
+ }
114
+ __name(managedTarget, "managedTarget");
115
+ async function managedFile(root, value, context, allowMissing, createParents) {
116
+ const target = await managedTarget(root, value, context, createParents);
117
+ let status;
118
+ try {
119
+ status = await (0, import_promises.lstat)(target.target);
120
+ } catch (error) {
121
+ if (allowMissing && missing(error)) return { ...target, exists: false };
122
+ throw error;
123
+ }
124
+ if (status.isSymbolicLink() || !status.isFile()) {
125
+ throw new Error(`${context} must be a real managed file`);
126
+ }
127
+ const real = await (0, import_promises.realpath)(target.target);
128
+ if (real !== target.target) {
129
+ throw new Error(`${context} cannot be a symlink or junction`);
130
+ }
131
+ return {
132
+ ...target,
133
+ exists: true,
134
+ identity: { device: status.dev, inode: status.ino },
135
+ };
136
+ }
137
+ __name(managedFile, "managedFile");
138
+ async function resolveProjectFile(root, projectPath, allowMissing) {
139
+ const path = safeRelativePath(projectPath, "project path");
140
+ const target = (0, import_node_path.resolve)(root, ...path.split("/"));
141
+ assertInside(root, target, "project path");
142
+ let current = root;
143
+ for (const [index, segment] of path.split("/").entries()) {
144
+ current = (0, import_node_path.resolve)(current, segment);
145
+ let status;
146
+ try {
147
+ status = await (0, import_promises.lstat)(current);
148
+ } catch (error) {
149
+ if (!missing(error) || !allowMissing) throw error;
150
+ const parent = await (0, import_promises.realpath)(
151
+ (0, import_node_path.dirname)(current),
152
+ );
153
+ assertInside(root, parent, "missing project path parent");
154
+ return { target, exists: false };
155
+ }
156
+ if (status.isSymbolicLink())
157
+ throw new Error(`project path cannot contain symlinks: ${path}`);
158
+ if (index < path.split("/").length - 1 && !status.isDirectory()) {
159
+ throw new Error(`project path parent is not a directory: ${path}`);
160
+ }
161
+ if (index === path.split("/").length - 1 && !status.isFile()) {
162
+ throw new Error(`project path is not a regular file: ${path}`);
163
+ }
164
+ }
165
+ return { target, exists: true };
166
+ }
167
+ __name(resolveProjectFile, "resolveProjectFile");
168
+ async function noFollowRead(path) {
169
+ const handle = await (0, import_promises.open)(
170
+ path,
171
+ import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_NOFOLLOW,
172
+ );
173
+ try {
174
+ const status = await handle.stat();
175
+ if (!status.isFile()) throw new Error("file must be regular");
176
+ return handle.readFile();
177
+ } finally {
178
+ await handle.close();
179
+ }
180
+ }
181
+ __name(noFollowRead, "noFollowRead");
182
+ async function contractFromFile(root, contractPath) {
183
+ const resolved = await resolveProjectFile(root, contractPath, false);
184
+ const source = (await noFollowRead(resolved.target)).toString("utf8");
185
+ const contract = (0, import_yaml.parse)(source);
186
+ object(contract, "architecture contract");
187
+ return contract;
188
+ }
189
+ __name(contractFromFile, "contractFromFile");
190
+ async function managedJsonFile(root, path, context) {
191
+ const file = await managedFile(root, path, context, false, false);
192
+ const source = await noFollowRead(file.target);
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(source.toString("utf8"));
196
+ } catch {
197
+ throw new Error(`${context} is not valid JSON`);
198
+ }
199
+ return { parsed, file };
200
+ }
201
+ __name(managedJsonFile, "managedJsonFile");
202
+ async function createManagedFile(root, path, body) {
203
+ const file = await managedFile(root, path, "managed file", true, true);
204
+ if (file.exists) throw new Error("managed file already exists");
205
+ await (0, import_promises.writeFile)(file.target, body, {
206
+ flag: "wx",
207
+ mode: 384,
208
+ });
209
+ return managedFile(root, path, "managed file", false, false);
210
+ }
211
+ __name(createManagedFile, "createManagedFile");
212
+ function sameIdentity(left, right) {
213
+ return left.device === right.device && left.inode === right.inode;
214
+ }
215
+ __name(sameIdentity, "sameIdentity");
216
+ async function replaceManagedFile(root, path, body, expectedIdentity) {
217
+ const current = await managedFile(root, path, "managed file", false, false);
218
+ if (!sameIdentity(current.identity, expectedIdentity)) {
219
+ throw new Error("managed file changed before update");
220
+ }
221
+ const temporaryPath = `${current.relativePath}.${(0, import_node_crypto.randomUUID)()}.tmp`;
222
+ const temporary = await createManagedFile(root, temporaryPath, body);
223
+ try {
224
+ const verified = await managedFile(root, path, "managed file", false, false);
225
+ if (!sameIdentity(verified.identity, expectedIdentity)) {
226
+ throw new Error("managed file changed before replacement");
227
+ }
228
+ await (0, import_promises.rename)(temporary.target, verified.target);
229
+ } catch (error) {
230
+ await (0, import_promises.unlink)(temporary.target);
231
+ throw error;
232
+ }
233
+ }
234
+ __name(replaceManagedFile, "replaceManagedFile");
235
+ async function initializeCheckpointLedger(input) {
236
+ const source = object(input, "ledger input");
237
+ const root = await repositoryRoot(source.repositoryRoot);
238
+ const ledger = {
239
+ schemaVersion: LEDGER_VERSION,
240
+ managedBy: MANAGED_OWNER,
241
+ repositoryRoot: root,
242
+ entries: [],
243
+ };
244
+ const file = await createManagedFile(
245
+ root,
246
+ source.ledgerPath,
247
+ `${JSON.stringify(ledger)}\n`,
248
+ );
249
+ return { schemaVersion: LEDGER_VERSION, path: file.target };
250
+ }
251
+ __name(initializeCheckpointLedger, "initializeCheckpointLedger");
252
+ async function createBlockSnapshot(input) {
253
+ const source = object(input, "snapshot input");
254
+ const root = await repositoryRoot(source.repositoryRoot);
255
+ const path = safeRelativePath(source.targetPath, "targetPath");
256
+ const projectFile = await resolveProjectFile(root, path, true);
257
+ const content = projectFile.exists
258
+ ? await noFollowRead(projectFile.target)
259
+ : null;
260
+ const fileMode = projectFile.exists
261
+ ? (await (0, import_promises.lstat)(projectFile.target)).mode & 511
262
+ : null;
263
+ const snapshot = {
264
+ schemaVersion: SNAPSHOT_VERSION,
265
+ managedBy: MANAGED_OWNER,
266
+ repositoryRoot: root,
267
+ targetPath: path,
268
+ existed: projectFile.exists,
269
+ beforeSha256: content ? sha256(content) : MISSING_SHA256,
270
+ fileMode,
271
+ contentBase64: content ? content.toString("base64") : null,
272
+ };
273
+ const snapshotFile = await createManagedFile(
274
+ root,
275
+ source.snapshotPath,
276
+ `${JSON.stringify(snapshot)}\n`,
277
+ );
278
+ return {
279
+ snapshotPath: snapshotFile.target,
280
+ beforeSha256: snapshot.beforeSha256,
281
+ existed: snapshot.existed,
282
+ };
283
+ }
284
+ __name(createBlockSnapshot, "createBlockSnapshot");
285
+ function astFindings(content, path, contract) {
286
+ const rules =
287
+ contract.rules?.custom?.filter((rule) => rule.engine === "ast") ?? [];
288
+ if (!rules.length) return [];
289
+ let tree;
290
+ try {
291
+ tree = (0, import_parser.parse)(content, {
292
+ sourceType: "unambiguous",
293
+ plugins: [
294
+ "typescript",
295
+ "jsx",
296
+ "decorators",
297
+ "classProperties",
298
+ "importAttributes",
299
+ ],
300
+ });
301
+ } catch (error) {
302
+ return [
303
+ {
304
+ severity: "P1",
305
+ ruleId: "ARCH-AST-PARSE",
306
+ entityRef: path,
307
+ message: error instanceof Error ? error.message : "AST parse failed",
308
+ category: "custom",
309
+ blocking: true,
310
+ evidence: {},
311
+ },
312
+ ];
313
+ }
314
+ const counts = new Map(rules.map((rule) => [rule.id, 0]));
315
+ const visit = __name((node) => {
316
+ if (!node || typeof node !== "object") return;
317
+ if (typeof node.type === "string")
318
+ for (const rule of rules) {
319
+ if (node.type === rule.pattern)
320
+ counts.set(rule.id, (counts.get(rule.id) ?? 0) + 1);
321
+ }
322
+ for (const [key, value] of Object.entries(node)) {
323
+ if (key === "loc" || key === "start" || key === "end") continue;
324
+ if (Array.isArray(value)) value.forEach(visit);
325
+ else if (value && typeof value === "object") visit(value);
326
+ }
327
+ }, "visit");
328
+ visit(tree);
329
+ return rules.flatMap((rule) =>
330
+ counts.get(rule.id)
331
+ ? [
332
+ {
333
+ severity: rule.blocking ? "P1" : "P2",
334
+ ruleId: rule.id,
335
+ entityRef: path,
336
+ message: rule.message,
337
+ category: "custom",
338
+ blocking: rule.blocking,
339
+ evidence: { matches: counts.get(rule.id), engine: "ast" },
340
+ },
341
+ ]
342
+ : [],
343
+ );
344
+ }
345
+ __name(astFindings, "astFindings");
346
+ async function restoreSnapshot(root, snapshot) {
347
+ const target = await resolveProjectFile(root, snapshot.targetPath, true);
348
+ if (snapshot.existed) {
349
+ const content = Buffer.from(snapshot.contentBase64, "base64");
350
+ if (
351
+ sha256(content) !== snapshot.beforeSha256 ||
352
+ !Number.isInteger(snapshot.fileMode)
353
+ ) {
354
+ throw new Error("snapshot content authority is invalid");
355
+ }
356
+ await (0, import_promises.mkdir)(
357
+ (0, import_node_path.dirname)(target.target),
358
+ { recursive: true },
359
+ );
360
+ await (0, import_promises.writeFile)(target.target, content, {
361
+ mode: snapshot.fileMode,
362
+ });
363
+ } else if (target.exists) await (0, import_promises.unlink)(target.target);
364
+ }
365
+ __name(restoreSnapshot, "restoreSnapshot");
366
+ async function checkpointLedger(root, path) {
367
+ const authority = await managedJsonFile(root, path, "checkpoint ledger");
368
+ const ledger = object(
369
+ authority.parsed,
370
+ "checkpoint ledger",
371
+ );
372
+ if (
373
+ ledger.schemaVersion !== LEDGER_VERSION ||
374
+ ledger.managedBy !== MANAGED_OWNER ||
375
+ ledger.repositoryRoot !== root ||
376
+ !Array.isArray(ledger.entries)
377
+ ) {
378
+ throw new Error("checkpoint ledger authority is invalid");
379
+ }
380
+ return { ledger, file: authority.file };
381
+ }
382
+ __name(checkpointLedger, "checkpointLedger");
383
+ async function checkpointSnapshot(root, snapshotPath, targetPath) {
384
+ const authority = await managedJsonFile(root, snapshotPath, "block snapshot");
385
+ const snapshot = object(authority.parsed, "block snapshot");
386
+ if (
387
+ snapshot.schemaVersion !== SNAPSHOT_VERSION ||
388
+ snapshot.managedBy !== MANAGED_OWNER ||
389
+ snapshot.repositoryRoot !== root ||
390
+ snapshot.targetPath !== targetPath
391
+ )
392
+ throw new Error("block snapshot authority is invalid");
393
+ return snapshot;
394
+ }
395
+ __name(checkpointSnapshot, "checkpointSnapshot");
396
+ async function checkpointFileAndRollbackUnchecked(input) {
397
+ const source = object(input, "checkpoint input");
398
+ const root = await repositoryRoot(source.repositoryRoot);
399
+ const targetPath = safeRelativePath(source.targetPath, "targetPath");
400
+ const snapshot = await checkpointSnapshot(root, source.snapshotPath, targetPath);
401
+ const projectFile = await resolveProjectFile(root, targetPath, true);
402
+ const content = projectFile.exists
403
+ ? (await noFollowRead(projectFile.target)).toString("utf8")
404
+ : "";
405
+ const contract = await contractFromFile(root, source.contractPath);
406
+ const ledgerAuthority = await checkpointLedger(root, source.ledgerPath);
407
+ const ledger = ledgerAuthority.ledger;
408
+ const missingTargetFindings = snapshot.existed && !projectFile.exists
409
+ ? [{
410
+ severity: "P0",
411
+ ruleId: "ARCH-TARGET-DELETED",
412
+ entityRef: targetPath,
413
+ message: "The checkpoint target was deleted after its snapshot",
414
+ category: "structure",
415
+ blocking: true,
416
+ evidence: { beforeSha256: snapshot.beforeSha256 },
417
+ }]
418
+ : [];
419
+ const trustedFindings = [
420
+ ...missingTargetFindings,
421
+ ...astFindings(content, targetPath, contract),
422
+ ];
423
+ const result = await (0, import_archguard_runtime.runTrustedLocalCheckpoint)({
424
+ schemaVersion: "archguard.skill.request/1.0",
425
+ requestId: `checkpoint-${(0, import_node_crypto.randomUUID)()}`,
426
+ operation: "checkpoint",
427
+ input: {
428
+ contract,
429
+ block: {
430
+ blockId: source.blockId,
431
+ path: targetPath,
432
+ content,
433
+ beforeSha256: snapshot.beforeSha256,
434
+ },
435
+ history: ledger.entries,
436
+ },
437
+ }, trustedFindings);
438
+ if (result.output.checkpoint.rollbackRequired)
439
+ await restoreSnapshot(root, snapshot);
440
+ const updated = {
441
+ schemaVersion: LEDGER_VERSION,
442
+ managedBy: MANAGED_OWNER,
443
+ repositoryRoot: root,
444
+ entries: [...ledger.entries, result.output.ledgerEntry],
445
+ };
446
+ await replaceManagedFile(
447
+ root,
448
+ source.ledgerPath,
449
+ `${JSON.stringify(updated)}\n`,
450
+ ledgerAuthority.file.identity,
451
+ );
452
+ return {
453
+ ...result,
454
+ rollbackCompleted: result.output.checkpoint.rollbackRequired,
455
+ };
456
+ }
457
+ __name(checkpointFileAndRollbackUnchecked, "checkpointFileAndRollbackUnchecked");
458
+ async function checkpointFileAndRollback(input) {
459
+ const source = object(input, "checkpoint input");
460
+ let gate;
461
+ try {
462
+ gate = await verifyMutationPassFile({ repositoryRoot: source.repositoryRoot,
463
+ chainId: source.chainId, targetPath: source.targetPath,
464
+ gatePassPath: source.gatePassPath });
465
+ } catch (error) {
466
+ const root = await repositoryRoot(source.repositoryRoot);
467
+ const targetPath = safeRelativePath(source.targetPath, "targetPath");
468
+ await restoreSnapshot(root, await checkpointSnapshot(root, source.snapshotPath, targetPath));
469
+ throw error;
470
+ }
471
+ const result = await checkpointFileAndRollbackUnchecked(source);
472
+ const contextBaseInvalidation = await recordContextBaseInvalidation(source.repositoryRoot,
473
+ source.targetPath, result.output.checkpoint.status);
474
+ return {
475
+ ...result,
476
+ contextBaseInvalidation,
477
+ gateEvidence: {
478
+ schemaVersion: gate.schemaVersion,
479
+ passId: gate.pass.passId,
480
+ chainId: gate.pass.chainId,
481
+ targetPath: gate.targetPath,
482
+ },
483
+ };
484
+ }
485
+ __name(checkpointFileAndRollback, "checkpointFileAndRollback");
486
+ export {
487
+ MISSING_SHA256,
488
+ SNAPSHOT_VERSION,
489
+ checkpointFileAndRollback,
490
+ createBlockSnapshot,
491
+ initializeCheckpointLedger,
492
+ };