genesis-compiler 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +443 -0
  3. package/bin/genesis.js +15 -0
  4. package/docs/assurance-model.md +26 -0
  5. package/docs/prompt-integration.md +98 -0
  6. package/docs/stack-components.md +304 -0
  7. package/package.json +57 -7
  8. package/plugins/genesis/.codex-plugin/plugin.json +19 -0
  9. package/plugins/genesis/hooks.json +18 -0
  10. package/prompts/blueprint.txt +9 -0
  11. package/prompts/describe.txt +17 -0
  12. package/prompts/deslop.txt +14 -0
  13. package/prompts/program.txt +12 -0
  14. package/prompts/reconcile.txt +12 -0
  15. package/prompts/review.txt +12 -0
  16. package/prompts/start.txt +30 -0
  17. package/prompts/work.txt +28 -0
  18. package/skills/genesis-deslop/SKILL.md +36 -0
  19. package/skills/genesis-deslop/agents/openai.yaml +4 -0
  20. package/skills/genesis-program/SKILL.md +66 -0
  21. package/skills/genesis-program/agents/openai.yaml +4 -0
  22. package/skills/genesis-project/SKILL.md +53 -0
  23. package/skills/genesis-project/agents/openai.yaml +4 -0
  24. package/src/cli.js +276 -0
  25. package/src/index/agent-skills.js +425 -0
  26. package/src/index/assets.js +19 -0
  27. package/src/index/blueprint.js +38 -0
  28. package/src/index/check.js +102 -0
  29. package/src/index/code-index.js +283 -0
  30. package/src/index/code-indexers/ast-grep.js +414 -0
  31. package/src/index/codex-hooks.js +367 -0
  32. package/src/index/codex-plugin.js +73 -0
  33. package/src/index/context.js +137 -0
  34. package/src/index/environment-files.js +19 -0
  35. package/src/index/errors.js +26 -0
  36. package/src/index/git.js +26 -0
  37. package/src/index/init.js +48 -0
  38. package/src/index/launch.js +34 -0
  39. package/src/index/paths.js +10 -0
  40. package/src/index/process.js +89 -0
  41. package/src/index/program.js +181 -0
  42. package/src/index/project-files.js +24 -0
  43. package/src/index/project-state.js +87 -0
  44. package/src/index/prompt.js +347 -0
  45. package/src/index/stack-catalog.js +72 -0
  46. package/src/index/stack-command.js +65 -0
  47. package/src/index/stack-composition.js +38 -0
  48. package/src/index/stack-environment-files.js +83 -0
  49. package/src/index/stack-launch.js +428 -0
  50. package/src/index/stack-piece.js +283 -0
  51. package/src/index/stack-preflight.js +25 -0
  52. package/src/index/stack-process.js +25 -0
  53. package/src/index/stack-workspace-setup.js +129 -0
  54. package/src/index/stack.js +302 -0
  55. package/src/index/utils.js +85 -0
  56. package/src/index/verification.js +77 -0
  57. package/src/index/workspace-setup.js +55 -0
  58. package/src/index.js +102 -0
  59. package/stacks/pieces/cpp.md +22 -0
  60. package/stacks/pieces/csharp.md +22 -0
  61. package/stacks/pieces/go.md +22 -0
  62. package/stacks/pieces/java.md +22 -0
  63. package/stacks/pieces/jskit-mysql.md +37 -0
  64. package/stacks/pieces/jskit.md +70 -0
  65. package/stacks/pieces/kotlin.md +22 -0
  66. package/stacks/pieces/mysql.md +18 -0
  67. package/stacks/pieces/nodejs.md +25 -0
  68. package/stacks/pieces/php.md +23 -0
  69. package/stacks/pieces/python.md +23 -0
  70. package/stacks/pieces/ruby.md +22 -0
  71. package/stacks/pieces/rust.md +22 -0
  72. package/stacks/pieces/shell.md +23 -0
  73. package/stacks/pieces/vue.md +19 -0
@@ -0,0 +1,48 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { syncProjectSkills } from './agent-skills.js';
5
+ import { BLUEPRINT_SKELETON_SOURCE } from './blueprint.js';
6
+ import { installCodexHooks } from './codex-hooks.js';
7
+ import { gitContext } from './git.js';
8
+ import { BLUEPRINT_PATH, PROGRAM_ROOT, STACK_PATH } from './paths.js';
9
+ import { EMPTY_STACK_SOURCE, readStack } from './stack.js';
10
+
11
+ async function createIfMissing(projectRoot, relativePath, source) {
12
+ const location = path.join(projectRoot, relativePath);
13
+ await mkdir(path.dirname(location), { recursive: true });
14
+ try {
15
+ await writeFile(location, source, { flag: 'wx', mode: 0o644 });
16
+ return relativePath;
17
+ } catch (error) {
18
+ if (error?.code === 'EEXIST') return null;
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ export async function initializeProject({ projectRoot } = {}) {
24
+ const root = (await gitContext(projectRoot)).repositoryRoot;
25
+ const created = (await Promise.all([
26
+ createIfMissing(root, BLUEPRINT_PATH, BLUEPRINT_SKELETON_SOURCE),
27
+ createIfMissing(root, STACK_PATH, EMPTY_STACK_SOURCE),
28
+ ])).filter(Boolean);
29
+ await mkdir(path.join(root, PROGRAM_ROOT), { recursive: true });
30
+ const stack = await readStack(root);
31
+ const skills = await syncProjectSkills({ projectRoot: root, stack });
32
+ const hooks = await installCodexHooks({ projectRoot: root });
33
+ const changedFiles = [...created, ...hooks.changedFiles, ...skills.changedFiles].sort();
34
+ return {
35
+ status: changedFiles.length > 0 ? 'updated' : 'unchanged',
36
+ summary: changedFiles.length > 0
37
+ ? 'Initialized Genesis, Agent Skills, and project Codex hooks.'
38
+ : 'Genesis, its Agent Skills, and project Codex hooks are already initialized.',
39
+ changedFiles,
40
+ diagnostics: skills.diagnostics,
41
+ guidance: [
42
+ 'Open Codex and use /hooks to review and trust the project hooks.',
43
+ 'Genesis workflow skills are available in .agents/skills/.',
44
+ 'Describe product intent in genesis/blueprint.md.',
45
+ 'Stack components are optional; add them with genesis stack add <piece...>.',
46
+ ].join(' '),
47
+ };
48
+ }
@@ -0,0 +1,34 @@
1
+ import { gitContext } from './git.js';
2
+ import { missingStackResources } from './stack-preflight.js';
3
+ import { readStack } from './stack.js';
4
+ import { uniqueSorted } from './utils.js';
5
+
6
+ /** Read the Stack's launch declaration without choosing or starting a runtime. */
7
+ export async function inspectProjectLaunch({
8
+ environment = process.env,
9
+ projectRoot,
10
+ } = {}) {
11
+ const root = (await gitContext(projectRoot)).repositoryRoot;
12
+ const stack = await readStack(root);
13
+ const diagnostics = missingStackResources({ environment, resources: stack.resources });
14
+ const disabledReason = diagnostics.length === 0
15
+ ? null
16
+ : diagnostics.map(({ message }) => message).join(' ');
17
+ const targets = stack.launchTargets.map((target) => ({
18
+ ...target,
19
+ available: diagnostics.length === 0,
20
+ disabledReason,
21
+ }));
22
+ let status = 'ready';
23
+ if (targets.length === 0) status = 'unconfigured';
24
+ else if (diagnostics.length > 0) status = 'blocked';
25
+ return {
26
+ status,
27
+ stackHash: stack.identityHash,
28
+ components: stack.components.map(({ id }) => id),
29
+ runtimeRequirements: uniqueSorted(targets.flatMap((target) => target.runtimeRequirements)),
30
+ resources: stack.resources,
31
+ targets,
32
+ diagnostics,
33
+ };
34
+ }
@@ -0,0 +1,10 @@
1
+ export const BLUEPRINT_PATH = 'genesis/blueprint.md';
2
+ export const STACK_PATH = 'genesis/stack.md';
3
+ export const PROGRAM_ROOT = 'genesis/program';
4
+ export const VERIFICATION_PATH = '.genesis/verification.json';
5
+
6
+ export function isProjectContentPath(file) {
7
+ const first = file.split('/')[0];
8
+ if (file === '.agents/skills' || file.startsWith('.agents/skills/')) return false;
9
+ return !['.codex', '.genesis', 'genesis'].includes(first);
10
+ }
@@ -0,0 +1,89 @@
1
+ import { execFile } from 'node:child_process';
2
+
3
+ import { GenesisError } from './errors.js';
4
+
5
+ const MAX_DIAGNOSTIC_OUTPUT = 16_384;
6
+
7
+ function executeFile(command, args, options) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = execFile(command, args, options, (error, stdout, stderr) => {
10
+ if (error) {
11
+ reject(error);
12
+ return;
13
+ }
14
+ resolve({ stdout, stderr });
15
+ });
16
+ child.stdin?.end();
17
+ });
18
+ }
19
+
20
+ function boundedDiagnosticOutput(buffer) {
21
+ const source = buffer.toString('utf8');
22
+ if (source.length <= MAX_DIAGNOSTIC_OUTPUT) return source;
23
+ const marker = '\n... diagnostic output omitted ...\n';
24
+ const available = MAX_DIAGNOSTIC_OUTPUT - marker.length;
25
+ const head = Math.floor(available / 2);
26
+ return `${source.slice(0, head)}${marker}${source.slice(-(available - head))}`;
27
+ }
28
+
29
+ function cleanGitEnvironment(overrides = {}) {
30
+ const environment = {};
31
+ for (const [key, value] of Object.entries(process.env)) {
32
+ if (!key.startsWith('GIT_')) environment[key] = value;
33
+ }
34
+ return { ...environment, ...overrides };
35
+ }
36
+
37
+ export async function runProcess(command, args, {
38
+ cwd,
39
+ env = process.env,
40
+ maxBytes = 32 * 1024 * 1024,
41
+ code = 'PROCESS_EXEC_FAILED',
42
+ } = {}) {
43
+ try {
44
+ const { stdout, stderr } = await executeFile(command, args, {
45
+ cwd,
46
+ env,
47
+ encoding: 'buffer',
48
+ maxBuffer: maxBytes,
49
+ shell: false,
50
+ windowsHide: true,
51
+ });
52
+ return { status: 0, signal: null, stdout, stderr };
53
+ } catch (error) {
54
+ const stdout = Buffer.isBuffer(error.stdout) ? error.stdout : Buffer.from(error.stdout || '');
55
+ const stderr = Buffer.isBuffer(error.stderr) ? error.stderr : Buffer.from(error.stderr || '');
56
+ throw new GenesisError(code, `${command} failed: ${error.message}`, {
57
+ command,
58
+ args,
59
+ status: typeof error.code === 'number' ? error.code : null,
60
+ signal: error.signal || null,
61
+ cause: typeof error.code === 'string' ? error.code : undefined,
62
+ stdout: boundedDiagnosticOutput(stdout),
63
+ stderr: boundedDiagnosticOutput(stderr),
64
+ });
65
+ }
66
+ }
67
+
68
+ export async function runGit(cwd, args, options = {}) {
69
+ return runProcess('git', [
70
+ '-c', 'core.hooksPath=/dev/null',
71
+ '-c', 'commit.gpgSign=false',
72
+ '-c', 'core.fsmonitor=false',
73
+ ...args,
74
+ ], {
75
+ cwd,
76
+ env: cleanGitEnvironment({
77
+ GIT_CONFIG_NOSYSTEM: '1',
78
+ GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
79
+ ...options.env,
80
+ }),
81
+ maxBytes: options.maxBytes,
82
+ code: options.code || 'GIT_EXEC_FAILED',
83
+ });
84
+ }
85
+
86
+ export async function runGitText(cwd, args, options = {}) {
87
+ const result = await runGit(cwd, args, options);
88
+ return result.stdout.toString('utf8').trimEnd();
89
+ }
@@ -0,0 +1,181 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { GenesisError } from './errors.js';
5
+ import { isProjectContentPath, PROGRAM_ROOT } from './paths.js';
6
+ import { normalizeRelative } from './utils.js';
7
+
8
+ const SOURCE_LINE = /^- `([^`]+)`$/u;
9
+ const CONCEPT_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
+
11
+ async function markdownFiles(projectRoot) {
12
+ const root = path.join(projectRoot, PROGRAM_ROOT);
13
+ const files = [];
14
+ let found = true;
15
+
16
+ async function visit(directory, relative = '') {
17
+ let entries;
18
+ try {
19
+ entries = await readdir(directory, { withFileTypes: true });
20
+ } catch (error) {
21
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code) && directory === root) {
22
+ found = false;
23
+ return;
24
+ }
25
+ throw error;
26
+ }
27
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
28
+ const child = path.join(directory, entry.name);
29
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
30
+ if (entry.isDirectory()) await visit(child, childRelative);
31
+ else if (entry.isFile() && entry.name.endsWith('.md')) files.push(`${PROGRAM_ROOT}/${childRelative}`);
32
+ else {
33
+ throw new GenesisError(
34
+ 'PROGRAM_INVALID',
35
+ `Program may contain only directories and Markdown modules: ${PROGRAM_ROOT}/${childRelative}.`,
36
+ );
37
+ }
38
+ }
39
+ }
40
+
41
+ await visit(root);
42
+ return { files, found };
43
+ }
44
+
45
+ async function sourceExists(projectRoot, sourcePath) {
46
+ try {
47
+ return (await stat(path.join(projectRoot, sourcePath))).isFile();
48
+ } catch (error) {
49
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return false;
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ function moduleIdentity(programPath) {
55
+ const relative = programPath.slice(`${PROGRAM_ROOT}/`.length, -3);
56
+ const normalized = normalizeRelative(relative);
57
+ const segments = normalized.split('/');
58
+ if (segments.length < 2 || segments.some((segment) => !CONCEPT_NAME.test(segment))) {
59
+ throw new GenesisError(
60
+ 'PROGRAM_INVALID',
61
+ `Program module needs lowercase conceptual subsystem and operation names: ${programPath}.`,
62
+ { path: programPath },
63
+ );
64
+ }
65
+ return {
66
+ name: segments.at(-1),
67
+ subsystem: segments.slice(0, -1).join('/'),
68
+ };
69
+ }
70
+
71
+ function moduleContents(source, programPath) {
72
+ const lines = source.replace(/\r\n?/gu, '\n').split('\n');
73
+ const title = lines.find((line) => /^#\s+\S/u.test(line))?.replace(/^#\s+/u, '').trim() || '';
74
+ const headings = lines
75
+ .map((line, index) => ({ line, index }))
76
+ .filter(({ line }) => /^##\s+/u.test(line));
77
+ const sourcesHeading = headings.filter(({ line }) => line === '## Sources');
78
+ const contractHeading = headings.filter(({ line }) => line === '## Public contract');
79
+ if (
80
+ sourcesHeading.length !== 1
81
+ || contractHeading.length !== 1
82
+ || sourcesHeading[0].index >= contractHeading[0].index
83
+ ) {
84
+ throw new GenesisError(
85
+ 'PROGRAM_INVALID',
86
+ `Program module needs one Sources section before one Public contract: ${programPath}.`,
87
+ { path: programPath },
88
+ );
89
+ }
90
+ const sourceLines = lines
91
+ .slice(sourcesHeading[0].index + 1, contractHeading[0].index)
92
+ .filter((line) => line.trim());
93
+ const sources = sourceLines.map((line) => line.match(SOURCE_LINE)?.[1]);
94
+ const nextHeading = headings.find(({ index }) => index > contractHeading[0].index);
95
+ const contractLines = lines
96
+ .slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length)
97
+ .filter((line) => line.trim());
98
+ if (
99
+ sources.length === 0
100
+ || sources.some((sourcePath) => sourcePath === undefined)
101
+ || new Set(sources).size !== sources.length
102
+ || contractLines.length === 0
103
+ ) {
104
+ throw new GenesisError(
105
+ 'PROGRAM_INVALID',
106
+ `Program module needs distinct backticked source bullets and a non-empty public contract: ${programPath}.`,
107
+ { path: programPath },
108
+ );
109
+ }
110
+ const implementationHeading = headings.filter(({ line }) => line === '## Implementation map');
111
+ if (implementationHeading.length > 1) {
112
+ throw new GenesisError(
113
+ 'PROGRAM_INVALID',
114
+ `Program module has duplicate Implementation maps: ${programPath}.`,
115
+ { path: programPath },
116
+ );
117
+ }
118
+ const description = lines
119
+ .slice(lines.findIndex((line) => /^#\s+\S/u.test(line)) + 1, sourcesHeading[0].index)
120
+ .join('\n')
121
+ .trim();
122
+ let implementationMap = '';
123
+ if (implementationHeading.length === 1) {
124
+ const heading = implementationHeading[0];
125
+ const following = headings.find(({ index }) => index > heading.index);
126
+ implementationMap = lines.slice(heading.index + 1, following?.index ?? lines.length).join('\n').trim();
127
+ }
128
+ return {
129
+ title,
130
+ description,
131
+ sources: sources.map((sourcePath) => normalizeRelative(sourcePath)),
132
+ publicContract: contractLines.join('\n').trim(),
133
+ implementationMap,
134
+ };
135
+ }
136
+
137
+ export async function inspectProgram(projectRoot) {
138
+ const { files, found } = await markdownFiles(projectRoot);
139
+ if (!found || files.length === 0) {
140
+ return { status: 'missing', files: [], modules: [], subsystems: [] };
141
+ }
142
+
143
+ const modules = [];
144
+ for (const programPath of files) {
145
+ const source = await readFile(path.join(projectRoot, programPath), 'utf8');
146
+ if ((source.match(/^#\s+\S.*$/gmu) || []).length !== 1) {
147
+ throw new GenesisError('PROGRAM_INVALID', `Program module has an invalid explanatory format: ${programPath}.`, {
148
+ path: programPath,
149
+ });
150
+ }
151
+ const identity = moduleIdentity(programPath);
152
+ const contents = moduleContents(source, programPath);
153
+ const { sources } = contents;
154
+ for (const sourcePath of sources) {
155
+ if (!sourcePath || !isProjectContentPath(sourcePath) || !await sourceExists(projectRoot, sourcePath)) {
156
+ throw new GenesisError(
157
+ 'PROGRAM_SOURCE_MISSING',
158
+ `Program module cites a missing or ineligible source file: ${sourcePath}.`,
159
+ { path: programPath, sourcePath },
160
+ );
161
+ }
162
+ }
163
+ modules.push({
164
+ path: programPath,
165
+ name: identity.name,
166
+ sources,
167
+ subsystem: identity.subsystem,
168
+ title: contents.title,
169
+ description: contents.description,
170
+ publicContract: contents.publicContract,
171
+ implementationMap: contents.implementationMap,
172
+ });
173
+ }
174
+
175
+ return {
176
+ status: 'valid',
177
+ files,
178
+ modules,
179
+ subsystems: [...new Set(modules.map(({ subsystem }) => subsystem))].sort(),
180
+ };
181
+ }
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+
3
+ import { runGit } from './process.js';
4
+ import { pathState } from './utils.js';
5
+
6
+ async function visiblePaths(projectRoot) {
7
+ const [visible, deleted] = await Promise.all([
8
+ runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.']),
9
+ runGit(projectRoot, ['ls-files', '-z', '--deleted', '--', '.']),
10
+ ]);
11
+ const absent = new Set(deleted.stdout.toString('utf8').split('\0').filter(Boolean));
12
+ return [...new Set(visible.stdout.toString('utf8').split('\0').filter(Boolean))]
13
+ .filter((file) => !absent.has(file) && file !== '.git' && !file.startsWith('.git/'))
14
+ .sort();
15
+ }
16
+
17
+ /** Hashable states for ordinary Git-visible files, excluding ignored output. */
18
+ export async function gitVisibleFileStates(projectRoot, { includePath = () => true } = {}) {
19
+ const states = new Map();
20
+ for (const file of (await visiblePaths(projectRoot)).filter(includePath)) {
21
+ states.set(file, await pathState(path.join(projectRoot, file)));
22
+ }
23
+ return states;
24
+ }
@@ -0,0 +1,87 @@
1
+ import { readFile, rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { isProjectContentPath, VERIFICATION_PATH } from './paths.js';
5
+ import { gitVisibleFileStates } from './project-files.js';
6
+ import { sha256, stableJson, writeFileAtomic } from './utils.js';
7
+
8
+ const SCHEMA_VERSION = 1;
9
+ const HASH = /^sha256:[0-9a-f]{64}$/u;
10
+
11
+ function commandRecords(stack) {
12
+ return stack.commands.map(({ label, argv }) => ({ label, argv }));
13
+ }
14
+
15
+ async function codebaseHash(projectRoot) {
16
+ const states = await gitVisibleFileStates(projectRoot, { includePath: isProjectContentPath });
17
+ return sha256(stableJson([...states].map(([file, state]) => ({
18
+ path: file,
19
+ hash: state.hash,
20
+ mode: state.mode,
21
+ }))));
22
+ }
23
+
24
+ async function readEvidence(projectRoot) {
25
+ try {
26
+ const value = JSON.parse(await readFile(path.join(projectRoot, VERIFICATION_PATH), 'utf8'));
27
+ const exactFields = value && typeof value === 'object'
28
+ && Object.keys(value).sort().join(',') === 'codeHash,commands,schemaVersion,stackHash';
29
+ const validCommands = Array.isArray(value?.commands) && value.commands.every((command) => (
30
+ command
31
+ && typeof command === 'object'
32
+ && Object.keys(command).sort().join(',') === 'argv,label'
33
+ && typeof command.label === 'string'
34
+ && command.label.length > 0
35
+ && Array.isArray(command.argv)
36
+ && command.argv.length > 0
37
+ && command.argv.every((argument) => typeof argument === 'string' && argument.length > 0)
38
+ ));
39
+ if (
40
+ !exactFields
41
+ || value.schemaVersion !== SCHEMA_VERSION
42
+ || !HASH.test(value.codeHash)
43
+ || !HASH.test(value.stackHash)
44
+ || !validCommands
45
+ ) return { status: 'invalid', evidence: null };
46
+ return { status: 'recorded', evidence: value };
47
+ } catch (error) {
48
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return { status: 'missing', evidence: null };
49
+ if (error instanceof SyntaxError) return { status: 'invalid', evidence: null };
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ export async function inspectVerification({ projectRoot, stack }) {
55
+ if (stack.commands.length === 0) return { status: 'unconfigured', evidence: null };
56
+ const saved = await readEvidence(projectRoot);
57
+ if (saved.status !== 'recorded') return saved;
58
+ const expected = {
59
+ codeHash: await codebaseHash(projectRoot),
60
+ stackHash: stack.identityHash,
61
+ commands: commandRecords(stack),
62
+ };
63
+ return {
64
+ status: stableJson({
65
+ codeHash: saved.evidence.codeHash,
66
+ stackHash: saved.evidence.stackHash,
67
+ commands: saved.evidence.commands,
68
+ }) === stableJson(expected) ? 'current' : 'stale',
69
+ evidence: saved.evidence,
70
+ };
71
+ }
72
+
73
+ export async function clearVerification(projectRoot) {
74
+ await rm(path.join(projectRoot, VERIFICATION_PATH), { force: true });
75
+ }
76
+
77
+ export async function writeVerification({ projectRoot, stack }) {
78
+ const location = path.join(projectRoot, VERIFICATION_PATH);
79
+ const evidence = {
80
+ schemaVersion: SCHEMA_VERSION,
81
+ codeHash: await codebaseHash(projectRoot),
82
+ stackHash: stack.identityHash,
83
+ commands: commandRecords(stack),
84
+ };
85
+ await writeFileAtomic(location, stableJson(evidence));
86
+ return evidence;
87
+ }