genesis-compiler 1.2.26 → 1.2.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.
@@ -10,6 +10,7 @@ import {
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
  const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/u;
13
+ const FIRST_PARTY_STACK_PACKAGE = 'genesis-stack';
13
14
 
14
15
  export function normalizeStackPackageName(value) {
15
16
  const name = String(value ?? '').trim();
@@ -50,6 +51,16 @@ async function packageDirectory(packageName, projectRoot) {
50
51
  );
51
52
  }
52
53
 
54
+ export async function installedFirstPartyStackPackages({ projectRoot } = {}) {
55
+ try {
56
+ await packageDirectory(FIRST_PARTY_STACK_PACKAGE, projectRoot);
57
+ return [FIRST_PARTY_STACK_PACKAGE];
58
+ } catch (error) {
59
+ if (error?.code === 'STACK_PACKAGE_UNAVAILABLE') return [];
60
+ throw error;
61
+ }
62
+ }
63
+
53
64
  async function readPieceDirectory(
54
65
  directory,
55
66
  sourcePrefix,
@@ -0,0 +1,205 @@
1
+ import { GenesisError } from './errors.js';
2
+ import { composeStackEnvironmentDefaults } from './stack-environment-defaults.js';
3
+ import { composeStackEnvironmentFiles } from './stack-environment-files.js';
4
+ import {
5
+ composeStackSections,
6
+ opaqueStackSections,
7
+ trimStackSectionLines,
8
+ } from './stack-section.js';
9
+
10
+ const PROJECT_CONTRACT_NAMES = [
11
+ 'Resources',
12
+ 'Environment defaults',
13
+ 'Environment files',
14
+ 'Verification',
15
+ ];
16
+
17
+ function distinctVerificationCommands(components) {
18
+ const seen = new Set();
19
+ return components.flatMap(({ verificationCommands }) => verificationCommands)
20
+ .filter(({ label, argv }) => {
21
+ const key = JSON.stringify([label, argv]);
22
+ if (seen.has(key)) return false;
23
+ seen.add(key);
24
+ return true;
25
+ });
26
+ }
27
+
28
+ function projectResource(resource) {
29
+ return {
30
+ id: resource.id,
31
+ kind: resource.kind,
32
+ environmentAlternatives: resource.environmentAlternatives.map((alternative) => ({
33
+ bindings: alternative.bindings,
34
+ ...(alternative.allowEmpty.length > 0 ? { allowEmpty: alternative.allowEmpty } : {}),
35
+ ...(alternative.preferred ? { preferred: true } : {}),
36
+ })),
37
+ ...(Object.keys(resource.optionalBindings).length > 0
38
+ ? { optionalBindings: resource.optionalBindings }
39
+ : {}),
40
+ };
41
+ }
42
+
43
+ function resourceLines(components) {
44
+ const resources = components.flatMap(({ resources }) => resources);
45
+ if (resources.length === 0) return null;
46
+ return resources.flatMap((resource, index) => [
47
+ ...(index > 0 ? [''] : []),
48
+ '```json genesis-resource',
49
+ JSON.stringify(projectResource(resource), null, 2),
50
+ '```',
51
+ ]);
52
+ }
53
+
54
+ function environmentDefaultLines(components) {
55
+ const defaults = composeStackEnvironmentDefaults(components);
56
+ if (defaults.length === 0) return null;
57
+ return defaults.map(({ name, value }) => `- Default \`${name}\`: \`${value}\``);
58
+ }
59
+
60
+ function environmentFileLines(components) {
61
+ const files = composeStackEnvironmentFiles(components, null);
62
+ if (files.length === 0) return null;
63
+ return files.map(({ path }) => `- Dotenv \`${path}\``);
64
+ }
65
+
66
+ function verificationLines(components) {
67
+ const commands = distinctVerificationCommands(components);
68
+ if (commands.length === 0) return null;
69
+ return commands.map(({ label, argv }) => (
70
+ `- Verify \`${label}\`: ${argv.map((value) => `\`${value}\``).join(' ')}`
71
+ ));
72
+ }
73
+
74
+ function componentGenesisContracts(components, sections) {
75
+ return new Map([
76
+ ['Resources', sections.has('Resources') ? null : resourceLines(components)],
77
+ ['Environment defaults', sections.has('Environment defaults')
78
+ ? null
79
+ : environmentDefaultLines(components)],
80
+ ['Environment files', sections.has('Environment files')
81
+ ? null
82
+ : environmentFileLines(components)],
83
+ ['Verification', sections.has('Verification') ? null : verificationLines(components)],
84
+ ].filter(([, lines]) => lines !== null));
85
+ }
86
+
87
+ export function stackPieceContractNames(piece) {
88
+ return [
89
+ ...(piece.resources.length > 0 ? ['Resources'] : []),
90
+ ...(piece.environmentDefaults.length > 0 ? ['Environment defaults'] : []),
91
+ ...(piece.environmentFiles.length > 0 ? ['Environment files'] : []),
92
+ ...(piece.verificationCommands.length > 0 ? ['Verification'] : []),
93
+ ...piece.extensions.map(({ name }) => name),
94
+ ];
95
+ }
96
+
97
+ function contractSections({ components, knownSectionNames, sections }) {
98
+ const genesisContracts = componentGenesisContracts(components, sections);
99
+ const materializedSections = [];
100
+ const projectLines = new Map();
101
+
102
+ for (const name of PROJECT_CONTRACT_NAMES) {
103
+ if (sections.has(name)) {
104
+ projectLines.set(name, trimStackSectionLines(sections.get(name)));
105
+ } else if (genesisContracts.has(name)) {
106
+ projectLines.set(name, genesisContracts.get(name));
107
+ materializedSections.push(name);
108
+ }
109
+ }
110
+
111
+ const extensions = composeStackSections(
112
+ components,
113
+ opaqueStackSections(sections, knownSectionNames),
114
+ );
115
+ for (const extension of extensions) {
116
+ const [diagnostic] = extension.diagnostics;
117
+ if (diagnostic) {
118
+ throw new GenesisError(diagnostic.code, diagnostic.message, diagnostic.details);
119
+ }
120
+ projectLines.set(extension.name, trimStackSectionLines(extension.lines));
121
+ if (!sections.has(extension.name)) materializedSections.push(extension.name);
122
+ }
123
+
124
+ return {
125
+ environmentDefaultLines: projectLines.get('Environment defaults') ?? null,
126
+ environmentFileLines: projectLines.get('Environment files') ?? null,
127
+ extensionSections: [...projectLines.entries()]
128
+ .filter(([name]) => !PROJECT_CONTRACT_NAMES.includes(name))
129
+ .map(([name, lines]) => ({ name, lines })),
130
+ materializedSections,
131
+ projectContracts: [...projectLines].map(([name, lines]) => ({ name, lines })),
132
+ resourceLines: projectLines.get('Resources') ?? null,
133
+ verificationLines: projectLines.get('Verification') ?? null,
134
+ };
135
+ }
136
+
137
+ export function materializeStackProjectContracts(options) {
138
+ return contractSections(options);
139
+ }
140
+
141
+ export function requireMaterializedStackProjectContracts(options) {
142
+ const result = contractSections(options);
143
+ if (result.materializedSections.length > 0) {
144
+ const headings = result.materializedSections.map((name) => `\`## ${name}\``);
145
+ throw new GenesisError(
146
+ 'STACK_PROJECT_CONTRACTS_INCOMPLETE',
147
+ `Selected Stack components require project-owned contracts missing from genesis/stack.md: ${headings.join(', ')}. Run \`genesis migrate\` for an older project or re-run the confirmed \`genesis stack add\` operation for a current project.`,
148
+ { sections: result.materializedSections },
149
+ );
150
+ }
151
+ return result;
152
+ }
153
+
154
+ function contractBlocks(projectContracts) {
155
+ if (projectContracts.length === 0) {
156
+ return [
157
+ 'No operation contract was proposed by these components. Inspect the application you are creating and declare only its real setup, verification, output, and other consumer operations before calling it ready.',
158
+ ];
159
+ }
160
+ return projectContracts.flatMap(({ name, lines }) => [
161
+ `### \`## ${name}\``,
162
+ '',
163
+ '~~~markdown',
164
+ ...lines,
165
+ '~~~',
166
+ '',
167
+ ]);
168
+ }
169
+
170
+ export function renderStackPreparationPrompt({
171
+ components,
172
+ materializedSections = [],
173
+ newlySelected = [],
174
+ projectContracts = [],
175
+ reviewSections = [],
176
+ } = {}) {
177
+ if (newlySelected.length === 0 && materializedSections.length === 0) return '';
178
+ const selected = components.map(({ id }) => `\`${id}\``).join(', ');
179
+ return [
180
+ '# Prepare the selected Stack',
181
+ '',
182
+ `Genesis selected ${selected} and made its concrete operation proposals project-owned in \`genesis/stack.md\`. That file is now the durable contract for this application; future catalog releases must not silently change it.`,
183
+ ...(materializedSections.length > 0 ? [
184
+ '',
185
+ `Materialized contract headings: ${materializedSections.map((name) => `\`## ${name}\``).join(', ')}.`,
186
+ ] : []),
187
+ ...(reviewSections.length > 0 ? [
188
+ '',
189
+ `These existing project contracts remained authoritative while the new components were added: ${reviewSections.map((name) => `\`## ${name}\``).join(', ')}. Reconcile them against the newly selected technology using source evidence; Genesis deliberately did not overwrite them.`,
190
+ ] : []),
191
+ '',
192
+ 'Continue this same task now:',
193
+ '',
194
+ '1. Read `genesis/blueprint.md`, `genesis/engineering.md`, and the complete `genesis/stack.md`, then run `genesis context .` (or the relevant source paths).',
195
+ '2. Load every applicable installed Agent Skill and follow the selected technology’s authoritative guidance before creating dependencies or source.',
196
+ '3. For a new project, implement the source and commands so every project contract below is true. For existing source, inspect reality first and change a complete project section when the inherited proposal is not already true; do not add compatibility code merely to imitate a proposal.',
197
+ '4. Keep each consumer-owned section opaque to Genesis. Validate it with the consumer or framework that owns its grammar and behavior.',
198
+ '5. Establish or update the non-technical Blueprint and affected Program explanations as the product becomes concrete.',
199
+ '6. Run `genesis check`, resolve structural or incomplete-contract diagnostics, and run `genesis verify` only after the declared verification commands exist. Passing verification is evidence for those commands, not proof of every consumer operation.',
200
+ '',
201
+ '## Project contracts to satisfy',
202
+ '',
203
+ ...contractBlocks(projectContracts),
204
+ ].join('\n').trimEnd();
205
+ }
@@ -31,8 +31,8 @@ export function opaqueStackSections(sections, knownNames) {
31
31
 
32
32
  /**
33
33
  * Compose consumer-owned Stack sections without interpreting their contents.
34
- * A project declaration replaces component defaults; otherwise exactly one
35
- * selected component may provide a section with a given name.
34
+ * An existing project declaration wins; otherwise exactly one selected
35
+ * component may propose a section for materialization under a given name.
36
36
  */
37
37
  export function composeStackSections(components = [], projectSections = []) {
38
38
  const projectByName = new Map(projectSections.map((section) => [section.name, section]));
@@ -10,18 +10,21 @@ import {
10
10
  import { composeStackCityPresentation } from './stack-city-presentation.js';
11
11
  import { resolveStackPieces } from './stack-composition.js';
12
12
  import {
13
- composeStackEnvironmentDefaults,
14
13
  parseStackEnvironmentDefaultLines,
15
14
  } from './stack-environment-defaults.js';
16
15
  import {
17
- composeStackEnvironmentFiles,
18
16
  parseStackEnvironmentFileLines,
19
17
  } from './stack-environment-files.js';
20
18
  import {
21
- composeStackSections,
22
19
  opaqueStackSections,
23
20
  trimStackSectionLines,
24
21
  } from './stack-section.js';
22
+ import {
23
+ materializeStackProjectContracts,
24
+ renderStackPreparationPrompt,
25
+ requireMaterializedStackProjectContracts,
26
+ stackPieceContractNames,
27
+ } from './stack-project-contracts.js';
25
28
  import { parseStackVerificationLines } from './stack-verification.js';
26
29
  import {
27
30
  applyStackPieceCustomization,
@@ -129,7 +132,7 @@ function renderStack({
129
132
  resourceLines = null,
130
133
  extensionSections = [],
131
134
  stackPackages = [],
132
- verificationLines = [],
135
+ verificationLines = null,
133
136
  }) {
134
137
  return [
135
138
  '# Stack',
@@ -148,7 +151,9 @@ function renderStack({
148
151
  ...(environmentFileLines === null
149
152
  ? []
150
153
  : ['', '## Environment files', '', ...trimStackSectionLines(environmentFileLines)]),
151
- ...(verificationLines.length > 0 ? ['', '## Verification', ...verificationLines] : []),
154
+ ...(verificationLines === null
155
+ ? []
156
+ : ['', '## Verification', '', ...trimStackSectionLines(verificationLines)]),
152
157
  ...extensionSections.flatMap(({ name, lines }) => (
153
158
  ['', `## ${name}`, '', ...trimStackSectionLines(lines)]
154
159
  )),
@@ -185,46 +190,48 @@ export async function addStackPieces({ pieces, projectRoot, stackPackages = [] }
185
190
  .map((piece) => customizePiece(projectRoot, piece)),
186
191
  );
187
192
  const selected = selectedPieces.map(({ id }) => id);
193
+ const newlySelectedPieces = selectedPieces.filter(({ id }) => !existing.includes(id));
188
194
  const selectedPackages = [...new Set([
189
195
  ...declaredPackages,
190
196
  ...selectedPieces.map(({ stackPackage }) => stackPackage).filter(Boolean),
191
197
  ])].sort();
192
- const verificationLines = parseStackVerificationLines(
193
- sections.get('Verification') || [],
198
+ const projectContracts = materializeStackProjectContracts({
199
+ components: selectedPieces,
200
+ knownSectionNames: STACK_SECTIONS,
201
+ sections,
202
+ });
203
+ const verificationLines = projectContracts.verificationLines === null ? null : parseStackVerificationLines(
204
+ projectContracts.verificationLines,
194
205
  { path: STACK_PATH },
195
206
  )
196
207
  .map(({ label, argv }) => `- Verify \`${label}\`: ${argv.map((value) => `\`${value}\``).join(' ')}`);
197
- const resourceLines = sections.has('Resources') ? sections.get('Resources') : null;
208
+ const resourceLines = projectContracts.resourceLines;
198
209
  parseStackResourceLines(resourceLines === null ? undefined : resourceLines, { path: STACK_PATH });
199
- const environmentDefaultLines = sections.has('Environment defaults')
200
- ? sections.get('Environment defaults')
201
- : null;
210
+ const environmentDefaultLines = projectContracts.environmentDefaultLines;
202
211
  parseStackEnvironmentDefaultLines(
203
212
  environmentDefaultLines === null ? undefined : environmentDefaultLines,
204
213
  { path: STACK_PATH },
205
214
  );
206
- const environmentFileLines = sections.has('Environment files')
207
- ? sections.get('Environment files')
208
- : null;
215
+ const environmentFileLines = projectContracts.environmentFileLines;
209
216
  parseStackEnvironmentFileLines(
210
217
  environmentFileLines === null ? undefined : environmentFileLines,
211
218
  { path: STACK_PATH },
212
219
  );
213
- const extensionSections = opaqueStackSections(sections, STACK_SECTIONS);
214
220
  const rendered = renderStack({
215
221
  componentIds: selected,
216
222
  environmentDefaultLines,
217
223
  environmentFileLines,
218
224
  resourceLines,
219
- extensionSections,
225
+ extensionSections: projectContracts.extensionSections,
220
226
  stackPackages: selectedPackages,
221
227
  verificationLines,
222
228
  });
223
229
  const stackChanged = rendered !== source;
224
230
  if (stackChanged) await writeFileAtomic(location, rendered);
231
+ const stack = await readStack(projectRoot, { stackPackages: availablePackages });
225
232
  const skills = await syncProjectSkills({
226
233
  projectRoot,
227
- stack: await readStack(projectRoot, { stackPackages: availablePackages }),
234
+ stack,
228
235
  });
229
236
  const changedFiles = [
230
237
  ...(stackChanged ? [STACK_PATH] : []),
@@ -238,24 +245,28 @@ export async function addStackPieces({ pieces, projectRoot, stackPackages = [] }
238
245
  components: selected,
239
246
  changedFiles,
240
247
  diagnostics: skills.diagnostics,
248
+ prompt: renderStackPreparationPrompt({
249
+ components: stack.components,
250
+ materializedSections: projectContracts.materializedSections,
251
+ newlySelected: newlySelectedPieces.map(({ id }) => id),
252
+ projectContracts: stack.projectContracts,
253
+ reviewSections: [...new Set(newlySelectedPieces.flatMap(stackPieceContractNames))]
254
+ .filter((name) => sections.has(name)),
255
+ }),
241
256
  };
242
257
  }
243
258
 
244
- function distinctVerificationCommands(commands) {
245
- const seen = new Set();
246
- return commands.filter((command) => {
247
- const key = JSON.stringify([command.label, command.argv]);
248
- if (seen.has(key)) return false;
249
- seen.add(key);
250
- return true;
251
- });
252
- }
253
-
254
- function resourceDeclarations(components) {
255
- return components.flatMap((piece) => piece.resources.map((resource) => ({
256
- component: piece.id,
257
- resource,
258
- })));
259
+ export async function materializeProjectStackContracts({ projectRoot, stackPackages = [] }) {
260
+ const source = await readFile(path.join(projectRoot, STACK_PATH), 'utf8');
261
+ const selected = componentIds(parseSections(source));
262
+ if (selected.length === 0) {
263
+ return { changedFiles: [], diagnostics: [], materializedSections: [] };
264
+ }
265
+ const result = await addStackPieces({ pieces: selected, projectRoot, stackPackages });
266
+ return {
267
+ changedFiles: result.changedFiles,
268
+ diagnostics: result.diagnostics,
269
+ };
259
270
  }
260
271
 
261
272
  function composedProse(components, field) {
@@ -280,7 +291,10 @@ async function customizePiece(projectRoot, piece) {
280
291
  }));
281
292
  }
282
293
 
283
- export async function readStack(projectRoot, { stackPackages = [] } = {}) {
294
+ async function readStackWithContractMode(projectRoot, {
295
+ allowComponentContracts,
296
+ stackPackages = [],
297
+ }) {
284
298
  const location = path.join(projectRoot, STACK_PATH);
285
299
  let source;
286
300
  try {
@@ -305,39 +319,45 @@ export async function readStack(projectRoot, { stackPackages = [] } = {}) {
305
319
  catalog,
306
320
  requested: componentIds(sections),
307
321
  })).map((piece) => customizePiece(projectRoot, piece)));
322
+ const projectContracts = (allowComponentContracts
323
+ ? materializeStackProjectContracts
324
+ : requireMaterializedStackProjectContracts)({
325
+ components,
326
+ knownSectionNames: STACK_SECTIONS,
327
+ sections,
328
+ });
308
329
  const projectVerificationCommands = parseStackVerificationLines(
309
- sections.get('Verification') || [],
330
+ projectContracts.verificationLines || [],
310
331
  { path: STACK_PATH },
311
332
  ).map(({ line: _line, ...command }) => command);
312
- const componentVerificationCommands = components.flatMap((piece) => piece.verificationCommands);
313
- const verificationCommands = distinctVerificationCommands(
314
- projectVerificationCommands.length > 0
315
- ? projectVerificationCommands
316
- : componentVerificationCommands,
317
- );
333
+ const verificationCommands = projectVerificationCommands;
318
334
  const cityPresentation = composeStackCityPresentation(components);
319
- const projectResources = sections.has('Resources')
320
- ? parseStackResourceLines(sections.get('Resources'), { path: STACK_PATH })
321
- : null;
322
- const resources = projectResources === null
323
- ? resourceDeclarations(components)
324
- : projectResources.map((resource) => ({ component: 'project', resource }));
335
+ const resources = parseStackResourceLines(
336
+ projectContracts.resourceLines === null ? undefined : projectContracts.resourceLines,
337
+ { path: STACK_PATH },
338
+ ).map((resource) => ({ component: 'project', resource }));
325
339
  const projectEnvironmentDefaults = parseStackEnvironmentDefaultLines(
326
- sections.has('Environment defaults') ? sections.get('Environment defaults') : undefined,
340
+ projectContracts.environmentDefaultLines === null
341
+ ? undefined
342
+ : projectContracts.environmentDefaultLines,
327
343
  { path: STACK_PATH },
328
344
  );
329
- const environmentDefaults = sections.has('Environment defaults')
330
- ? projectEnvironmentDefaults.map((entry) => ({ ...entry, sources: ['project'] }))
331
- : composeStackEnvironmentDefaults(components);
345
+ const environmentDefaults = projectEnvironmentDefaults
346
+ .map((entry) => ({ ...entry, sources: ['project'] }));
332
347
  const projectEnvironmentFiles = parseStackEnvironmentFileLines(
333
- sections.has('Environment files') ? sections.get('Environment files') : undefined,
348
+ projectContracts.environmentFileLines === null
349
+ ? undefined
350
+ : projectContracts.environmentFileLines,
334
351
  { path: STACK_PATH },
335
352
  );
336
- const environmentFiles = composeStackEnvironmentFiles(components, projectEnvironmentFiles);
337
- const extensions = composeStackSections(
338
- components,
339
- opaqueStackSections(sections, STACK_SECTIONS),
340
- );
353
+ const environmentFiles = (projectEnvironmentFiles || [])
354
+ .map((file) => ({ ...file, source: 'project' }));
355
+ const extensions = projectContracts.extensionSections.map(({ name, lines }) => ({
356
+ name,
357
+ lines,
358
+ source: 'project',
359
+ diagnostics: [],
360
+ }));
341
361
  return {
342
362
  path: STACK_PATH,
343
363
  identityHash: sha256(stableJson({
@@ -359,6 +379,7 @@ export async function readStack(projectRoot, { stackPackages = [] } = {}) {
359
379
  environmentDefaults,
360
380
  environmentFiles,
361
381
  extensions,
382
+ projectContracts: projectContracts.projectContracts,
362
383
  resources,
363
384
  guidance: composedProse(components, 'guidance'),
364
385
  adoption: composedProse(components, 'adoption'),
@@ -367,9 +388,22 @@ export async function readStack(projectRoot, { stackPackages = [] } = {}) {
367
388
  };
368
389
  }
369
390
 
391
+ export function readStack(projectRoot, { stackPackages = [] } = {}) {
392
+ return readStackWithContractMode(projectRoot, {
393
+ allowComponentContracts: false,
394
+ stackPackages,
395
+ });
396
+ }
397
+
398
+ export function readLegacyStack(projectRoot, { stackPackages = [] } = {}) {
399
+ return readStackWithContractMode(projectRoot, {
400
+ allowComponentContracts: true,
401
+ stackPackages,
402
+ });
403
+ }
404
+
370
405
  export function stackPromptContext(stack) {
371
406
  return {
372
- stackPackages: stack.stackPackages,
373
407
  components: stack.components.map((piece) => ({
374
408
  id: piece.id,
375
409
  description: piece.description,