genesis-compiler 1.2.15 → 1.2.16

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.16",
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.16",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -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 ')}.`,