genesis-compiler 1.10.4 → 1.10.6

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/README.md CHANGED
@@ -264,8 +264,10 @@ files alone does not refresh its existing context.
264
264
 
265
265
  Project-scoped options follow the command, for example
266
266
  `genesis check --project-root /path/to/project`. Commands support `--json` when
267
- a host or script needs the normalized result. Run `genesis --help` for the
268
- compact CLI synopsis.
267
+ a host or script needs the normalized result. A failed command prints its
268
+ diagnostic code and message on standard error; when the failure came from a
269
+ child process such as Git, its bounded output follows, so the actual reason is
270
+ visible without `--json`. Run `genesis --help` for the compact CLI synopsis.
269
271
 
270
272
  ## Collaboration approach
271
273
 
@@ -941,6 +943,7 @@ import {
941
943
  syncSkills,
942
944
  validateArchitectureFacts,
943
945
  validateArchitectureObservations,
946
+ validateCityDocument,
944
947
  verify,
945
948
  withTrustedGitRepository,
946
949
  } from 'genesis-compiler';
@@ -991,6 +994,12 @@ and packages; `connectArchitectureObservations()` joins exact owners and
991
994
  controller operations. `mergeArchitectureObservations()` combines distinct
992
995
  inspectors while rejecting conflicting facts or package snapshots.
993
996
  `validateArchitectureFacts()` validates the combined architecture snapshot.
997
+ `validateCityDocument(city, kind)` validates a saved or freshly built Machine
998
+ (`'machine'`) or Program (`'program'`) City document against the format Genesis
999
+ writes, including embedded architecture facts, and returns it unchanged; hosts
1000
+ reading `.genesis/*-city.json` call it rather than keeping their own copy.
1001
+ `MACHINE_CITY_SCHEMA`, `PROGRAM_CITY_SCHEMA` and `CITY_SCHEMA_VERSION` name
1002
+ those formats.
994
1003
  These operations neither run a framework inspector nor claim complete runtime
995
1004
  behavior.
996
1005
  `sourceExplanationGuidance()` and `sourceExplanationReviewGuidance()` supply
package/docs/templates.md CHANGED
@@ -83,7 +83,14 @@ The framework starters live in `seed-django`, `seed-rails`, `seed-spring-boot`,
83
83
  source, dependency lockfile, setup/run contracts and focused checks.
84
84
 
85
85
  Applying fetches the explicitly named branch once, resolves its commit and reads
86
- its ordinary Git blobs into a temporary project. Genesis rejects arbitrary links,
86
+ its ordinary Git blobs into a temporary project. Both scratch repositories, the
87
+ fetched source and the prepared project, live inside the destination's Git-local
88
+ `genesis-template.lock` directory (`git rev-parse --git-path genesis-template.lock`),
89
+ never in the system temporary directory or the work tree. Every Git command
90
+ therefore runs inside the destination repository, which hosts that confine Git
91
+ to the project accept, and the scratch is removed with the lock when the
92
+ operation succeeds or fails. A linked worktree keeps its lock and scratch in its
93
+ own private Git directory. Genesis rejects arbitrary links,
87
94
  submodules, unsafe paths, dependencies and private dotenv files in the template.
88
95
  The only link exception is the exact generated Claude skill discovery link
89
96
  `.claude/skills/<name>` → `../../.agents/skills/<name>`. Incoming discovery links
@@ -94,9 +101,11 @@ generated links without following links supplied by a template.
94
101
  It validates the prepared project before changing the destination. A project
95
102
  lock serializes competing imports; the source and authored bootstrap files are
96
103
  checked again immediately before the copy. An ordinary copy failure rolls back
97
- written files. If a process is forcibly killed, inspect any partial work and the
98
- Git-local `genesis-template.lock` before removing the abandoned lock and retrying;
99
- Genesis never treats partial application code as an empty destination.
104
+ written files. `SIGINT` and `SIGTERM` let the CLI finish that cleanup. If a process
105
+ is forcibly killed, inspect any partial work and the Git-local
106
+ `genesis-template.lock` before removing the abandoned lock, and the scratch inside
107
+ it, and retrying; Genesis never treats partial application code as an empty
108
+ destination.
100
109
 
101
110
  The destination keeps its Git history, remotes, branch, meaningful Blueprint,
102
111
  collaboration/engineering choices, and authored Stack operation sections.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with project agent guidance.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
package/src/cli.js CHANGED
@@ -233,6 +233,14 @@ function line(stream, value = '') {
233
233
  stream.write(`${String(value).replace(/\n$/u, '')}\n`);
234
234
  }
235
235
 
236
+ // A failed command's bounded output usually carries its actual reason.
237
+ function diagnosticOutput(stream, item) {
238
+ for (const field of ['stdout', 'stderr']) {
239
+ const output = item.details?.[field];
240
+ if (typeof output === 'string' && output.trim()) line(stream, output.trim());
241
+ }
242
+ }
243
+
236
244
  function namedItems(label, values) {
237
245
  if (!Array.isArray(values) || values.length === 0) return;
238
246
  line(process.stdout, `${label}:`);
@@ -413,10 +421,7 @@ function writeResult(command, result) {
413
421
  namedItems('Verification commands', result.commands?.map(({ label, argv }) => `${label}: ${argv.join(' ')}`));
414
422
  for (const item of result.diagnostics || []) {
415
423
  line(process.stdout, `${item.code}: ${item.message}`);
416
- for (const field of ['stdout', 'stderr']) {
417
- const output = item.details?.[field]?.trim();
418
- if (output) line(process.stdout, output);
419
- }
424
+ diagnosticOutput(process.stdout, item);
420
425
  }
421
426
  if (result.guidance) line(process.stdout, result.guidance);
422
427
  if (result.prompt) {
@@ -711,7 +716,10 @@ export async function runCli(argv = process.argv.slice(2)) {
711
716
  } catch (error) {
712
717
  const diagnostic = asDiagnostic(error);
713
718
  if (options.json) line(process.stderr, JSON.stringify(diagnostic));
714
- else line(process.stderr, `${diagnostic.code}: ${diagnostic.message}`);
719
+ else {
720
+ line(process.stderr, `${diagnostic.code}: ${diagnostic.message}`);
721
+ diagnosticOutput(process.stderr, diagnostic);
722
+ }
715
723
  return 1;
716
724
  } finally {
717
725
  process.removeListener('SIGINT', abort);
@@ -0,0 +1,148 @@
1
+ import { GenesisError } from './errors.js';
2
+ import { validateArchitectureFacts } from './architecture.js';
3
+
4
+ // The Machine and Program City document formats are produced here
5
+ // (code-index.js), so their shape and validation are owned here too. A host
6
+ // reading a saved City calls validateCityDocument instead of keeping a copy.
7
+ export const MACHINE_CITY_SCHEMA = 'genesis.machine-city.v1';
8
+ export const PROGRAM_CITY_SCHEMA = 'genesis.program-city.v1';
9
+ export const CITY_SCHEMA_VERSION = 1;
10
+
11
+ const CITY_KINDS = Object.freeze({
12
+ machine: Object.freeze({ schema: MACHINE_CITY_SCHEMA, title: 'Machine City' }),
13
+ program: Object.freeze({ schema: PROGRAM_CITY_SCHEMA, title: 'Program City' }),
14
+ });
15
+
16
+ function checker(kind) {
17
+ const title = CITY_KINDS[kind].title;
18
+ const check = (condition, message) => {
19
+ if (!condition) throw new GenesisError('CITY_INVALID', `${title} ${message}`, { kind });
20
+ };
21
+ const record = (value, label) => check(value !== null && typeof value === 'object' && !Array.isArray(value), `${label} must be an object.`);
22
+ const array = (value, label) => check(Array.isArray(value), `${label} must be an array.`);
23
+ const string = (value, label) => check(typeof value === 'string', `${label} must be a string.`);
24
+ const optionalString = (value, label) => check(value === null || typeof value === 'string', `${label} must be a string or null.`);
25
+ const boolean = (value, label) => check(typeof value === 'boolean', `${label} must be a boolean.`);
26
+ const count = (value, label) => check(Number.isInteger(value) && value >= 0, `${label} must be a non-negative integer.`);
27
+ const strings = (value, label) => {
28
+ array(value, label);
29
+ value.forEach((entry, index) => string(entry, `${label}[${index}]`));
30
+ };
31
+ const diagnostics = (value) => {
32
+ array(value, 'diagnostics');
33
+ value.forEach((entry, index) => record(entry, `diagnostics[${index}]`));
34
+ };
35
+ const uniqueIds = (records, label) => {
36
+ const seen = new Set();
37
+ records.forEach((entry, index) => {
38
+ record(entry, `${label}[${index}]`);
39
+ string(entry.id, `${label}[${index}].id`);
40
+ check(!seen.has(entry.id), `${label} contains duplicate id ${entry.id}.`);
41
+ seen.add(entry.id);
42
+ });
43
+ return seen;
44
+ };
45
+ const districts = (value) => {
46
+ array(value, 'districts');
47
+ const ids = uniqueIds(value, 'districts');
48
+ value.forEach((district, index) => {
49
+ const label = `districts[${index}]`;
50
+ string(district.path, `${label}.path`);
51
+ string(district.title, `${label}.title`);
52
+ optionalString(district.parentId, `${label}.parentId`);
53
+ if (district.parentId !== null) check(ids.has(district.parentId), `${label}.parentId does not identify a district.`);
54
+ });
55
+ return ids;
56
+ };
57
+ return { array, boolean, check, count, diagnostics, districts, optionalString, record, string, strings, uniqueIds };
58
+ }
59
+
60
+ function validateMachineCity(city) {
61
+ const v = checker('machine');
62
+ v.string(city.codeHash, 'codeHash');
63
+ v.strings(city.stackComponents, 'stackComponents');
64
+ v.strings(city.indexers, 'indexers');
65
+ v.diagnostics(city.diagnostics);
66
+ const districtIds = v.districts(city.districts);
67
+
68
+ v.array(city.buildings, 'buildings');
69
+ const buildingIds = v.uniqueIds(city.buildings, 'buildings');
70
+ city.buildings.forEach((building, index) => {
71
+ const label = `buildings[${index}]`;
72
+ for (const field of ['path', 'title', 'districtId', 'language', 'role', 'hash']) v.string(building[field], `${label}.${field}`);
73
+ v.check(districtIds.has(building.districtId), `${label}.districtId does not identify a district.`);
74
+ for (const field of ['mode', 'bytes', 'lines']) v.count(building[field], `${label}.${field}`);
75
+ v.strings(building.extractors, `${label}.extractors`);
76
+ v.strings(building.functionIds, `${label}.functionIds`);
77
+ v.count(building.publicFunctionCount, `${label}.publicFunctionCount`);
78
+ v.count(building.internalFunctionCount, `${label}.internalFunctionCount`);
79
+ });
80
+
81
+ v.array(city.functions, 'functions');
82
+ const functionIds = v.uniqueIds(city.functions, 'functions');
83
+ city.functions.forEach((entry, index) => {
84
+ const label = `functions[${index}]`;
85
+ for (const field of ['name', 'qualifiedName', 'kind', 'visibility', 'path', 'fileId', 'role', 'language', 'extractor']) {
86
+ v.string(entry[field], `${label}.${field}`);
87
+ }
88
+ v.optionalString(entry.container, `${label}.container`);
89
+ v.check(buildingIds.has(entry.fileId), `${label}.fileId does not identify a building.`);
90
+ v.count(entry.line, `${label}.line`);
91
+ v.count(entry.column, `${label}.column`);
92
+ v.strings(entry.parameters, `${label}.parameters`);
93
+ for (const field of ['async', 'generator', 'static']) v.boolean(entry[field], `${label}.${field}`);
94
+ });
95
+ city.buildings.forEach((building, index) => {
96
+ for (const functionId of building.functionIds) {
97
+ v.check(functionIds.has(functionId), `buildings[${index}].functionIds contains an unknown function id.`);
98
+ }
99
+ });
100
+ if (city.architecture) validateArchitectureFacts(city.architecture);
101
+ }
102
+
103
+ function validateProgramCity(city) {
104
+ const v = checker('program');
105
+ v.string(city.programHash, 'programHash');
106
+ v.diagnostics(city.diagnostics);
107
+ const districtIds = v.districts(city.districts);
108
+
109
+ v.array(city.buildings, 'buildings');
110
+ const buildingIds = v.uniqueIds(city.buildings, 'buildings');
111
+ city.buildings.forEach((building, index) => {
112
+ const label = `buildings[${index}]`;
113
+ for (const field of ['name', 'title', 'description', 'publicContract', 'implementationMap', 'path']) v.string(building[field], `${label}.${field}`);
114
+ v.optionalString(building.subsystem, `${label}.subsystem`);
115
+ v.optionalString(building.districtId, `${label}.districtId`);
116
+ v.check(building.districtId === null || districtIds.has(building.districtId), `${label}.districtId does not identify a district.`);
117
+ v.strings(building.sources, `${label}.sources`);
118
+ if (building.mainSource !== undefined) {
119
+ v.string(building.mainSource, `${label}.mainSource`);
120
+ v.check(building.sources.includes(building.mainSource), `${label}.mainSource must be a cited source.`);
121
+ }
122
+ v.strings(building.sourceFileIds, `${label}.sourceFileIds`);
123
+ });
124
+
125
+ v.array(city.links, 'links');
126
+ city.links.forEach((link, index) => {
127
+ const label = `links[${index}]`;
128
+ v.record(link, label);
129
+ v.check(link.kind === 'implemented-by', `${label}.kind must be implemented-by.`);
130
+ v.string(link.fromId, `${label}.fromId`);
131
+ v.string(link.toId, `${label}.toId`);
132
+ v.check(buildingIds.has(link.fromId), `${label}.fromId does not identify a Program building.`);
133
+ });
134
+ }
135
+
136
+ // Validates a saved or freshly built City document of the given kind
137
+ // ('machine' or 'program') and returns it unchanged.
138
+ export function validateCityDocument(city, kind) {
139
+ if (!Object.hasOwn(CITY_KINDS, kind)) throw new GenesisError('CITY_KIND_UNSUPPORTED', `City kind is not supported: ${String(kind || '')}`);
140
+ const v = checker(kind);
141
+ v.record(city, 'document');
142
+ v.check(city.schema === CITY_KINDS[kind].schema, `schema must be ${CITY_KINDS[kind].schema}.`);
143
+ v.check(city.schemaVersion === CITY_SCHEMA_VERSION, `schemaVersion must be ${CITY_SCHEMA_VERSION}.`);
144
+ v.string(city.status, 'status');
145
+ if (kind === 'machine') validateMachineCity(city);
146
+ else validateProgramCity(city);
147
+ return city;
148
+ }
@@ -11,6 +11,7 @@ import { isProjectContentPath } from './paths.js';
11
11
  import { inspectProgram } from './program.js';
12
12
  import { inspectSubsystems } from './subsystems.js';
13
13
  import { buildArchitectureFacts } from './architecture.js';
14
+ import { CITY_SCHEMA_VERSION, MACHINE_CITY_SCHEMA, PROGRAM_CITY_SCHEMA } from './city-documents.js';
14
15
  import { gitVisibleFileStates } from './project-files.js';
15
16
  import { readStack } from './stack.js';
16
17
  import { sha256, stableJson, writeFileAtomic } from './utils.js';
@@ -162,8 +163,8 @@ function machineCity({ cityRegions, components, contributions, diagnostics, inde
162
163
  mode,
163
164
  }))));
164
165
  return {
165
- schema: 'genesis.machine-city.v1',
166
- schemaVersion: 1,
166
+ schema: MACHINE_CITY_SCHEMA,
167
+ schemaVersion: CITY_SCHEMA_VERSION,
167
168
  status: indexers.length === 0 ? 'unconfigured' : diagnostics.length > 0 ? 'completed-with-warning' : 'current',
168
169
  codeHash,
169
170
  presentationRegions: presentation.regions,
@@ -206,8 +207,8 @@ async function programCity(projectRoot) {
206
207
  sourceFileIds: module.sources.map(fileId),
207
208
  }));
208
209
  return {
209
- schema: 'genesis.program-city.v1',
210
- schemaVersion: 1,
210
+ schema: PROGRAM_CITY_SCHEMA,
211
+ schemaVersion: CITY_SCHEMA_VERSION,
211
212
  status: program.status,
212
213
  programHash: sha256(stableJson({ operations, subsystems: program.subsystemMap })),
213
214
  diagnostics: [],
@@ -221,8 +222,8 @@ async function programCity(projectRoot) {
221
222
  };
222
223
  } catch (error) {
223
224
  return {
224
- schema: 'genesis.program-city.v1',
225
- schemaVersion: 1,
225
+ schema: PROGRAM_CITY_SCHEMA,
226
+ schemaVersion: CITY_SCHEMA_VERSION,
226
227
  status: 'invalid',
227
228
  programHash: sha256('invalid'),
228
229
  diagnostics: [asDiagnostic(error)],
@@ -64,7 +64,7 @@ async function installedCatalogs(stackPackages, projectRoot) {
64
64
  }
65
65
 
66
66
  /** Catalogues are explicit data sources. A Stack or Skill never implicitly installs a template. */
67
- export async function listTemplates({ projectRoot = process.cwd(), stackPackages = [], templateSources = [] } = {}) {
67
+ export async function listTemplates({ projectRoot = process.cwd(), stackPackages = [], templateSources = [], scratchRoot } = {}) {
68
68
  let recorded = [];
69
69
  try { recorded = (await readStack(projectRoot, { stackPackages })).stackPackages; } catch { /* Explicit sources also work before init. */ }
70
70
  stackPackages = [...new Set([...recorded, ...stackPackages])];
@@ -78,7 +78,7 @@ export async function listTemplates({ projectRoot = process.cwd(), stackPackages
78
78
  namespaces.add(source.namespace);
79
79
  let catalog = source.catalog;
80
80
  if (!catalog) {
81
- const snapshot = await readGitSnapshot({ repository: source.repository, branch: source.branch || 'main' });
81
+ const snapshot = await readGitSnapshot({ repository: source.repository, branch: source.branch || 'main', scratchRoot });
82
82
  const file = snapshot.files.find(({ path: name }) => name === 'genesis.templates.json');
83
83
  if (!file) throw templateCatalogError(`${source.namespace} does not contain genesis.templates.json.`);
84
84
  try { catalog = JSON.parse(file.contents.toString('utf8')); }
@@ -1,5 +1,4 @@
1
- import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, rmdir, symlink, writeFile } from 'node:fs/promises';
2
- import os from 'node:os';
1
+ import { lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, symlink, writeFile } from 'node:fs/promises';
3
2
  import path from 'node:path';
4
3
  import { parseBlueprintSource } from './blueprint.js';
5
4
  import { buildProjectIndex } from './code-index.js';
@@ -79,22 +78,23 @@ async function assertSeedable(root, stackPackages) {
79
78
  /** Apply one complete source tree without changing Git history or running application commands. */
80
79
  export async function applyTemplate({ projectRoot, templateId, stackPackages = [], templateSources = [] } = {}) {
81
80
  const root = (await gitContext(projectRoot)).repositoryRoot;
81
+ // The lock directory also holds the scratch repositories, so a host that
82
+ // confines Git to the project accepts them and they never outlive the lock.
82
83
  const lockPath = path.resolve(root, await runGitText(root, ['rev-parse', '--git-path', 'genesis-template.lock']));
83
84
  try { await mkdir(lockPath); }
84
85
  catch (error) {
85
86
  if (error.code === 'EEXIST') throw new GenesisError('TEMPLATE_PROJECT_BUSY', 'Another template operation holds this project lock.');
86
87
  throw error;
87
88
  }
88
- let stagingRoot;
89
89
  try {
90
90
  await assertSeedable(root, stackPackages);
91
- const { templates } = await listTemplates({ projectRoot: root, stackPackages, templateSources });
91
+ const { templates } = await listTemplates({ projectRoot: root, stackPackages, templateSources, scratchRoot: lockPath });
92
92
  const template = resolveTemplate(templates, String(templateId || ''));
93
93
  const preserved = new Map();
94
94
  // A seedable project's .gitignore can only be the one init wrote for the Cities.
95
95
  for (const name of [...GENESIS_BOOTSTRAP_PATHS, '.gitignore']) preserved.set(name, await existingFile(root, name));
96
- const snapshot = await readGitSnapshot(template);
97
- stagingRoot = await mkdtemp(path.join(os.tmpdir(), 'genesis-template-project-'));
96
+ const snapshot = await readGitSnapshot({ ...template, scratchRoot: lockPath });
97
+ const stagingRoot = await mkdtemp(path.join(lockPath, 'genesis-template-project-'));
98
98
  await runGit(stagingRoot, ['init', '--quiet']);
99
99
  for (const file of snapshot.files) {
100
100
  if (file.path.split('/').includes('node_modules') || file.path.split('/').some((part) => part === '.env' || part.startsWith('.env.') && part !== '.env.example')) {
@@ -168,7 +168,6 @@ export async function applyTemplate({ projectRoot, templateId, stackPackages = [
168
168
  guidance: 'The starting application is ready for its declared workspace setup. No application commands or Git commits were run.',
169
169
  };
170
170
  } finally {
171
- if (stagingRoot) await rm(stagingRoot, { recursive: true, force: true });
172
- await rmdir(lockPath);
171
+ await rm(lockPath, { recursive: true, force: true });
173
172
  }
174
173
  }
@@ -17,13 +17,16 @@ function validateRepository(repository) {
17
17
  }
18
18
  }
19
19
 
20
- /** Fetch a branch once and read ordinary blobs; never execute or check out remote hooks/filters. */
21
- export async function readGitSnapshot({ repository, branch }) {
20
+ /**
21
+ * Fetch a branch once and read ordinary blobs; never execute or check out remote hooks/filters.
22
+ * The scratch repository is created below scratchRoot (the system temporary directory by default).
23
+ */
24
+ export async function readGitSnapshot({ repository, branch, scratchRoot = os.tmpdir() }) {
22
25
  validateRepository(repository);
23
26
  if (typeof branch !== 'string' || !branch || branch.startsWith('-')) {
24
27
  throw new GenesisError('TEMPLATE_SOURCE_INVALID', 'A template branch is required.');
25
28
  }
26
- const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'genesis-template-source-'));
29
+ const temporaryRoot = await mkdtemp(path.join(scratchRoot, 'genesis-template-source-'));
27
30
  try {
28
31
  await runGit(temporaryRoot, ['init', '--bare', '--quiet']);
29
32
  await runGit(temporaryRoot, ['check-ref-format', `refs/heads/${branch}`]);
package/src/index.js CHANGED
@@ -248,3 +248,4 @@ export { inspectOpenCodePlugin, installOpenCodePlugin as syncOpenCodePlugin } fr
248
248
  export { ARCHITECTURE_OBSERVATIONS_SCHEMA, validateArchitectureObservations, connectArchitectureObservations, mergeArchitectureObservations } from './index/architecture-observations.js';
249
249
 
250
250
  export { validateArchitectureFacts } from './index/architecture.js';
251
+ export { CITY_SCHEMA_VERSION, MACHINE_CITY_SCHEMA, PROGRAM_CITY_SCHEMA, validateCityDocument } from './index/city-documents.js';