genesis-compiler 1.2.15 → 1.2.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with optional Codex hooks.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -22,12 +22,13 @@ codebase, tests, Git review, or the coding agent.
22
22
  agent's own installed skill catalog.
23
23
 
24
24
  For an existing application's first Stack selection, inspect its real setup,
25
- build, and launch commands before relying on component defaults. A selected
25
+ build, and output commands before relying on component defaults. A selected
26
26
  component describes its current foundation; it does not silently port older
27
27
  source. When the existing commands differ, keep the implementation unchanged
28
- and declare exact project-owned `## Workspace setup` and `## Launch` overrides.
29
- Do not claim the inherited component recipe is usable until it matches the
30
- source.
28
+ and declare exact project-owned consumer-operation overrides (for Vibe64,
29
+ `## Workspace setup` and `## Outputs`). Genesis composes those sections as
30
+ opaque text; the named consumer alone owns their meaning and execution. Do not
31
+ claim the inherited component recipe is usable until it matches the source.
31
32
 
32
33
  Program is concise, fallible explanation. Its Sources and optional
33
34
  Implementation maps aid navigation but never substitute for reading code,
@@ -42,11 +43,12 @@ Neither is authority or proof; both may be regenerated with `genesis index`.
42
43
  - Work directly in the current Git tree and leave useful edits visible in the
43
44
  ordinary diff.
44
45
  - Do not edit the Blueprint, Program, or `.genesis/` during implementation; the
45
- separate reconciliation turn owns explanatory updates. Keep an exact Stack
46
- `## Launch` declaration aligned when the implementation establishes or
47
- intentionally changes how the project is started or how a preview host may request
48
- an application preview identity. Record only capability metadata and
49
- environment variable names there, never environment values or secrets.
46
+ separate reconciliation turn owns explanatory updates. Keep exact
47
+ consumer-owned operation declarations, such as Vibe64 `## Outputs`, aligned
48
+ when implementation intentionally changes the corresponding commands or
49
+ capabilities. Record only capability metadata and environment-variable names
50
+ there, never values or secrets; do not infer or execute the section as
51
+ Genesis behavior.
50
52
  - Follow established project and technology seams instead of creating parallel
51
53
  frameworks, persistence layers, transports, validators, or UI systems.
52
54
  - Never invent unavailable external-resource values or pass literal
@@ -1,6 +1,6 @@
1
1
  export const GENESIS_CONTRACTS = Object.freeze({
2
2
  derivedArtifacts: 'genesis.derived-artifacts.v1',
3
- environment: 'genesis.environment.v1',
3
+ environment: 'genesis.environment.v2',
4
4
  stackSection: 'genesis.stack-section.v1',
5
5
  verification: 'genesis.verification.v1',
6
6
  });
@@ -7,6 +7,8 @@ import { parseStackVerificationLines } from './stack-verification.js';
7
7
  import { normalizeSource } from './utils.js';
8
8
 
9
9
  const STACK_PIECE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
10
+ const RESOURCE_SEMANTIC_PATTERN = /^[a-z][A-Za-z0-9]*$/u;
11
+ const ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
10
12
  const STACK_CUSTOMIZATION_TITLE = /^# Stack customization: ([a-z0-9]+(?:-[a-z0-9]+)*)\s*$/u;
11
13
  const STACK_CUSTOMIZATION_FIELDS = new Map([
12
14
  ['Description', 'description'],
@@ -190,6 +192,14 @@ export function parseStackResourceLines(lines, {
190
192
  }
191
193
  if (
192
194
  !resource
195
+ || typeof resource !== 'object'
196
+ || Array.isArray(resource)
197
+ || Object.keys(resource).some((name) => ![
198
+ 'environmentAlternatives',
199
+ 'id',
200
+ 'kind',
201
+ 'optionalBindings',
202
+ ].includes(name))
193
203
  || typeof resource.id !== 'string'
194
204
  || !STACK_PIECE_ID_PATTERN.test(resource.id)
195
205
  || typeof resource.kind !== 'string'
@@ -199,26 +209,70 @@ export function parseStackResourceLines(lines, {
199
209
  ) {
200
210
  invalid(resourcePath, 'A Stack resource needs an id, kind, and at least one environment alternative.');
201
211
  }
212
+ const optionalBindings = normalizeResourceBindings(
213
+ resource.optionalBindings === undefined ? {} : resource.optionalBindings,
214
+ resourcePath,
215
+ resource.id,
216
+ { allowEmpty: true },
217
+ );
218
+ const optionalSemantics = new Set(Object.keys(optionalBindings));
202
219
  const environmentAlternatives = resource.environmentAlternatives.map((alternative) => {
203
- const required = alternative?.required;
204
- const allowEmpty = alternative?.allowEmpty || [];
220
+ if (!alternative || typeof alternative !== 'object' || Array.isArray(alternative)) {
221
+ invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
222
+ }
223
+ const unknown = Object.keys(alternative).filter((name) => (
224
+ !['allowEmpty', 'bindings', 'preferred'].includes(name)
225
+ ));
226
+ const allowEmpty = alternative.allowEmpty === undefined ? [] : alternative.allowEmpty;
205
227
  if (
206
- !Array.isArray(required)
228
+ unknown.length > 0
207
229
  || !Array.isArray(allowEmpty)
230
+ || (alternative.preferred !== undefined && typeof alternative.preferred !== 'boolean')
208
231
  ) {
209
232
  invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
210
233
  }
211
- const names = [...required, ...allowEmpty];
234
+ const bindings = normalizeResourceBindings(
235
+ alternative.bindings,
236
+ resourcePath,
237
+ resource.id,
238
+ );
239
+ const semantics = Object.keys(bindings);
212
240
  if (
213
- required.length === 0
214
- || names.some((name) => typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name))
215
- || allowEmpty.some((name) => !required.includes(name))
216
- || new Set(required).size !== required.length
241
+ allowEmpty.some((semantic) => !semantics.includes(semantic))
242
+ || allowEmpty.some((semantic) => typeof semantic !== 'string')
217
243
  || new Set(allowEmpty).size !== allowEmpty.length
218
- ) invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
219
- return { required: [...required], allowEmpty: [...allowEmpty] };
244
+ || semantics.some((semantic) => optionalSemantics.has(semantic))
245
+ || Object.values(bindings).some((name) => Object.values(optionalBindings).includes(name))
246
+ ) {
247
+ invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
248
+ }
249
+ return {
250
+ allowEmpty: [...allowEmpty].sort(),
251
+ bindings,
252
+ preferred: alternative.preferred === true,
253
+ };
220
254
  });
221
- return { id: resource.id, kind: resource.kind, environmentAlternatives };
255
+ if (environmentAlternatives.filter(({ preferred }) => preferred).length !== 1) {
256
+ invalid(resourcePath, `Stack resource ${resource.id} needs exactly one preferred environment alternative.`);
257
+ }
258
+ const semanticByEnvironment = new Map(Object.entries(optionalBindings).map(([semantic, name]) => [
259
+ name,
260
+ semantic,
261
+ ]));
262
+ for (const alternative of environmentAlternatives) {
263
+ for (const [semantic, name] of Object.entries(alternative.bindings)) {
264
+ if (semanticByEnvironment.has(name) && semanticByEnvironment.get(name) !== semantic) {
265
+ invalid(resourcePath, `Stack resource ${resource.id} assigns conflicting meanings to ${name}.`);
266
+ }
267
+ semanticByEnvironment.set(name, semantic);
268
+ }
269
+ }
270
+ return {
271
+ id: resource.id,
272
+ kind: resource.kind,
273
+ environmentAlternatives,
274
+ optionalBindings,
275
+ };
222
276
  });
223
277
  if (new Set(parsed.map(({ id }) => id)).size !== parsed.length) {
224
278
  invalid(resourcePath, '## Resources contains a duplicate resource id.');
@@ -226,6 +280,29 @@ export function parseStackResourceLines(lines, {
226
280
  return parsed;
227
281
  }
228
282
 
283
+ function normalizeResourceBindings(value, resourcePath, resourceId, {
284
+ allowEmpty = false,
285
+ } = {}) {
286
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
287
+ invalid(resourcePath, `Stack resource ${resourceId} has invalid semantic environment bindings.`);
288
+ }
289
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
290
+ if (!allowEmpty && entries.length === 0) {
291
+ invalid(resourcePath, `Stack resource ${resourceId} has an empty environment alternative.`);
292
+ }
293
+ if (
294
+ entries.some(([semantic, name]) => (
295
+ !RESOURCE_SEMANTIC_PATTERN.test(semantic)
296
+ || typeof name !== 'string'
297
+ || !ENVIRONMENT_NAME_PATTERN.test(name)
298
+ ))
299
+ || new Set(entries.map(([, name]) => name)).size !== entries.length
300
+ ) {
301
+ invalid(resourcePath, `Stack resource ${resourceId} has invalid semantic environment bindings.`);
302
+ }
303
+ return Object.fromEntries(entries);
304
+ }
305
+
229
306
  function resources(all, piecePath) {
230
307
  return parseStackResourceLines(all.has('Resources') ? all.get('Resources') : undefined, {
231
308
  path: piecePath,
@@ -1,8 +1,8 @@
1
- function present(environment, name, allowEmpty) {
1
+ function present(environment, name, allowEmpty = false) {
2
2
  if (!Object.hasOwn(environment, name) || typeof environment[name] !== 'string') return false;
3
3
  const value = environment[name].trim();
4
4
  if (value === `$${name}` || value === `\${${name}}`) return false;
5
- return value.length > 0 || allowEmpty.includes(name);
5
+ return value.length > 0 || allowEmpty;
6
6
  }
7
7
 
8
8
  /** Core reports declarations; each Stack component defines their meaning. */
@@ -11,11 +11,15 @@ export function missingStackResources({ environment = process.env, resources = [
11
11
  throw new TypeError('Stack resource inspection requires an environment object and resource declarations.');
12
12
  }
13
13
  return resources.flatMap(({ component, resource }) => {
14
- const satisfied = resource.environmentAlternatives.some(({ required, allowEmpty }) => (
15
- required.every((name) => present(environment, name, allowEmpty))
14
+ const satisfied = resource.environmentAlternatives.some(({ bindings, allowEmpty }) => (
15
+ Object.entries(bindings).every(([semantic, name]) => (
16
+ present(environment, name, allowEmpty.includes(semantic))
17
+ ))
16
18
  ));
17
19
  if (satisfied) return [];
18
- const alternatives = resource.environmentAlternatives.map(({ required }) => required.join(' + '));
20
+ const alternatives = resource.environmentAlternatives.map(({ bindings }) => (
21
+ Object.values(bindings).join(' + ')
22
+ ));
19
23
  return [{
20
24
  code: 'STACK_RESOURCE_MISSING',
21
25
  message: `Stack component ${component} requires ${resource.id}: ${alternatives.join(' OR ')}.`,