@entrorove/skill 0.1.23 → 0.1.24

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 (155) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CHANGELOG.md +5 -0
  3. package/SKILL.md +12 -4
  4. package/archguard-runtime-build.mjs +15 -0
  5. package/blueprint-criterion-runner.mjs +110 -0
  6. package/blueprint-criterion-wire.mjs +78 -0
  7. package/calctool-runtime-build.mjs +59 -0
  8. package/calctool-runtime-entry.mjs +92 -0
  9. package/cli.mjs +17 -82
  10. package/local-approved-plan.mjs +74 -15
  11. package/local-archguard-ast.mjs +371 -0
  12. package/local-archguard-checkpoint.mjs +123 -0
  13. package/local-archguard-components.mjs +177 -0
  14. package/local-archguard-contract.mjs +109 -0
  15. package/local-archguard-contracts.mjs +42 -0
  16. package/local-archguard-evaluate.mjs +120 -0
  17. package/local-archguard-language-ast.mjs +165 -0
  18. package/local-archguard-mutation-fence.mjs +28 -0
  19. package/local-archguard-operations.mjs +25 -0
  20. package/local-archguard-scope.mjs +64 -0
  21. package/local-archguard-signing.mjs +52 -0
  22. package/local-archguard-snapshot.mjs +126 -0
  23. package/local-archguard-state.mjs +200 -0
  24. package/local-archguard.mjs +74 -0
  25. package/local-budget-git-attributes.mjs +49 -0
  26. package/local-budget-git-loader.mjs +61 -0
  27. package/local-budget-git-native-attributes.mjs +81 -0
  28. package/local-budget-git-native-darwin.mjs +104 -0
  29. package/local-budget-git-native-linux.mjs +93 -0
  30. package/local-budget-git-native-reads.mjs +88 -0
  31. package/local-budget-git-native-request.mjs +53 -0
  32. package/local-budget-git-native-runtime.mjs +18 -0
  33. package/local-budget-git-native.c +313 -0
  34. package/local-budget-git-native.mjs +109 -0
  35. package/local-budget-git-outcome.mjs +33 -0
  36. package/local-budget-git-scratch-loader.mjs +158 -0
  37. package/local-budget-git-scratch.mjs +112 -0
  38. package/local-budget-git-system-attributes.mjs +44 -0
  39. package/local-budget-git.mjs +92 -0
  40. package/local-budget-grants.mjs +82 -0
  41. package/local-budget-handoff-completion.mjs +70 -0
  42. package/local-budget-handoff-contracts.mjs +53 -0
  43. package/local-budget-handoff.mjs +229 -0
  44. package/local-budget-lock.mjs +55 -0
  45. package/local-budget-reads.mjs +135 -0
  46. package/local-budget-renewal.mjs +86 -34
  47. package/local-budget-source-bindings.mjs +51 -0
  48. package/local-budget-store.mjs +131 -0
  49. package/local-budget-transition.mjs +46 -0
  50. package/local-budget.mjs +185 -0
  51. package/local-calctool-cli.mjs +22 -0
  52. package/local-calctool-contracts.mjs +42 -0
  53. package/local-calctool-runtime.mjs +10 -0
  54. package/local-calctool-storage.mjs +101 -0
  55. package/local-calctool.mjs +103 -0
  56. package/local-cli.mjs +49 -16
  57. package/local-contract-types.mjs +46 -0
  58. package/local-contracts.mjs +124 -0
  59. package/local-credential.mjs +101 -0
  60. package/local-decimal.mjs +85 -0
  61. package/local-evidence-verification.mjs +199 -0
  62. package/local-execution.mjs +115 -304
  63. package/local-final-gate16-bundle.mjs +21 -0
  64. package/local-final-gate16-checks.mjs +88 -0
  65. package/local-final-gate16-contract.mjs +15 -0
  66. package/local-final-gate16-plan.mjs +66 -0
  67. package/local-final-gate16-proof.mjs +75 -0
  68. package/local-final-gate16-release.mjs +17 -0
  69. package/local-final-gate16-state.mjs +70 -0
  70. package/local-final-gate16-worker.mjs +19 -0
  71. package/local-final-gate16.mjs +137 -0
  72. package/local-formula-ast.mjs +97 -0
  73. package/local-formula-engine.mjs +111 -0
  74. package/local-formula-tests.mjs +61 -0
  75. package/local-formula.mjs +25 -125
  76. package/local-integrity.mjs +22 -41
  77. package/local-invocations.mjs +107 -0
  78. package/local-ledger.mjs +2 -7
  79. package/local-merge-archguard-verification.d.mts +44 -0
  80. package/local-merge-archguard-verification.mjs +240 -0
  81. package/local-merge-archguard.mjs +138 -0
  82. package/local-merge-artifacts.mjs +86 -0
  83. package/local-merge-authority.mjs +89 -0
  84. package/local-merge-contracts.mjs +56 -0
  85. package/local-merge-controller.mjs +170 -0
  86. package/local-merge-head.mjs +56 -0
  87. package/local-merge-journal.mjs +77 -0
  88. package/local-merge-mutation.mjs +97 -0
  89. package/local-merge-rollback.mjs +126 -0
  90. package/local-merge-snapshot-rollback.mjs +26 -0
  91. package/local-merge-storage.mjs +103 -0
  92. package/local-merge-targets.mjs +47 -0
  93. package/local-merge-transaction.mjs +133 -0
  94. package/local-mutation-audit.mjs +34 -0
  95. package/local-mutation-authority.mjs +96 -0
  96. package/local-mutation-batch-receipt.mjs +84 -0
  97. package/local-mutation-batch.mjs +143 -0
  98. package/local-mutation-binding.mjs +86 -0
  99. package/local-mutation-files.mjs +245 -0
  100. package/local-mutation-pass-store.mjs +63 -0
  101. package/local-mutation-snapshot.mjs +200 -0
  102. package/local-mutation-wire.mjs +89 -0
  103. package/local-probe.mjs +133 -11
  104. package/local-scope-candidate.mjs +28 -0
  105. package/local-scope-context.mjs +28 -0
  106. package/local-scope-files.mjs +106 -0
  107. package/local-scope-git-attributes.mjs +200 -0
  108. package/local-scope-git-comparison.mjs +8 -0
  109. package/local-scope-git-history.mjs +72 -0
  110. package/local-scope-git-reads.mjs +66 -0
  111. package/local-scope-git.mjs +124 -0
  112. package/local-scope-repository.mjs +249 -0
  113. package/local-scope-signature.mjs +70 -0
  114. package/local-scope-snapshot.mjs +39 -0
  115. package/local-scope-state.mjs +204 -0
  116. package/local-scope-workspaces.mjs +83 -0
  117. package/local-swarm-authority.mjs +143 -0
  118. package/local-swarm-baseline.mjs +32 -0
  119. package/local-swarm-completion.mjs +102 -0
  120. package/local-swarm-contracts.mjs +47 -0
  121. package/local-swarm-mutation.mjs +142 -0
  122. package/local-swarm-priority.mjs +237 -0
  123. package/local-swarm-state.mjs +102 -0
  124. package/local-swarm-storage.mjs +135 -0
  125. package/local-swarm.mjs +179 -0
  126. package/local-task-handoff-completion-wire.mjs +71 -0
  127. package/local-task-handoff-contracts.mjs +66 -0
  128. package/local-task-handoff-fence.mjs +478 -0
  129. package/local-task-handoff-observation.mjs +38 -0
  130. package/local-task-handoff-overlay-wire.mjs +94 -0
  131. package/local-task-handoff-read-authority.mjs +236 -0
  132. package/local-task-handoff-storage.mjs +73 -0
  133. package/local-task-handoff-write-archive.mjs +80 -0
  134. package/local-task-handoff-write-constraints.mjs +66 -0
  135. package/local-task-handoff-write-receipts.mjs +99 -0
  136. package/local-task-handoff-write-wire.mjs +99 -0
  137. package/local-task-routing.mjs +202 -22
  138. package/manifest.json +1 -1
  139. package/package.json +153 -3
  140. package/release-state.json +153 -17
  141. package/release.mjs +238 -4
  142. package/releases.json +6 -0
  143. package/remote-client.mjs +127 -36
  144. package/remote-transport.mjs +80 -0
  145. package/review-client.mjs +38 -0
  146. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  147. package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
  148. package/runtime/git-native-darwin-v1.mjs +5 -0
  149. package/runtime/git-native-linux-v1.mjs +5 -0
  150. package/task-message-cli.mjs +47 -0
  151. package/task-message-entry.mjs +159 -0
  152. package/workflow-local-client.mjs +35 -0
  153. package/workflow-local-contracts.mjs +58 -0
  154. package/workflow-local-request.mjs +53 -0
  155. package/workflow-local-runner.mjs +350 -0
@@ -0,0 +1,371 @@
1
+ import ts from 'typescript';
2
+ import { createHash } from 'node:crypto';
3
+ import { analyzeArchguardLanguage, archguardNativeLanguage } from './local-archguard-language-ast.mjs';
4
+ import { analyzeArchguardComponent, archguardComponentLanguage } from './local-archguard-components.mjs';
5
+
6
+ const LOCKFILES = new Set(['pnpm-lock.yaml', 'package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock',
7
+ 'bun.lock', 'bun.lockb', 'Cargo.lock', 'go.sum', 'composer.lock', 'Gemfile.lock', 'Pipfile.lock',
8
+ 'poetry.lock', 'uv.lock', 'pdm.lock', 'pubspec.lock', 'packages.lock.json', 'Package.resolved']);
9
+ const THIRD_PARTY = new Set(['node_modules', 'vendor', 'third_party', 'third-party', '.venv', 'site-packages']);
10
+ const SCRIPT_KINDS = new Map([['ts', ts.ScriptKind.TS], ['mts', ts.ScriptKind.TS], ['cts', ts.ScriptKind.TS],
11
+ ['tsx', ts.ScriptKind.TSX], ['js', ts.ScriptKind.JS], ['mjs', ts.ScriptKind.JS], ['cjs', ts.ScriptKind.JS], ['jsx', ts.ScriptKind.JSX]]);
12
+ const NON_SOURCE_EXTENSIONS = new Set(['txt', 'md', 'markdown', 'json', 'jsonc', 'json5', 'yaml', 'yml',
13
+ 'toml', 'xml', 'ini', 'csv', 'tsv', 'log', 'map', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico',
14
+ 'woff', 'woff2', 'ttf', 'otf']);
15
+ const NON_SOURCE_NAMES = new Set(['readme', 'license', 'licence', 'notice', 'changelog', 'authors', 'codeowners',
16
+ '.gitignore', '.gitattributes', '.editorconfig', '.npmrc', '.yarnrc', '.nvmrc', '.node-version', '.python-version',
17
+ '.env', '.env.example', '.env.local', '.env.development', '.env.production', '.env.test']);
18
+ const COLOR_PROPERTIES = new Set(['color', 'background', 'backgroundColor', 'border', 'borderColor',
19
+ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor', 'fill', 'stroke', 'boxShadow', 'textShadow']);
20
+ const BUILTINS = new Map([
21
+ ['no-eval', ['security', 'Dynamic code evaluation is forbidden.']],
22
+ ['no-debug', ['standard', 'Remove debug statements.']],
23
+ ['no-inline-style', ['standard', 'Use the locked styling system instead of inline style.']],
24
+ ['no-hardcoded-color', ['standard', 'Use project theme tokens instead of hardcoded colors.']],
25
+ ['no-magic-number', ['standard', 'Name repeated or domain-significant numeric values.']],
26
+ ]);
27
+ function error(code, message) { return Object.assign(new Error(`${code}: ${message}`), { code }); }
28
+ function finding(ruleId, path, message, category = 'architecture', blocking = true, evidence = {}) {
29
+ return { severity: blocking ? (category === 'architecture' ? 'P0' : 'P1') : 'P2', ruleId,
30
+ entityRef: path, message, category, blocking, evidence };
31
+ }
32
+ function safePath(path) {
33
+ return typeof path === 'string' && path.length > 0 && path === path.normalize('NFC') && path === path.trim()
34
+ && !path.startsWith('/') && !path.startsWith('~') && !/[\\:\u0000-\u001f\u007f]/.test(path)
35
+ && path.split('/').every(part => part && part !== '.' && part !== '..' && !/[. ]$/.test(part)
36
+ && !/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?$/i.test(part));
37
+ }
38
+ function nonSourceFile(path) {
39
+ const basename = path.split('/').at(-1).toLowerCase();
40
+ return NON_SOURCE_NAMES.has(basename) || NON_SOURCE_EXTENSIONS.has(basename.split('.').at(-1));
41
+ }
42
+ export function archguardInspectionScope(path, content, audit) {
43
+ if (!safePath(path)) return 'first-party';
44
+ const parts = path.split('/'); const directories = parts.slice(0, -1);
45
+ if (directories.some(part => THIRD_PARTY.has(part))) return 'third-party';
46
+ if (LOCKFILES.has(parts.at(-1))) return 'dependency-lockfile';
47
+ const pin = audit?.[`generatedSourcePin:${path}`];
48
+ if (typeof content === 'string' && typeof pin === 'string' && /^[0-9a-f]{64}$/.test(pin)
49
+ && pin === createHash('sha256').update(content).digest('hex')) return 'generated';
50
+ return 'first-party';
51
+ }
52
+ function syntaxKind(pattern) {
53
+ const kind = Object.hasOwn(ts.SyntaxKind, pattern) ? ts.SyntaxKind[pattern] : undefined;
54
+ const literals = [ts.SyntaxKind.Identifier, ts.SyntaxKind.PrivateIdentifier, ts.SyntaxKind.NumericLiteral,
55
+ ts.SyntaxKind.BigIntLiteral, ts.SyntaxKind.StringLiteral, ts.SyntaxKind.NoSubstitutionTemplateLiteral,
56
+ ts.SyntaxKind.RegularExpressionLiteral];
57
+ if (typeof kind !== 'number' || /^(?:First|Last|JSDoc)|^(?:Count|SyntaxList|Bundle|Unknown)$/.test(pattern)
58
+ || (kind < ts.SyntaxKind.FirstNode && !literals.includes(kind))) {
59
+ throw error('ARCH-AST-SELECTOR-UNSUPPORTED', `Unsupported SyntaxKind selector ${String(pattern)}`);
60
+ }
61
+ return kind;
62
+ }
63
+ export function validateArchguardAstRules(rules) {
64
+ if (!Array.isArray(rules)) throw error('ARCH-RULES-INVALID', 'Custom rules must be an array');
65
+ for (const rule of rules) {
66
+ if (!rule || typeof rule.id !== 'string' || !rule.id || typeof rule.pattern !== 'string'
67
+ || typeof rule.message !== 'string' || typeof rule.blocking !== 'boolean' || !['ast', 'regex'].includes(rule.engine)) {
68
+ throw error('ARCH-RULES-INVALID', 'Custom rules require id, engine, pattern, message and blocking');
69
+ }
70
+ if (rule.engine === 'ast') syntaxKind(rule.pattern);
71
+ else {
72
+ if (/\\[1-9]|\(\?<|\(\?=|\(\?!/.test(rule.pattern)) throw error('ARCH-REGEX-UNSUPPORTED', 'Unsafe regex rule');
73
+ try { new RegExp(rule.pattern, 'gu'); } catch { throw error('ARCH-REGEX-UNSUPPORTED', 'Invalid regex rule'); }
74
+ }
75
+ }
76
+ }
77
+ function position(node, source) {
78
+ const start = node.getStart(source); const at = source.getLineAndCharacterOfPosition(start);
79
+ return { line: at.line + 1, column: at.character + 1, start, end: node.end, syntaxKind: ts.SyntaxKind[node.kind] };
80
+ }
81
+ function unwrap(node) {
82
+ while (node) {
83
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
84
+ || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) node = node.expression;
85
+ else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.CommaToken) node = node.right;
86
+ else break;
87
+ }
88
+ return node;
89
+ }
90
+ function memberName(node) {
91
+ node = unwrap(node);
92
+ if (ts.isIdentifier(node)) return node.text;
93
+ if (ts.isPropertyAccessExpression(node)) return `${memberName(node.expression)}.${node.name.text}`;
94
+ if (ts.isElementAccessExpression(node) && node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression)) {
95
+ return `${memberName(node.expression)}.${node.argumentExpression.text}`;
96
+ }
97
+ return '';
98
+ }
99
+ function propertyName(node) {
100
+ return node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : '';
101
+ }
102
+ function valueProperty(node) {
103
+ const parent = node.parent;
104
+ if (parent && ts.isPropertyAssignment(parent) && parent.initializer === node) return propertyName(parent.name);
105
+ if (parent && ts.isJsxAttribute(parent)) return parent.name.getText();
106
+ if (parent && ts.isJsxExpression(parent) && parent.parent && ts.isJsxAttribute(parent.parent)) return parent.parent.name.getText();
107
+ return '';
108
+ }
109
+ function builtinMatches(node) {
110
+ const matches = [];
111
+ const call = ts.isCallExpression(node); const construct = ts.isNewExpression(node);
112
+ const name = call || construct ? memberName(node.expression) : '';
113
+ if ((call && /^(?:(?:globalThis|window|global)\.)?eval$/.test(name))
114
+ || ((call || construct) && /^(?:(?:globalThis|window|global)\.)?Function$/.test(name))) matches.push('no-eval');
115
+ if (ts.isDebuggerStatement(node) || (call && /^(?:(?:globalThis|window|global)\.)?console\.(?:log|debug)$/.test(name))) matches.push('no-debug');
116
+ if (ts.isJsxAttribute(node) && node.name.getText() === 'style') matches.push('no-inline-style');
117
+ if (ts.isStringLiteralLike(node) && COLOR_PROPERTIES.has(valueProperty(node))
118
+ && /#[0-9a-f]{3,8}\b|\brgba?\s*\(|\bhsla?\s*\(/i.test(node.text)) matches.push('no-hardcoded-color');
119
+ if (ts.isNumericLiteral(node) && Number(node.text) > 1) matches.push('no-magic-number');
120
+ return matches;
121
+ }
122
+ function importedDependency(node) {
123
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier
124
+ && ts.isStringLiteralLike(node.moduleSpecifier)) return node.moduleSpecifier.text;
125
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
126
+ && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) return node.moduleReference.expression.text;
127
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)
128
+ && ts.isStringLiteralLike(node.argument.literal)) return node.argument.literal.text;
129
+ if (ts.isCallExpression(node) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0])
130
+ && (node.expression.kind === ts.SyntaxKind.ImportKeyword || memberName(node.expression) === 'require')) return node.arguments[0].text;
131
+ return null;
132
+ }
133
+ function bodyFunction(node) {
134
+ return ts.isFunctionLike(node) && node.body !== undefined;
135
+ }
136
+ function controlNode(node) {
137
+ return ts.isIfStatement(node) || ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)
138
+ || ts.isWhileStatement(node) || ts.isDoStatement(node) || ts.isSwitchStatement(node) || ts.isTryStatement(node)
139
+ || ts.isCatchClause(node) || ts.isWithStatement(node) || ts.isConditionalExpression(node);
140
+ }
141
+ function nestingIncrement(node) {
142
+ if (controlNode(node)) return 1;
143
+ if (!ts.isBlock(node)) return 0;
144
+ if (node.parent && (controlNode(node.parent) || ts.isCaseClause(node.parent) || ts.isDefaultClause(node.parent))) return 0;
145
+ return 1;
146
+ }
147
+ function astAnalysis(source, rules) {
148
+ const selected = new Map(rules.filter(rule => rule.engine === 'ast').map(rule => [rule.id, { rule, kind: syntaxKind(rule.pattern), locations: [] }]));
149
+ const builtin = new Map([...BUILTINS.keys()].map(id => [id, []]));
150
+ const dependencies = []; let maxFunctionLines = 0; let maxNestingDepth = 0; let animations = 0; let transitions = 0;
151
+ function visit(node, depth) {
152
+ const nextDepth = depth + nestingIncrement(node); maxNestingDepth = Math.max(maxNestingDepth, nextDepth);
153
+ if (bodyFunction(node)) {
154
+ const start = source.getLineAndCharacterOfPosition(node.getStart(source)).line;
155
+ const end = source.getLineAndCharacterOfPosition(Math.max(node.getStart(source), node.end - 1)).line;
156
+ maxFunctionLines = Math.max(maxFunctionLines, end - start + 1);
157
+ }
158
+ for (const selectedRule of selected.values()) if (node.kind === selectedRule.kind) selectedRule.locations.push(position(node, source));
159
+ for (const id of builtinMatches(node)) builtin.get(id).push(position(node, source));
160
+ const dependency = importedDependency(node); if (dependency) dependencies.push({ dependency, ...position(node, source) });
161
+ if (ts.isPropertyAssignment(node) || ts.isJsxAttribute(node)) {
162
+ const property = propertyName(node.name);
163
+ if (['animation', 'animationName'].includes(property)) animations += 1;
164
+ if (['transition', 'transitionProperty'].includes(property)) transitions += 1;
165
+ }
166
+ ts.forEachChild(node, child => visit(child, nextDepth));
167
+ }
168
+ visit(source, 0);
169
+ return { selected, builtin, dependencies, maxFunctionLines, maxNestingDepth, animations, transitions };
170
+ }
171
+ function manifestDependencies(file) {
172
+ const basename = file.path.split('/').at(-1);
173
+ if (basename === 'package.json') {
174
+ const source = ts.parseJsonText(file.path, file.content);
175
+ if (source.parseDiagnostics.length) throw error('ARCH-DEPENDENCY-MANIFEST-INVALID', 'Invalid package.json syntax');
176
+ const manifest = JSON.parse(file.content);
177
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw error('ARCH-DEPENDENCY-MANIFEST-INVALID', 'package.json must be an object');
178
+ return ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].flatMap(key => {
179
+ const entries = manifest[key];
180
+ if (entries === undefined) return [];
181
+ if (!entries || typeof entries !== 'object' || Array.isArray(entries)
182
+ || Object.values(entries).some(value => typeof value !== 'string')) throw error('ARCH-DEPENDENCY-MANIFEST-INVALID', `Invalid ${key} declarations`);
183
+ return Object.entries(entries).flatMap(([name, declaration]) => {
184
+ const alias = declaration.match(/^npm:(@[^/\s]+\/[^@\s]+|[^@\s]+)(?:@.*)?$/)?.[1];
185
+ return alias ? [name, alias] : [name];
186
+ });
187
+ });
188
+ }
189
+ if (/^requirements(?:[-.][^/]*)?\.txt$/i.test(basename)) return file.content.split(/\r?\n/)
190
+ .map(line => line.replace(/\s+#.*$/, '').trim()).filter(line => line && !line.startsWith('#') && !line.startsWith('-'))
191
+ .map(line => line.split(/[<>=!~\[;\s]/, 1)[0]);
192
+ if (basename === 'pom.xml') return [...file.content.matchAll(/<dependency\b[^>]*>[\s\S]*?<artifactId>\s*([^<\s]+)\s*<\/artifactId>[\s\S]*?<\/dependency>/g)].map(match => match[1]);
193
+ if (basename === 'Cargo.toml') {
194
+ let section = false; let dependencyTable = false;
195
+ return file.content.split(/\r?\n/).flatMap(raw => {
196
+ const line = raw.replace(/\s+#.*$/, '');
197
+ const heading = line.trim().match(/^\[([^\]]+)]$/);
198
+ if (heading) {
199
+ const dependency = heading[1].match(/(?:^|\.)(?:(?:dev|build)-)?dependencies(?:\.([A-Za-z0-9_-]+))?$/);
200
+ section = Boolean(dependency); dependencyTable = Boolean(dependency?.[1]);
201
+ return dependencyTable ? [dependency[1]] : [];
202
+ }
203
+ const item = section ? line.match(/^\s*([A-Za-z0-9_-]+)\s*=/)?.[1] : undefined;
204
+ const renamed = section && (dependencyTable || item) ? line.match(/\bpackage\s*=\s*["']([^"']+)["']/)?.[1] : undefined;
205
+ return [...(item && !dependencyTable ? [item] : []), ...(renamed ? [renamed] : [])];
206
+ });
207
+ }
208
+ return [];
209
+ }
210
+ const dependencyKey = value => value.toLowerCase().replace(/[._]+/g, '-');
211
+ function dependencyMatch(actual, forbidden) {
212
+ return dependencyKey(actual) === dependencyKey(forbidden) || actual.startsWith(`${forbidden}/`)
213
+ || actual.startsWith(`${forbidden}.`)
214
+ || dependencyKey(actual.split(':').at(-1)) === dependencyKey(forbidden);
215
+ }
216
+ function dependencyFindings(contract, file, imported = []) {
217
+ let declared;
218
+ try { declared = manifestDependencies(file); } catch (cause) {
219
+ return [finding('ARCH-DEPENDENCY-MANIFEST-INVALID', file.path, 'Dependency manifest cannot be inspected.', 'architecture', true, { reason: cause.code ?? 'INVALID_JSON' })];
220
+ }
221
+ const forbidden = [...new Set([...(contract.stack.frontend?.forbiddenDeps ?? []), ...(contract.stack.backend?.forbiddenDeps ?? [])])];
222
+ return forbidden.flatMap(dependency => {
223
+ const locations = imported.filter(item => dependencyMatch(item.dependency, dependency));
224
+ const declarations = declared.filter(item => dependencyMatch(item, dependency));
225
+ return locations.length || declarations.length ? [finding('ARCH-FORBIDDEN-DEPENDENCY', file.path,
226
+ `Dependency ${dependency} is forbidden by the contract.`, 'architecture', true, { dependency, declarations, locations })] : [];
227
+ });
228
+ }
229
+ function languageFindings(contract, file) {
230
+ const languages = [contract.stack.frontend?.language, contract.stack.backend?.language].filter(Boolean);
231
+ const extensions = new Set(languages.flatMap(language => language === 'typescript' ? ['ts', 'tsx', 'mts', 'cts']
232
+ : language.startsWith('nodejs') ? ['js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'mts', 'cts']
233
+ : language === 'javascript' ? ['js', 'jsx', 'mjs', 'cjs']
234
+ : language.startsWith('python') ? ['py'] : language.startsWith('java') ? ['java'] : language.startsWith('rust') ? ['rs'] : []));
235
+ const extension = file.path.split('.').at(-1);
236
+ const framework = contract.stack.frontend?.framework ?? '';
237
+ if (framework.startsWith('vue')) extensions.add('vue');
238
+ if (framework.startsWith('svelte')) extensions.add('svelte');
239
+ if (framework.startsWith('wechat')) extensions.add('wxml');
240
+ return (SCRIPT_KINDS.has(extension) || archguardComponentLanguage(file.path) || ['py', 'java', 'rs', 'go', 'c', 'cpp'].includes(extension))
241
+ && extensions.size && !extensions.has(extension) ? [finding('ARCH-LANGUAGE', file.path,
242
+ `File extension .${extension} is outside the locked languages.`, 'architecture', true, { allowedExtensions: [...extensions] })] : [];
243
+ }
244
+ function budgetFindings(contract, metric) {
245
+ if (!metric.complexityChecked) return [];
246
+ const checks = [['maxFileLines', 'fileLines'], ['maxFunctionLines', 'maxFunctionLines'],
247
+ ['maxNestingDepth', 'maxNestingDepth'], ['animations', 'animations'], ['transitions', 'transitions']];
248
+ return checks.flatMap(([budgetKey, metricKey]) => {
249
+ const budget = contract.budgets[budgetKey];
250
+ if (!Number.isSafeInteger(budget) || budget < -1) throw error('ARCH-BUDGET-INVALID', `Invalid ${budgetKey} budget`);
251
+ return budget >= 0 && metric[metricKey] > budget ? [finding(`BUDGET-${budgetKey.toUpperCase()}`, metric.path,
252
+ `${metricKey} ${metric[metricKey]} exceeds budget ${budget}.`, 'complexity', true, { actual: metric[metricKey], budget })] : [];
253
+ });
254
+ }
255
+ function evaluatedRules(contract, file, analysis) {
256
+ const findings = [];
257
+ for (const [id, locations] of analysis.builtin) {
258
+ if (!locations.length) continue;
259
+ const [category, message] = BUILTINS.get(id);
260
+ const blocking = category === 'security' || contract.rules.strictStandard || contract.rules.blocking.includes(id);
261
+ findings.push(finding(id, file.path, message, category, blocking, { matches: locations.length, locations }));
262
+ }
263
+ for (const { rule, locations } of analysis.selected.values()) if (locations.length) {
264
+ findings.push(finding(rule.id, file.path, rule.message, 'custom', rule.blocking, { matches: locations.length, locations }));
265
+ }
266
+ return [...findings, ...regexFindings(contract, file)];
267
+ }
268
+ function regexFindings(contract, file) {
269
+ const findings = [];
270
+ for (const rule of contract.rules.custom.filter(rule => rule.engine === 'regex')) {
271
+ const matches = [...file.content.matchAll(new RegExp(rule.pattern, 'gu'))];
272
+ if (matches.length) findings.push(finding(rule.id, file.path, rule.message, 'custom', rule.blocking,
273
+ { matches: matches.length, offsets: matches.map(match => match.index) }));
274
+ }
275
+ return findings;
276
+ }
277
+ function syntaxDiagnostics(source) {
278
+ const fileName = source.fileName;
279
+ const host = { getSourceFile: name => name === fileName ? source : undefined,
280
+ getDefaultLibFileName: () => '', writeFile: () => {}, getCurrentDirectory: () => '',
281
+ getDirectories: () => [], fileExists: name => name === fileName,
282
+ readFile: name => name === fileName ? source.text : undefined, getCanonicalFileName: name => name,
283
+ useCaseSensitiveFileNames: () => true, getNewLine: () => '\n' };
284
+ const program = ts.createProgram([fileName], { allowJs: true, noLib: true, noResolve: true,
285
+ target: ts.ScriptTarget.Latest, jsx: ts.JsxEmit.Preserve }, host);
286
+ return program.getSyntacticDiagnostics(source);
287
+ }
288
+ function analyzeScriptFile(file, rules) {
289
+ const source = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, true, SCRIPT_KINDS.get(file.path.split('.').at(-1)));
290
+ const diagnostics = syntaxDiagnostics(source).map(diagnostic => {
291
+ const at = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
292
+ return { code: diagnostic.code, start: diagnostic.start ?? 0, end: (diagnostic.start ?? 0) + (diagnostic.length ?? 0),
293
+ line: at.line + 1, column: at.character + 1, message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') };
294
+ });
295
+ return diagnostics.length ? { diagnostics } : { ...astAnalysis(source, rules), diagnostics: [] };
296
+ }
297
+ function evaluateFile(contract, file) {
298
+ const inspectionScope = archguardInspectionScope(file.path, file.content, contract.audit);
299
+ const metric = { path: file.path, inspectionScope, complexityChecked: false, fileLines: file.content.split(/\r?\n/).length,
300
+ maxNestingDepth: 0, maxFunctionLines: 0, animations: 0, transitions: 0 };
301
+ if (!safePath(file.path)) return { metric, findings: [finding('ARCH-PATH', file.path, 'File path is unsafe.')] };
302
+ if (inspectionScope !== 'first-party') return { metric,
303
+ findings: inspectionScope === 'generated' ? dependencyFindings(contract, file) : [] };
304
+ const findings = languageFindings(contract, file);
305
+ if (file.path === 'arch.contract.yaml') findings.push(finding('ARCH-CONTRACT-MUTATION', file.path,
306
+ 'The locked architecture contract cannot be changed by checkpoint.'));
307
+ const extension = file.path.split('.').at(-1); const scriptKind = SCRIPT_KINDS.get(extension);
308
+ if (archguardComponentLanguage(file.path)) {
309
+ const analysis = analyzeArchguardComponent(file, contract.rules.custom, analyzeScriptFile);
310
+ if (analysis.diagnostics.length) findings.push(finding('ARCH-AST-SYNTAX', file.path,
311
+ 'Component contains parser errors or an uninspectable block.', 'architecture', true, { diagnostics: analysis.diagnostics }));
312
+ else {
313
+ for (const key of ['maxNestingDepth', 'maxFunctionLines', 'animations', 'transitions']) metric[key] = analysis[key];
314
+ metric.complexityChecked = true;
315
+ findings.push(...dependencyFindings(contract, file, analysis.dependencies), ...evaluatedRules(contract, file, analysis), ...budgetFindings(contract, metric));
316
+ }
317
+ return { metric, findings };
318
+ }
319
+ if (archguardNativeLanguage(file.path)) {
320
+ const analysis = analyzeArchguardLanguage(file, contract.rules.custom);
321
+ if (analysis.diagnostics.length) findings.push(finding('ARCH-AST-SYNTAX', file.path,
322
+ 'Source contains parser errors; AST inspection is incomplete.', 'architecture', true, { diagnostics: analysis.diagnostics }));
323
+ else {
324
+ for (const key of ['maxNestingDepth', 'maxFunctionLines', 'animations', 'transitions']) metric[key] = analysis[key];
325
+ metric.complexityChecked = true;
326
+ findings.push(...dependencyFindings(contract, file, analysis.dependencies), ...evaluatedRules(contract, file, analysis), ...budgetFindings(contract, metric));
327
+ }
328
+ return { metric, findings };
329
+ }
330
+ if (scriptKind === undefined) {
331
+ findings.push(...dependencyFindings(contract, file), ...regexFindings(contract, file));
332
+ if (!nonSourceFile(file.path)) {
333
+ findings.push(finding('ARCH-AST-LANGUAGE-UNSUPPORTED', file.path,
334
+ 'AST language is unsupported. Supported source: TypeScript/JavaScript, CSS/WXSS, Vue/Svelte/WXML, Python, Java and Rust.',
335
+ 'architecture', true, { extension }));
336
+ } else if (!findings.some(item => item.ruleId === 'ARCH-DEPENDENCY-MANIFEST-INVALID')) metric.complexityChecked = true;
337
+ return { metric, findings: [...findings, ...budgetFindings(contract, metric)] };
338
+ }
339
+ const source = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, true, scriptKind);
340
+ const diagnostics = syntaxDiagnostics(source);
341
+ if (diagnostics.length) {
342
+ findings.push(finding('ARCH-AST-SYNTAX', file.path, 'Source contains parser errors; AST inspection is incomplete.', 'architecture', true,
343
+ { diagnostics: diagnostics.map(diagnostic => {
344
+ const at = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
345
+ return { code: diagnostic.code, line: at.line + 1, column: at.character + 1,
346
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') };
347
+ }) }));
348
+ return { metric, findings };
349
+ }
350
+ const analysis = astAnalysis(source, contract.rules.custom);
351
+ for (const key of ['maxNestingDepth', 'maxFunctionLines', 'animations', 'transitions']) metric[key] = analysis[key];
352
+ metric.complexityChecked = true;
353
+ findings.push(...dependencyFindings(contract, file, analysis.dependencies), ...evaluatedRules(contract, file, analysis), ...budgetFindings(contract, metric));
354
+ return { metric, findings };
355
+ }
356
+ export function evaluateArchguardFiles(contract, files) {
357
+ if (!contract?.rules || !contract?.stack || !contract?.budgets || !Array.isArray(contract.rules.blocking)
358
+ || typeof contract.rules.strictStandard !== 'boolean') throw error('ARCH-CONTRACT-INVALID', 'Inspection needs a normalized architecture contract');
359
+ validateArchguardAstRules(contract.rules.custom);
360
+ if (!Array.isArray(files) || !files.length) throw error('ARCH-FILES-EMPTY', 'At least one actual file is required');
361
+ const paths = new Set();
362
+ for (const file of files) {
363
+ if (!file || typeof file.path !== 'string' || typeof file.content !== 'string'
364
+ || Object.keys(file).some(key => !['path', 'content'].includes(key)) || paths.has(file.path)) {
365
+ throw error('ARCH-FILE-INVALID', 'Files require a unique path and content without caller-supplied evidence');
366
+ }
367
+ paths.add(file.path);
368
+ }
369
+ const evaluated = files.map(file => evaluateFile(contract, file));
370
+ return { findings: evaluated.flatMap(item => item.findings), metrics: evaluated.map(item => item.metric) };
371
+ }
@@ -0,0 +1,123 @@
1
+ import { withArchguardMutationFence } from './local-archguard-mutation-fence.mjs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { verifyMutationPass } from './local-execution.mjs';
5
+ import { context, canonical, sha256, safePath, safeId, readBytes, readJson, fail, authority,
6
+ verifyAuthority, withExclusiveLock, appendLedger, readLedger } from './local-archguard-state.mjs';
7
+ import { freezeContract } from './local-archguard-contract.mjs';
8
+ import { loadSnapshot, restoreSnapshot, MISSING_SHA256, history, drift } from './local-archguard-snapshot.mjs';
9
+ import { evaluateArchguardCheckpoint, parseArchguardContract, evaluateArchguardDrift } from './local-archguard-evaluate.mjs';
10
+ import { signRunnerReceipt } from './local-archguard-signing.mjs';
11
+
12
+ async function verifyWriteAuthorization(ctx, input, snapshot) {
13
+ const savedPass = await readJson(input.gatePassPath);
14
+ const verified = await verifyMutationPass({ ...input, repositoryRoot: ctx.root, targetPath: input.path });
15
+ const pass = verified.pass;
16
+ if (canonical(pass) !== canonical(savedPass) || canonical(await readJson(input.gatePassPath)) !== canonical(savedPass)) fail('ARCH_PASS_RACE', 'gate pass changed during verification');
17
+ if (pass.snapshotId !== snapshot.snapshotId || pass.snapshotReceipt.snapshotDigest !== snapshot.snapshotDigest
18
+ || pass.snapshotReceipt.contractDigest !== snapshot.contractDigest || pass.stepId !== snapshot.stepId
19
+ || canonical(pass.paths) !== canonical([...snapshot.paths, 'arch.contract.yaml'].sort())) fail('ARCH_PASS_BINDING', 'gate pass is not bound to the exact snapshot and path set');
20
+ const verifiedScope = await verifyAuthority(ctx, input.scopeAuthorization, 'entrorove.archguard-scope-authorization/1.0');
21
+ const scope = verifiedScope.value;
22
+ if (scope.stepId !== snapshot.stepId || scope.snapshotDigest !== snapshot.snapshotDigest || scope.contractDigest !== snapshot.contractDigest
23
+ || scope.passDigest !== sha256(canonical(pass)) || canonical(scope.paths) !== canonical(snapshot.paths)
24
+ || !verifiedScope.authority.actors.includes(scope.ownerId) || typeof scope.leaseId !== 'string' || !scope.leaseId
25
+ || !Number.isSafeInteger(scope.generation) || scope.generation < 1
26
+ || ['taskDigest', 'chainDigest', 'scopeArtifactDigest'].some(key => !/^[0-9a-f]{64}$/.test(scope[key]))) fail('ARCH_SCOPE_AUTHORIZATION', 'signed scope authorization does not bind the accepted owner, task, chain, lease, snapshot, pass, and paths');
27
+ return { scope, pass };
28
+ }
29
+ function invalidCheckpoint(snapshot, input, afterSha256, error) {
30
+ const ruleId = /^[A-Z0-9_-]+$/.test(error.code ?? '') ? error.code : 'ARCH_LOCAL_EXECUTION_FAILED';
31
+ const finding = { severity: 'P1', ruleId, entityRef: input.path, message: `Local authorization or contract verification failed (${ruleId}).`,
32
+ category: 'authorization', blocking: true, evidence: error.evidence ?? {} };
33
+ return { checkpoint: { status: 'blocked', writeAllowed: false, rollbackRequired: true, findings: [finding],
34
+ metrics: { path: input.path, inspectionScope: 'first-party', complexityChecked: false, fileLines: 0,
35
+ maxFunctionLines: 0, maxNestingDepth: 0, animations: 0, transitions: 0 }, instruction: 'rollback-block-and-correct' },
36
+ ledgerEntry: { schemaVersion: 'archguard.checkpoint-ledger/1.0', checkpointId: sha256(canonical({ snapshot: snapshot.snapshotDigest, blockId: input.blockId, ruleId, afterSha256 })),
37
+ blockId: input.blockId, path: input.path, contractRevision: snapshot.contractRevision, beforeSha256: snapshot.files.find(file => file.path === input.path).sha256,
38
+ afterSha256, status: 'blocked', ruleIds: [ruleId], blockingRuleIds: [ruleId], rollbackRequired: true } };
39
+ }
40
+ async function checkpointReceipt(ctx, input, snapshot, result, authorization, rollback) {
41
+ const scope = authorization.scope; const entry = result.ledgerEntry;
42
+ const payload = { schemaVersion: 'entrorove.archguard-checkpoint-receipt/1.0', repositoryDigest: ctx.repositoryDigest,
43
+ flowId: ctx.flowId, stepId: input.stepId, ownerId: scope.ownerId,
44
+ leaseId: scope.leaseId, generation: scope.generation,
45
+ taskDigest: scope.taskDigest, chainDigest: scope.chainDigest, scopeArtifactDigest: scope.scopeArtifactDigest,
46
+ contractDigest: snapshot.contractDigest, contractRevision: entry.contractRevision, snapshotDigest: snapshot.snapshotDigest,
47
+ passDigest: sha256(canonical(authorization.pass)), path: input.path, blockId: input.blockId,
48
+ beforeSha256: entry.beforeSha256, afterSha256: entry.afterSha256, checkpoint: result.checkpoint, ledgerEntry: entry,
49
+ rollbackOutcome: rollback.restored ? 'restored' : 'retained', issuedAt: new Date().toISOString(),
50
+ expiresAt: new Date(Date.now() + 60_000).toISOString(), nonce: randomUUID(),
51
+ scopeAuthorizationDigest: sha256(canonical(input.scopeAuthorization)) };
52
+ return signRunnerReceipt(ctx, payload);
53
+ }
54
+ async function checkpointLocked(ctx, input) {
55
+ safeId(input.stepId, 'stepId'); safeId(input.blockId, 'blockId'); safePath(input.path);
56
+ const ledger = await readLedger(ctx); const snapshot = await loadSnapshot(ctx, input.snapshotId);
57
+ if (ledger.entries.some(entry => entry.event === 'checkpoint' && entry.data.snapshotId === snapshot.snapshotId)) fail('ARCH_SNAPSHOT_CONSUMED', 'snapshot checkpoint was already consumed; replay rejected');
58
+ if (snapshot.stepId !== input.stepId || !snapshot.paths.includes(input.path)) fail('ARCH_CHECKPOINT_SCOPE', 'checkpoint path or step is outside the snapshot scope');
59
+ const original = snapshot.files.find(file => file.path === input.path); let authorization; let result; let current;
60
+ let afterSha256 = MISSING_SHA256; let contract;
61
+ const contractBaseline = snapshot.files.find(file => file.path === 'arch.contract.yaml');
62
+ contract = await parseArchguardContract(Buffer.from(contractBaseline.content, 'base64').toString('utf8'));
63
+ snapshot.contractRevision = contract.revision;
64
+ try {
65
+ if (drift(ledger).interrupted) fail('ARCH_RED_INTERRUPT', 'mutation is interrupted by three consecutive blocks');
66
+ authorization = await verifyWriteAuthorization(ctx, input, snapshot);
67
+ const locked = await freezeContract(ctx);
68
+ if (locked.contractDigest !== snapshot.contractDigest || locked.rawDigest !== snapshot.contractRawDigest) fail('ARCH_CONTRACT_DRIFT', 'checkpoint contract differs from its original snapshot');
69
+ for (const expected of snapshot.files.filter(file => file.path !== input.path && file.path !== 'arch.contract.yaml')) {
70
+ const actual = await readBytes(path.join(ctx.root, expected.path), true, ctx);
71
+ const actualSha256 = actual.exists ? sha256(actual.content) : MISSING_SHA256;
72
+ if (actualSha256 !== expected.sha256) throw Object.assign(new Error('another source path changed without its own block checkpoint'), {
73
+ code: 'ARCH_UNCHECKPOINTED_PATH', evidence: { path: expected.path, beforeSha256: expected.sha256, afterSha256: actualSha256 } });
74
+ }
75
+ current = await readBytes(path.join(ctx.root, input.path), false, ctx); afterSha256 = sha256(current.content);
76
+ result = await evaluateArchguardCheckpoint(contract, { blockId: input.blockId, path: input.path,
77
+ content: current.content.toString('utf8'), beforeSha256: original.sha256 }, history(ledger));
78
+ } catch (error) {
79
+ if (!current) {
80
+ try { current = await readBytes(path.join(ctx.root, input.path), true, ctx); afterSha256 = current.exists ? sha256(current.content) : MISSING_SHA256; } catch { /* No unsafe file read is necessary for rollback. */ }
81
+ }
82
+ result = invalidCheckpoint(snapshot, input, afterSha256, error);
83
+ }
84
+ let status = result.checkpoint.status;
85
+ if (current) {
86
+ try {
87
+ const latest = await readBytes(path.join(ctx.root, input.path), true, ctx);
88
+ if (latest.exists !== current.exists || (latest.exists && sha256(latest.content) !== afterSha256)) {
89
+ afterSha256 = latest.exists ? sha256(latest.content) : MISSING_SHA256;
90
+ fail('ARCH_CHECKPOINT_RACE', 'source changed during checkpoint; no stale result was accepted');
91
+ }
92
+ } catch (error) { result = invalidCheckpoint(snapshot, input, afterSha256, error); status = 'blocked'; }
93
+ }
94
+ result.checkpoint.drift = await evaluateArchguardDrift([...history(ledger), result.ledgerEntry]);
95
+ let rollback = status === 'blocked' ? await restoreSnapshot(ctx, snapshot) : { restored: false, paths: [], reason: 'passed block retained' }; let receipt = null;
96
+ try { receipt = authorization ? await checkpointReceipt(ctx, input, snapshot, result, authorization, rollback) : null; }
97
+ catch (error) {
98
+ result = invalidCheckpoint(snapshot, input, afterSha256, error); status = 'blocked';
99
+ result.checkpoint.drift = await evaluateArchguardDrift([...history(ledger), result.ledgerEntry]);
100
+ if (!rollback.restored) rollback = await restoreSnapshot(ctx, snapshot);
101
+ }
102
+ let record;
103
+ try {
104
+ record = await appendLedger(ctx, 'checkpoint', { snapshotId: snapshot.snapshotId, status,
105
+ contractDigest: snapshot.contractDigest, ledgerEntry: result.ledgerEntry, checkpoint: result.checkpoint, rollback,
106
+ receiptDigest: receipt ? sha256(canonical(receipt)) : null });
107
+ } catch (error) {
108
+ if (!rollback.restored) await restoreSnapshot(ctx, snapshot);
109
+ fail('ARCH_CHECKPOINT_AUDIT_FAILED', `checkpoint append failed (${error.code ?? 'state error'}); block was restored and no success may be reported`);
110
+ }
111
+ return { schemaVersion: 'entrorove.archguard-checkpoint-result/1.0', ...result, status, rollback,
112
+ exitCode: status === 'blocked' ? 2 : 0, evidenceState: receipt ? 'signed' : 'reported', receipt,
113
+ processLedgerDigest: record.digest, ledgerPath: path.join(ctx.directory, 'ledger.jsonl') };
114
+ }
115
+ export async function archguardCheckpoint(input) {
116
+ return withArchguardMutationFence(input, async assertPaths => {
117
+ const ctx = await context(input);
118
+ const snapshot = await loadSnapshot(ctx, input.snapshotId);
119
+ await assertPaths(snapshot.paths);
120
+ return withExclusiveLock(path.join(ctx.repositoryState, 'mutation.lock'), async () =>
121
+ withExclusiveLock(path.join(ctx.directory, 'mutation.lock'), async () => checkpointLocked(ctx, input)));
122
+ });
123
+ }