genesis-compiler 1.2.22 → 1.2.23

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
@@ -12,6 +12,7 @@ interaction.
12
12
 
13
13
  ```text
14
14
  genesis/
15
+ version project-file format version used for deterministic migrations
15
16
  blueprint.md non-technical product intent
16
17
  engineering.md selected engineering profile and project-specific requirements
17
18
  stack.md selected technology guidance, verification, declarations, and opaque extensions
@@ -106,7 +107,8 @@ type a command. It never runs adoption without approval and does nothing in an
106
107
  empty or already-adopted project.
107
108
 
108
109
  `genesis init` creates an empty Blueprint, a focused engineering approach, an
109
- empty optional Stack, three project Agent Skills, and project-local Codex hooks. The skills are ordinary
110
+ empty optional Stack, the current `genesis/version`, three project Agent Skills,
111
+ and project-local Codex hooks. The skills are ordinary
110
112
  [Agent Skills](https://agentskills.io): `genesis-project`, `genesis-program`,
111
113
  and `genesis-deslop`, each with standard `SKILL.md` and `agents/openai.yaml`
112
114
  metadata. Open `/hooks` once in Codex to review and trust the hooks. After
@@ -473,6 +475,8 @@ genesis check
473
475
 
474
476
  `check` is read-only. It reports only structural or recorded facts:
475
477
 
478
+ - whether the project format is current, outdated, unversioned, newer than the
479
+ running CLI, or invalid;
476
480
  - Blueprint, engineering approach, and Stack validity;
477
481
  - selected Agent Skill presence and structural validity;
478
482
  - Program presence and structural validity;
@@ -491,6 +495,20 @@ hashes match. It does not mean that the product behavior makes sense. Generate
491
495
  Genesis does not label Program semantically current. Program is ordinary
492
496
  Markdown reviewed through Git.
493
497
 
498
+ When `check` reports an outdated or unversioned project, migrate its Genesis
499
+ files to the running CLI's project format:
500
+
501
+ ```bash
502
+ genesis migrate
503
+ ```
504
+
505
+ Migration is deterministic and forward-only. It updates only recognized
506
+ Genesis formats, synchronizes managed skills and hooks, regenerates derived
507
+ indexes, and then returns a fresh `check` result. A newer project is never
508
+ downgraded. Every project-scoped CLI invocation warns when the recorded format
509
+ does not match the running CLI, and ordinary project operations stop until the
510
+ migration is resolved.
511
+
494
512
  ## Public API
495
513
 
496
514
  ```js
@@ -508,6 +526,7 @@ import {
508
526
  installCodex,
509
527
  listEngineeringProfiles,
510
528
  listStackPieces,
529
+ migrate,
511
530
  setEngineeringProfile,
512
531
  verify,
513
532
  } from 'genesis-compiler';
@@ -515,6 +534,8 @@ import {
515
534
 
516
535
  `initialize()` installs the project files, Genesis workflow skills, selected
517
536
  Stack skills, and local Codex hooks.
537
+ `migrate()` updates recognized older Genesis project-file formats and returns
538
+ the resulting structural check for hosts such as Vibe64.
518
539
  `adoptProject()` also returns the initial `adopt` prompt for an existing
519
540
  codebase. `installCodex()` installs the optional global discovery plugin.
520
541
  `listEngineeringProfiles()`, `inspectEngineering()`, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.22",
3
+ "version": "1.2.23",
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.22",
3
+ "version": "1.2.23",
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
@@ -15,6 +15,7 @@ import {
15
15
  installCodex,
16
16
  listEngineeringProfiles,
17
17
  listStackPieces,
18
+ migrate,
18
19
  setEngineeringProfile,
19
20
  verify,
20
21
  } from './index.js';
@@ -28,6 +29,10 @@ import {
28
29
  } from './index/codex-hooks.js';
29
30
  import { engineeringProfile, readEngineeringBaseline } from './index/engineering.js';
30
31
  import { asDiagnostic, fail } from './index/errors.js';
32
+ import {
33
+ inspectProjectFormat,
34
+ projectFormatDiagnostic,
35
+ } from './index/project-format.js';
31
36
 
32
37
  const USAGE = `Usage:
33
38
  genesis init
@@ -40,6 +45,7 @@ const USAGE = `Usage:
40
45
  genesis stack add <piece...>
41
46
  genesis context <path...>
42
47
  genesis index [function-or-path...]
48
+ genesis migrate
43
49
  genesis inspect environment
44
50
  genesis inspect section <name>
45
51
  genesis prompt [request...]
@@ -59,7 +65,7 @@ prompt to the agent you already use. Review all edits through the ordinary Git
59
65
  diff, then run genesis verify for the Stack's concrete checks.
60
66
  `;
61
67
 
62
- const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'prompt', 'stack', 'verify']);
68
+ const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'stack', 'verify']);
63
69
 
64
70
  function parseCommand(argv) {
65
71
  if (argv.length === 0 || argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
@@ -151,6 +157,9 @@ function namedItems(label, values) {
151
157
  }
152
158
 
153
159
  function writeCheck(result) {
160
+ const format = result.projectFormat;
161
+ const version = format.projectVersion === null ? format.status : format.projectVersion;
162
+ line(process.stdout, `Project format: ${version} (${format.status}; CLI supports ${format.supportedVersion})`);
154
163
  line(process.stdout, `Blueprint: ${result.blueprint}`);
155
164
  line(process.stdout, `Engineering approach: ${result.engineering}`);
156
165
  line(process.stdout, `Stack: ${result.stack}`);
@@ -301,6 +310,7 @@ async function execute({ command, operands, options }, { signal } = {}) {
301
310
  const projectRoot = options.projectRoot || process.cwd();
302
311
  const stackPackages = options.stackPackages || [];
303
312
  if (command === 'init') return initialize({ projectRoot, stackPackages });
313
+ if (command === 'migrate') return migrate({ projectRoot, stackPackages });
304
314
  if (command === 'adopt') {
305
315
  return adoptProject({ projectRoot, request: operands.join(' '), stackPackages });
306
316
  }
@@ -401,6 +411,34 @@ async function execute({ command, operands, options }, { signal } = {}) {
401
411
  fail('CLI_UNKNOWN_COMMAND', `Unknown command: ${command}`);
402
412
  }
403
413
 
414
+ async function optionalProjectFormat(projectRoot) {
415
+ try {
416
+ return await inspectProjectFormat({ projectRoot });
417
+ } catch (error) {
418
+ if (error?.code === 'GIT_REPOSITORY_REQUIRED') return null;
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ function requiresCurrentProjectFormat(command, operands) {
424
+ if (['check', 'codex', 'hook', 'migrate'].includes(command)) return false;
425
+ if (command === 'engineering') {
426
+ return operands[0] === 'set' || (operands[0] === 'show' && operands.length === 1);
427
+ }
428
+ return command !== 'help';
429
+ }
430
+
431
+ function withProjectFormatWarning(result, projectFormat, diagnostic) {
432
+ if (!diagnostic) return result;
433
+ const warnings = [...(result.warnings || [])];
434
+ if (!warnings.some((warning) => warning.code === diagnostic.code)) warnings.unshift(diagnostic);
435
+ return {
436
+ ...result,
437
+ projectFormat: result.projectFormat || projectFormat,
438
+ warnings,
439
+ };
440
+ }
441
+
404
442
  export async function runCli(argv = process.argv.slice(2)) {
405
443
  let options = {};
406
444
  const controller = new AbortController();
@@ -412,12 +450,24 @@ export async function runCli(argv = process.argv.slice(2)) {
412
450
  return 0;
413
451
  }
414
452
  options = parsed.options;
453
+ const projectFormat = await optionalProjectFormat(options.projectRoot || process.cwd());
454
+ const formatDiagnostic = projectFormatDiagnostic(projectFormat);
455
+ if (formatDiagnostic && !options.json) {
456
+ line(process.stderr, `WARNING: ${formatDiagnostic.message}`);
457
+ }
458
+ if (formatDiagnostic && requiresCurrentProjectFormat(parsed.command, parsed.operands)) {
459
+ fail(formatDiagnostic.code, formatDiagnostic.message, formatDiagnostic.details);
460
+ }
415
461
  const finiteCommand = parsed.command === 'verify';
416
462
  if (finiteCommand) {
417
463
  process.once('SIGINT', abort);
418
464
  process.once('SIGTERM', abort);
419
465
  }
420
- const result = await execute(parsed, { signal: controller.signal });
466
+ const result = withProjectFormatWarning(
467
+ await execute(parsed, { signal: controller.signal }),
468
+ projectFormat,
469
+ formatDiagnostic,
470
+ );
421
471
  if (options.json) line(process.stdout, JSON.stringify(result));
422
472
  else writeResult(parsed.command, result);
423
473
  return ['blocked', 'failed', 'invalid'].includes(result?.status) ? 2 : 0;
@@ -9,10 +9,15 @@ import { missingStackResources } from './stack-preflight.js';
9
9
  import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
10
10
  import { inspectProjectEnvironment } from './environment-files.js';
11
11
  import { readEngineering } from './engineering.js';
12
+ import {
13
+ inspectProjectFormatAtRoot,
14
+ projectFormatDiagnostic,
15
+ } from './project-format.js';
12
16
 
13
- function invalidResult(area, error) {
17
+ function invalidResult(area, error, projectFormat) {
14
18
  return {
15
19
  status: 'invalid',
20
+ projectFormat,
16
21
  blueprint: area === 'blueprint' ? 'invalid' : 'valid',
17
22
  engineering: area === 'engineering' ? 'invalid' : 'unknown',
18
23
  stack: area === 'stack' ? 'invalid' : 'unknown',
@@ -29,16 +34,44 @@ function invalidResult(area, error) {
29
34
  };
30
35
  }
31
36
 
37
+ function formatMismatchResult(projectFormat) {
38
+ const diagnostic = projectFormatDiagnostic(projectFormat);
39
+ const status = ['unversioned', 'outdated'].includes(projectFormat.status)
40
+ ? 'attention'
41
+ : projectFormat.status === 'newer' ? 'blocked' : 'invalid';
42
+ return {
43
+ status,
44
+ projectFormat,
45
+ blueprint: 'unknown',
46
+ engineering: 'unknown',
47
+ stack: 'unknown',
48
+ skills: 'unknown',
49
+ program: 'unknown',
50
+ environment: 'unknown',
51
+ extensions: 'unknown',
52
+ resources: 'unknown',
53
+ verification: 'unknown',
54
+ programFiles: [],
55
+ subsystems: [],
56
+ diagnostics: [diagnostic],
57
+ guidance: diagnostic.message,
58
+ };
59
+ }
60
+
32
61
  export async function checkProject({
33
62
  environment = process.env,
34
63
  projectRoot,
35
64
  stackPackages = [],
36
65
  } = {}) {
37
66
  const root = (await gitContext(projectRoot)).repositoryRoot;
67
+ const projectFormat = await inspectProjectFormatAtRoot(root);
68
+ if (!['current', 'uninitialized'].includes(projectFormat.status)) {
69
+ return formatMismatchResult(projectFormat);
70
+ }
38
71
  try {
39
72
  await readBlueprint(root, { required: true, requireDescription: true });
40
73
  } catch (error) {
41
- return invalidResult('blueprint', error);
74
+ return invalidResult('blueprint', error, projectFormat);
42
75
  }
43
76
 
44
77
  let stack;
@@ -46,13 +79,13 @@ export async function checkProject({
46
79
  try {
47
80
  engineering = await readEngineering(root);
48
81
  } catch (error) {
49
- return invalidResult('engineering', error);
82
+ return invalidResult('engineering', error, projectFormat);
50
83
  }
51
84
 
52
85
  try {
53
86
  stack = await readStack(root, { stackPackages });
54
87
  } catch (error) {
55
- return invalidResult('stack', error);
88
+ return invalidResult('stack', error, projectFormat);
56
89
  }
57
90
 
58
91
  let program;
@@ -119,6 +152,7 @@ export async function checkProject({
119
152
  status: program.status === 'invalid' || skills.status === 'invalid' || verification.status === 'invalid'
120
153
  ? 'invalid'
121
154
  : needsAttention ? 'attention' : 'ok',
155
+ projectFormat,
122
156
  blueprint: 'valid',
123
157
  engineering: engineering.status,
124
158
  stack: 'valid',
package/src/index/init.js CHANGED
@@ -6,7 +6,18 @@ import { BLUEPRINT_SKELETON_SOURCE } from './blueprint.js';
6
6
  import { installCodexHooks } from './codex-hooks.js';
7
7
  import { ENGINEERING_SKELETON_SOURCE } from './engineering.js';
8
8
  import { gitContext } from './git.js';
9
- import { BLUEPRINT_PATH, ENGINEERING_PATH, PROGRAM_ROOT, STACK_PATH } from './paths.js';
9
+ import {
10
+ BLUEPRINT_PATH,
11
+ ENGINEERING_PATH,
12
+ PROGRAM_ROOT,
13
+ PROJECT_VERSION_PATH,
14
+ STACK_PATH,
15
+ } from './paths.js';
16
+ import {
17
+ CURRENT_PROJECT_FORMAT_SOURCE,
18
+ inspectProjectFormatAtRoot,
19
+ requireCurrentProjectFormat,
20
+ } from './project-format.js';
10
21
  import { EMPTY_STACK_SOURCE, readStack } from './stack.js';
11
22
 
12
23
  async function createIfMissing(projectRoot, relativePath, source) {
@@ -23,6 +34,8 @@ async function createIfMissing(projectRoot, relativePath, source) {
23
34
 
24
35
  export async function initializeProject({ projectRoot, stackPackages = [] } = {}) {
25
36
  const root = (await gitContext(projectRoot)).repositoryRoot;
37
+ const projectFormat = await inspectProjectFormatAtRoot(root);
38
+ requireCurrentProjectFormat(projectFormat, { allowUninitialized: true });
26
39
  const created = (await Promise.all([
27
40
  createIfMissing(root, BLUEPRINT_PATH, BLUEPRINT_SKELETON_SOURCE),
28
41
  createIfMissing(root, ENGINEERING_PATH, ENGINEERING_SKELETON_SOURCE),
@@ -32,7 +45,12 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
32
45
  const stack = await readStack(root, { stackPackages });
33
46
  const skills = await syncProjectSkills({ projectRoot: root, stack });
34
47
  const hooks = await installCodexHooks({ projectRoot: root });
35
- const changedFiles = [...created, ...hooks.changedFiles, ...skills.changedFiles].sort();
48
+ const version = projectFormat.status === 'uninitialized'
49
+ ? await createIfMissing(root, PROJECT_VERSION_PATH, CURRENT_PROJECT_FORMAT_SOURCE)
50
+ : null;
51
+ const changedFiles = [...created, ...hooks.changedFiles, ...skills.changedFiles, version]
52
+ .filter(Boolean)
53
+ .sort();
36
54
  return {
37
55
  status: changedFiles.length > 0 ? 'updated' : 'unchanged',
38
56
  summary: changedFiles.length > 0
@@ -0,0 +1,85 @@
1
+ import { readBlueprint } from './blueprint.js';
2
+ import { readEngineering } from './engineering.js';
3
+ import { asDiagnostic, GenesisError } from './errors.js';
4
+ import { gitContext } from './git.js';
5
+ import { initializeProject } from './init.js';
6
+ import { inspectProgram } from './program.js';
7
+ import {
8
+ CURRENT_PROJECT_FORMAT_VERSION,
9
+ inspectProjectFormatAtRoot,
10
+ projectFormatDiagnostic,
11
+ writeProjectFormatVersion,
12
+ } from './project-format.js';
13
+ import { PROJECT_VERSION_PATH } from './paths.js';
14
+ import { readStack } from './stack.js';
15
+ import { uniqueSorted } from './utils.js';
16
+
17
+ async function validateLegacyProject({ projectRoot, stackPackages }) {
18
+ try {
19
+ await readBlueprint(projectRoot, { required: true });
20
+ await readEngineering(projectRoot);
21
+ await readStack(projectRoot, { stackPackages });
22
+ try {
23
+ await inspectProgram(projectRoot);
24
+ } catch (error) {
25
+ if (error?.code !== 'PROGRAM_SOURCE_MISSING') throw error;
26
+ }
27
+ } catch (error) {
28
+ throw new GenesisError(
29
+ 'PROJECT_FORMAT_MIGRATION_UNAVAILABLE',
30
+ `Genesis cannot automatically migrate these legacy project files: ${error.message}`,
31
+ { cause: asDiagnostic(error) },
32
+ );
33
+ }
34
+ }
35
+
36
+ const MIGRATIONS = new Map([
37
+ [0, validateLegacyProject],
38
+ ]);
39
+
40
+ export async function migrateProject({ projectRoot, stackPackages = [] } = {}) {
41
+ const root = (await gitContext(projectRoot)).repositoryRoot;
42
+ const initial = await inspectProjectFormatAtRoot(root);
43
+ if (initial.status === 'uninitialized') {
44
+ throw new GenesisError(
45
+ 'PROJECT_INITIALIZATION_REQUIRED',
46
+ 'No Genesis project files exist. Run `genesis init` first.',
47
+ );
48
+ }
49
+ if (['invalid', 'newer'].includes(initial.status)) {
50
+ const diagnostic = projectFormatDiagnostic(initial);
51
+ throw new GenesisError(diagnostic.code, diagnostic.message, diagnostic.details);
52
+ }
53
+
54
+ let version = initial.status === 'unversioned' ? 0 : initial.projectVersion;
55
+ const migratedFiles = [];
56
+ while (version < CURRENT_PROJECT_FORMAT_VERSION) {
57
+ const migration = MIGRATIONS.get(version);
58
+ if (!migration) {
59
+ throw new GenesisError(
60
+ 'PROJECT_FORMAT_MIGRATION_UNAVAILABLE',
61
+ `No migration from project format ${version} is installed.`,
62
+ { projectVersion: version, supportedVersion: CURRENT_PROJECT_FORMAT_VERSION },
63
+ );
64
+ }
65
+ await migration({ projectRoot: root, stackPackages });
66
+ version += 1;
67
+ await writeProjectFormatVersion(root, version);
68
+ migratedFiles.push(PROJECT_VERSION_PATH);
69
+ }
70
+
71
+ const initialized = await initializeProject({ projectRoot: root, stackPackages });
72
+ const changedFiles = uniqueSorted([...migratedFiles, ...initialized.changedFiles]);
73
+ const migrated = initial.status !== 'current';
74
+ return {
75
+ status: changedFiles.length > 0 ? 'updated' : 'unchanged',
76
+ summary: migrated
77
+ ? `Migrated Genesis project format from ${initial.status === 'unversioned' ? 'unversioned' : initial.projectVersion} to ${CURRENT_PROJECT_FORMAT_VERSION}.`
78
+ : `Genesis project format ${CURRENT_PROJECT_FORMAT_VERSION} is current.`,
79
+ fromVersion: initial.projectVersion,
80
+ toVersion: CURRENT_PROJECT_FORMAT_VERSION,
81
+ projectFormat: await inspectProjectFormatAtRoot(root),
82
+ changedFiles,
83
+ diagnostics: initialized.diagnostics,
84
+ };
85
+ }
@@ -2,6 +2,7 @@ export const BLUEPRINT_PATH = 'genesis/blueprint.md';
2
2
  export const ENGINEERING_PATH = 'genesis/engineering.md';
3
3
  export const STACK_PATH = 'genesis/stack.md';
4
4
  export const PROGRAM_ROOT = 'genesis/program';
5
+ export const PROJECT_VERSION_PATH = 'genesis/version';
5
6
  export const VERIFICATION_PATH = '.genesis/verification.json';
6
7
 
7
8
  export function isProjectContentPath(file) {
@@ -0,0 +1,138 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { asDiagnostic, GenesisError } from './errors.js';
5
+ import { gitContext } from './git.js';
6
+ import {
7
+ BLUEPRINT_PATH,
8
+ ENGINEERING_PATH,
9
+ PROGRAM_ROOT,
10
+ PROJECT_VERSION_PATH,
11
+ STACK_PATH,
12
+ } from './paths.js';
13
+ import { normalizeSource, pathState, writeFileAtomic } from './utils.js';
14
+
15
+ export const CURRENT_PROJECT_FORMAT_VERSION = 1;
16
+ export const CURRENT_PROJECT_FORMAT_SOURCE = `${CURRENT_PROJECT_FORMAT_VERSION}\n`;
17
+
18
+ const VERSION_PATTERN = /^(0|[1-9][0-9]*)\n?$/u;
19
+
20
+ function projectFormat(status, projectVersion, action, diagnostic = null) {
21
+ return {
22
+ path: PROJECT_VERSION_PATH,
23
+ status,
24
+ projectVersion,
25
+ supportedVersion: CURRENT_PROJECT_FORMAT_VERSION,
26
+ action,
27
+ ...(diagnostic ? { diagnostic } : {}),
28
+ };
29
+ }
30
+
31
+ export function parseProjectFormatVersion(source) {
32
+ const normalized = normalizeSource(source);
33
+ const match = normalized.match(VERSION_PATTERN);
34
+ const version = match ? Number(match[1]) : NaN;
35
+ if (!Number.isSafeInteger(version)) {
36
+ throw new GenesisError(
37
+ 'PROJECT_FORMAT_INVALID',
38
+ `${PROJECT_VERSION_PATH} must contain exactly one non-negative integer.`,
39
+ { path: PROJECT_VERSION_PATH },
40
+ );
41
+ }
42
+ return version;
43
+ }
44
+
45
+ async function hasGenesisSource(projectRoot) {
46
+ const states = await Promise.all([
47
+ BLUEPRINT_PATH,
48
+ ENGINEERING_PATH,
49
+ STACK_PATH,
50
+ PROGRAM_ROOT,
51
+ ].map((relative) => pathState(path.join(projectRoot, relative))));
52
+ return states.some(({ exists }) => exists);
53
+ }
54
+
55
+ export async function inspectProjectFormatAtRoot(projectRoot) {
56
+ let source;
57
+ try {
58
+ source = await readFile(path.join(projectRoot, PROJECT_VERSION_PATH), 'utf8');
59
+ } catch (error) {
60
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
61
+ const genesisSource = await hasGenesisSource(projectRoot);
62
+ return projectFormat(
63
+ genesisSource ? 'unversioned' : 'uninitialized',
64
+ null,
65
+ genesisSource ? 'migrate' : 'init',
66
+ );
67
+ }
68
+ const diagnostic = asDiagnostic(new GenesisError(
69
+ 'PROJECT_FORMAT_INVALID',
70
+ `${PROJECT_VERSION_PATH} could not be read: ${error.message}.`,
71
+ { path: PROJECT_VERSION_PATH, cause: error.code || error.message },
72
+ ));
73
+ return projectFormat('invalid', null, 'repair', diagnostic);
74
+ }
75
+
76
+ let version;
77
+ try {
78
+ version = parseProjectFormatVersion(source);
79
+ } catch (error) {
80
+ return projectFormat('invalid', null, 'repair', asDiagnostic(error));
81
+ }
82
+ if (version < CURRENT_PROJECT_FORMAT_VERSION) {
83
+ return projectFormat('outdated', version, 'migrate');
84
+ }
85
+ if (version > CURRENT_PROJECT_FORMAT_VERSION) {
86
+ return projectFormat('newer', version, 'update-genesis');
87
+ }
88
+ return projectFormat('current', version, null);
89
+ }
90
+
91
+ export async function inspectProjectFormat({ projectRoot } = {}) {
92
+ const root = (await gitContext(projectRoot)).repositoryRoot;
93
+ return inspectProjectFormatAtRoot(root);
94
+ }
95
+
96
+ export function projectFormatDiagnostic(format) {
97
+ if (!format || ['current', 'uninitialized'].includes(format.status)) return null;
98
+ if (format.status === 'invalid') return format.diagnostic;
99
+ if (format.status === 'unversioned') {
100
+ return {
101
+ code: 'PROJECT_FORMAT_UNVERSIONED',
102
+ message: `Genesis project files are unversioned; this CLI uses project format ${format.supportedVersion}. Run \`genesis migrate\`.`,
103
+ details: { path: format.path, supportedVersion: format.supportedVersion },
104
+ };
105
+ }
106
+ if (format.status === 'outdated') {
107
+ return {
108
+ code: 'PROJECT_FORMAT_OUTDATED',
109
+ message: `Project format ${format.projectVersion} is older than this CLI's format ${format.supportedVersion}. Run \`genesis migrate\`.`,
110
+ details: {
111
+ path: format.path,
112
+ projectVersion: format.projectVersion,
113
+ supportedVersion: format.supportedVersion,
114
+ },
115
+ };
116
+ }
117
+ return {
118
+ code: 'PROJECT_FORMAT_NEWER',
119
+ message: `Project format ${format.projectVersion} is newer than this CLI's format ${format.supportedVersion}. Update Genesis before using this project.`,
120
+ details: {
121
+ path: format.path,
122
+ projectVersion: format.projectVersion,
123
+ supportedVersion: format.supportedVersion,
124
+ },
125
+ };
126
+ }
127
+
128
+ export function requireCurrentProjectFormat(format, { allowUninitialized = false } = {}) {
129
+ if (format.status === 'current' || (allowUninitialized && format.status === 'uninitialized')) return;
130
+ const diagnostic = projectFormatDiagnostic(format);
131
+ throw new GenesisError(diagnostic.code, diagnostic.message, diagnostic.details);
132
+ }
133
+
134
+ export function writeProjectFormatVersion(projectRoot, version) {
135
+ const source = `${version}\n`;
136
+ parseProjectFormatVersion(source);
137
+ return writeFileAtomic(path.join(projectRoot, PROJECT_VERSION_PATH), source);
138
+ }
package/src/index.js CHANGED
@@ -11,17 +11,19 @@ import {
11
11
  selectEngineeringProfile,
12
12
  } from './index/engineering.js';
13
13
  import { initializeProject } from './index/init.js';
14
+ import { migrateProject } from './index/migration.js';
14
15
  import { installCodexPlugin } from './index/codex-plugin.js';
15
16
  import { inspectProjectEnvironment } from './index/environment-files.js';
16
17
  import { inspectProjectStackSection } from './index/stack-section-inspection.js';
17
18
  import { listStackCatalogPieces } from './index/stack-catalog.js';
18
19
  import { addStackPieces, readStack } from './index/stack.js';
20
+ import { uniqueSorted } from './index/utils.js';
19
21
  import { verifyProject } from './index/verification.js';
20
22
 
21
23
  export { GENESIS_CONTRACTS };
22
24
 
23
25
  function withIndexResult(result, index) {
24
- const changedFiles = [...new Set([...result.changedFiles, ...index.changedFiles])].sort();
26
+ const changedFiles = uniqueSorted([...result.changedFiles, ...index.changedFiles]);
25
27
  const refreshedOnly = result.status === 'unchanged' && index.changedFiles.length > 0;
26
28
  return {
27
29
  ...result,
@@ -42,6 +44,20 @@ export function initialize({ projectRoot = process.cwd(), stackPackages = [] } =
42
44
  return initializeWithIndex(projectRoot, stackPackages);
43
45
  }
44
46
 
47
+ export async function migrate({ projectRoot = process.cwd(), stackPackages = [] } = {}) {
48
+ const migrated = await migrateProject({ projectRoot, stackPackages });
49
+ const index = await buildProjectIndex({ projectRoot, stackPackages });
50
+ const result = withIndexResult(migrated, index);
51
+ const checked = await checkProject({ projectRoot, stackPackages });
52
+ return {
53
+ ...result,
54
+ projectFormat: checked.projectFormat,
55
+ check: checked,
56
+ diagnostics: [...result.diagnostics, ...checked.diagnostics],
57
+ guidance: checked.guidance,
58
+ };
59
+ }
60
+
45
61
  export async function adoptProject({
46
62
  projectRoot = process.cwd(),
47
63
  request = '',