genesis-compiler 1.2.9 → 1.2.12

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
@@ -122,9 +122,12 @@ Genesis does not install substitute generic `nodejs`, `vue`, `php`, or similar
122
122
  skills. Official, user, or host skills retain their normal names. A Stack piece
123
123
  may instead name one authoritative technology skill; Genesis copies that
124
124
  complete directory into `.agents/skills/`, including its `references/`,
125
- `scripts/`, `assets/`, and agent metadata. The agent loads those resources only
126
- when the skill requires them. For example, the `genesis-stack` catalog's JSKIT
127
- piece installs the JSKIT package's own `jskit` skill. A piece's optional
125
+ `scripts/`, `assets/`, and agent metadata. Every relative Markdown link and image
126
+ must resolve within that same Skill directory after relocation; a missing
127
+ target or path that escapes the Skill root makes the Skill invalid. The agent
128
+ loads valid resources only when the Skill requires them. For example, the
129
+ `genesis-stack` catalog's JSKIT piece installs the JSKIT package's own `jskit`
130
+ skill. A piece's optional
128
131
  `## Guidance` is concise
129
132
  supplemental project-work guidance; it does not create or replace a generic
130
133
  technology skill.
@@ -510,7 +513,7 @@ executing or provisioning it.
510
513
 
511
514
  Normalized operational results identify their stable public contract in the
512
515
  `contract` field: `genesis.workspace-setup.v1`, `genesis.environment.v1`,
513
- `genesis.launch.v1`, `genesis.deployment.v1`, or
516
+ `genesis.launch.v1`, `genesis.deployment.v2`, or
514
517
  `genesis.verification.v1`. Hosts validate that identity instead of
515
518
  feature-detecting individual fields.
516
519
 
@@ -126,7 +126,10 @@ selects one complete [Agent Skills](https://agentskills.io) directory, either
126
126
  from Genesis or from the declared npm package. Its `SKILL.md`, `references/`,
127
127
  `scripts/`, `assets/`, and `agents/` metadata are copied together to
128
128
  `.agents/skills/<skill-name>/`; the agent loads them progressively instead of
129
- Genesis expanding every manual into every prompt. `Resources` declares generic
129
+ Genesis expanding every manual into every prompt. Relative Markdown links and
130
+ images must resolve within that copied Skill root. Missing targets and paths
131
+ that escape the root are invalid, even if an escaped path happens to exist in
132
+ the source package or project. `Resources` declares generic
130
133
  alternative environment-name sets reported by prompt generation and checked
131
134
  before verification. `allowEmpty` may name a required variable whose empty
132
135
  string is valid. `Environment defaults` declares public non-secret constants
@@ -257,6 +260,7 @@ complete form is:
257
260
 
258
261
  - Workdir: `.`
259
262
  - Runtimes: `nodejs`
263
+ - Recreate on restore: `node_modules`
260
264
  - Ready when: `GET` `/api/health` returns `200`
261
265
  - Prepare `Install production dependencies`: `npm` `install` `--omit=dev`
262
266
  - Build `Build`: `npm` `run` `build`
@@ -270,6 +274,19 @@ Prepare, Build, and Migrate steps are optional and retain their declared order.
270
274
  There is exactly one final Serve step and one exact HTTP readiness predicate
271
275
  whose successful status is from 200 through 399. Runtime ids remain abstract.
272
276
 
277
+ `Recreate on restore` lists project-relative paths that a host may remove when
278
+ retaining an inactive release. Every listed path must be completely recreated
279
+ by the Deployment Prepare steps, which a host runs after restoring the release
280
+ and before Serve. Declaring these paths without a Prepare step is invalid.
281
+ An entry without `/` matches that path segment at every depth, so
282
+ `node_modules` covers root and nested workspace dependency trees. `*` stays
283
+ within one segment and `**` may cross path separators. A matched directory
284
+ includes its complete subtree.
285
+ This is application and Stack knowledge: for example, JSKIT declares
286
+ `node_modules`, while compiled output such as `dist` remains part of the
287
+ artifact because Prepare does not rebuild it. Genesis normalizes the paths but
288
+ does not remove, archive, restore, or reconstruct anything itself.
289
+
273
290
  A project `## Deployment` section replaces every component recipe as one unit,
274
291
  including with `- Nothing.`. Without a project section, exactly one selected
275
292
  component recipe may apply; several are blocked as ambiguous and never merged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.9",
3
+ "version": "1.2.12",
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": {
@@ -57,6 +57,7 @@
57
57
  "@ast-grep/lang-ruby": "^0.0.7",
58
58
  "@ast-grep/lang-rust": "^0.0.7",
59
59
  "@ast-grep/napi": "^0.45.1",
60
+ "mdast-util-from-markdown": "^2.0.3",
60
61
  "yaml": "^2.9.0"
61
62
  }
62
63
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.9",
3
+ "version": "1.2.12",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -12,9 +12,10 @@ import {
12
12
  import path from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
 
15
+ import { fromMarkdown } from 'mdast-util-from-markdown';
15
16
  import { parse as parseYaml } from 'yaml';
16
17
 
17
- import { GenesisError } from './errors.js';
18
+ import { asDiagnostic, GenesisError } from './errors.js';
18
19
  import { sha256, stableJson, writeFileAtomic } from './utils.js';
19
20
 
20
21
  const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
@@ -76,6 +77,90 @@ function frontmatter(source, location) {
76
77
  return metadata;
77
78
  }
78
79
 
80
+ function markdownDestinations(tree) {
81
+ const destinations = [];
82
+ const nodes = [tree];
83
+ while (nodes.length > 0) {
84
+ const node = nodes.pop();
85
+ if (['definition', 'image', 'link'].includes(node?.type) && typeof node.url === 'string') {
86
+ destinations.push({ line: node.position?.start?.line, url: node.url });
87
+ }
88
+ if (Array.isArray(node?.children)) nodes.push(...node.children);
89
+ }
90
+ return destinations.sort((left, right) => (left.line || 0) - (right.line || 0));
91
+ }
92
+
93
+ function localReferencePath(url) {
94
+ const value = String(url || '').trim();
95
+ if (
96
+ !value
97
+ || value.startsWith('#')
98
+ || value.startsWith('?')
99
+ || value.startsWith('/')
100
+ || /^[a-z][a-z\d+.-]*:/iu.test(value)
101
+ ) return null;
102
+ const separator = value.search(/[?#]/u);
103
+ const encodedPath = separator === -1 ? value : value.slice(0, separator);
104
+ if (!encodedPath) return null;
105
+ try {
106
+ return { path: decodeURIComponent(encodedPath) };
107
+ } catch {
108
+ return { error: 'malformed', path: encodedPath };
109
+ }
110
+ }
111
+
112
+ function outsideDirectory(directory, target) {
113
+ const relative = path.relative(directory, target);
114
+ return relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
115
+ }
116
+
117
+ async function validateMarkdownReferences(directory, files) {
118
+ const root = path.resolve(directory);
119
+ const problems = [];
120
+ for (const file of files.filter(({ path: filePath }) => filePath.endsWith('.md'))) {
121
+ const location = path.join(root, file.path);
122
+ let tree;
123
+ try {
124
+ tree = fromMarkdown(await readFile(location, 'utf8'));
125
+ } catch (error) {
126
+ invalidSkill(`Agent Skill Markdown is invalid: ${location}.`, {
127
+ cause: error.message,
128
+ path: file.path,
129
+ });
130
+ }
131
+ for (const destination of markdownDestinations(tree)) {
132
+ const local = localReferencePath(destination.url);
133
+ if (!local) continue;
134
+ const problem = {
135
+ path: file.path,
136
+ target: destination.url,
137
+ ...(destination.line ? { line: destination.line } : {}),
138
+ };
139
+ if (local.error) {
140
+ problems.push({ ...problem, reason: local.error });
141
+ continue;
142
+ }
143
+ const target = path.resolve(path.dirname(location), local.path);
144
+ if (outsideDirectory(root, target)) {
145
+ problems.push({ ...problem, reason: 'outside-skill' });
146
+ continue;
147
+ }
148
+ try {
149
+ await lstat(target);
150
+ } catch (error) {
151
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
152
+ problems.push({ ...problem, reason: 'missing' });
153
+ }
154
+ }
155
+ }
156
+ if (problems.length > 0) {
157
+ invalidSkill(
158
+ `Agent Skill contains ${problems.length} unresolved or non-portable local Markdown reference${problems.length === 1 ? '' : 's'}: ${directory}.`,
159
+ { references: problems },
160
+ );
161
+ }
162
+ }
163
+
79
164
  async function skillTree(directory, relative = '') {
80
165
  const entries = await readdir(path.join(directory, relative), { withFileTypes: true });
81
166
  const files = [];
@@ -146,6 +231,7 @@ async function describeSkill(directory, source) {
146
231
  )
147
232
  ) invalidSkill(`Agent Skill metadata must map strings to strings: ${skillFile}.`);
148
233
  const files = await skillTree(directory);
234
+ await validateMarkdownReferences(directory, files);
149
235
  return {
150
236
  name,
151
237
  description,
@@ -395,7 +481,7 @@ export async function inspectProjectSkills({ projectRoot, stack } = {}) {
395
481
  let installed;
396
482
  try { installed = await describeSkill(target, `project:${expected.name}`); } catch (error) {
397
483
  invalid = true;
398
- diagnostics.push({ code: error.code || 'AGENT_SKILL_INVALID', message: error.message });
484
+ diagnostics.push(asDiagnostic(error));
399
485
  continue;
400
486
  }
401
487
  if (!installed) {
@@ -15,6 +15,19 @@ import { sha256, stableJson, writeFileAtomic } from './utils.js';
15
15
  export const MACHINE_CITY_PATH = '.genesis/machine-city.json';
16
16
  export const PROGRAM_CITY_PATH = '.genesis/program-city.json';
17
17
 
18
+ export const GENESIS_DERIVED_ARTIFACTS = Object.freeze([
19
+ Object.freeze({
20
+ id: 'machine-city',
21
+ path: MACHINE_CITY_PATH,
22
+ recreator: 'index-codebase',
23
+ }),
24
+ Object.freeze({
25
+ id: 'program-city',
26
+ path: PROGRAM_CITY_PATH,
27
+ recreator: 'index-codebase',
28
+ }),
29
+ ]);
30
+
18
31
  const INDEXERS = astGrepCodeIndexers;
19
32
 
20
33
  function directoryId(value) {
@@ -250,10 +250,19 @@ async function changedProjectPaths(projectRoot, before, after) {
250
250
  const dirtyChanges = [...new Set([...previous.keys(), ...current.keys()])].filter((file) => (
251
251
  stableJson(previous.get(file)) !== stableJson(current.get(file))
252
252
  ));
253
- return [...new Set([
253
+ const candidates = [...new Set([
254
254
  ...dirtyChanges,
255
255
  ...await committedPaths(projectRoot, before.head, after.head),
256
256
  ])].sort();
257
+ const changed = await Promise.all(candidates.map(async (file) => {
258
+ if (!previous.has(file)) return file;
259
+ // Moving pre-turn file bytes into HEAD is a save operation, not new implementation.
260
+ const finalState = current.has(file)
261
+ ? current.get(file)
262
+ : await pathState(path.join(projectRoot, file));
263
+ return stableJson(previous.get(file)) === stableJson(finalState) ? null : file;
264
+ }));
265
+ return changed.filter(Boolean);
257
266
  }
258
267
 
259
268
  function changedPathLines(changedPaths) {
@@ -1,5 +1,6 @@
1
1
  export const GENESIS_CONTRACTS = Object.freeze({
2
- deployment: 'genesis.deployment.v1',
2
+ deployment: 'genesis.deployment.v2',
3
+ derivedArtifacts: 'genesis.derived-artifacts.v1',
3
4
  environment: 'genesis.environment.v1',
4
5
  launch: 'genesis.launch.v1',
5
6
  verification: 'genesis.verification.v1',
@@ -13,6 +13,7 @@ export async function inspectProjectDeployment({ projectRoot, stackPackages = []
13
13
  else if (stack.deployment.steps.length > 0) status = 'ready';
14
14
  const recipe = {
15
15
  version: stack.deployment.version,
16
+ artifact: stack.deployment.artifact,
16
17
  workdir: stack.deployment.workdir,
17
18
  runtimeRequirements: stack.deployment.runtimeRequirements,
18
19
  readiness: stack.deployment.readiness,
@@ -7,6 +7,7 @@ import {
7
7
 
8
8
  const RUNTIME_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
9
9
  const STEP_LINE = /^- (Prepare|Build|Migrate|Serve) `([^`\r\n]+)`:[ \t]+(.+)$/u;
10
+ const RESTORE_PATHS_PREFIX = '- Recreate on restore: ';
10
11
  const STEP_ROLE_ORDER = Object.freeze({
11
12
  prepare: 0,
12
13
  build: 1,
@@ -45,6 +46,36 @@ function parseRuntimes(source, deploymentPath, line) {
45
46
  return values;
46
47
  }
47
48
 
49
+ function parseDisposablePaths(source, deploymentPath, line) {
50
+ const values = parseBacktickedArguments(source.slice(RESTORE_PATHS_PREFIX.length));
51
+ if (!values || values.length === 0) {
52
+ invalid(
53
+ deploymentPath,
54
+ 'Deployment Recreate on restore paths must be separate non-empty backticked values.',
55
+ line,
56
+ );
57
+ }
58
+ const normalized = values.map((value) => String(value || '').trim());
59
+ if (normalized.some((value) => (
60
+ !value
61
+ || value.startsWith('/')
62
+ || value.includes('\\')
63
+ || value.includes('***')
64
+ || /[?\[\]{}!]/u.test(value)
65
+ || value.split('/').some((segment) => !segment || ['.', '..'].includes(segment))
66
+ ))) {
67
+ invalid(
68
+ deploymentPath,
69
+ 'Deployment Recreate on restore paths must be canonical project-relative globs using only * and ** wildcards.',
70
+ line,
71
+ );
72
+ }
73
+ if (new Set(normalized).size !== normalized.length) {
74
+ invalid(deploymentPath, 'Deployment contains a duplicate Recreate on restore path.', line);
75
+ }
76
+ return normalized;
77
+ }
78
+
48
79
  function parseUrlPath(value, deploymentPath, line) {
49
80
  const normalized = String(value || '').trim();
50
81
  if (!normalized.startsWith('/') || normalized.startsWith('//') || /[\\?#]/u.test(normalized)) {
@@ -103,10 +134,15 @@ export function parseStackDeploymentLines(lines, {
103
134
  } = {}) {
104
135
  if (lines === undefined) return null;
105
136
  const entries = lines.map((line) => line.trim()).filter(Boolean);
106
- if (entries.length === 1 && entries[0] === '- Nothing.') return { version: 1, steps: [] };
137
+ if (entries.length === 1 && entries[0] === '- Nothing.') {
138
+ return { version: 2, artifact: { disposablePaths: [] }, steps: [] };
139
+ }
107
140
 
108
141
  const result = {
109
- version: 1,
142
+ version: 2,
143
+ artifact: {
144
+ disposablePaths: [],
145
+ },
110
146
  workdir: '.',
111
147
  runtimeRequirements: [],
112
148
  readiness: null,
@@ -133,6 +169,14 @@ export function parseStackDeploymentLines(lines, {
133
169
  result.runtimeRequirements = parseRuntimes(source, deploymentPath, line);
134
170
  continue;
135
171
  }
172
+ if (source.startsWith(RESTORE_PATHS_PREFIX)) {
173
+ if (seen.has('restore-paths')) {
174
+ invalid(deploymentPath, 'Duplicate Deployment Recreate on restore entry.', line);
175
+ }
176
+ seen.add('restore-paths');
177
+ result.artifact.disposablePaths = parseDisposablePaths(source, deploymentPath, line);
178
+ continue;
179
+ }
136
180
  if (source.startsWith('- Ready when: ')) {
137
181
  if (seen.has('readiness')) invalid(deploymentPath, 'Duplicate Deployment Ready when.', line);
138
182
  seen.add('readiness');
@@ -164,6 +208,15 @@ export function parseStackDeploymentLines(lines, {
164
208
  if (!result.readiness) {
165
209
  invalid(deploymentPath, 'Deployment needs one Ready when entry.');
166
210
  }
211
+ if (
212
+ result.artifact.disposablePaths.length > 0
213
+ && !result.steps.some(({ role }) => role === 'prepare')
214
+ ) {
215
+ invalid(
216
+ deploymentPath,
217
+ 'Deployment may recreate paths on restore only when it declares a Prepare step.',
218
+ );
219
+ }
167
220
  return result;
168
221
  }
169
222
 
@@ -177,7 +230,8 @@ export function composeStackDeployment(components, projectDeployment) {
177
230
  }));
178
231
  if (declarations.length === 0) {
179
232
  return {
180
- version: 1,
233
+ version: 2,
234
+ artifact: { disposablePaths: [] },
181
235
  workdir: '.',
182
236
  runtimeRequirements: [],
183
237
  readiness: null,
@@ -189,7 +243,8 @@ export function composeStackDeployment(components, projectDeployment) {
189
243
  if (declarations.length === 1) return { ...declarations[0], diagnostics: [] };
190
244
  const sources = declarations.map(({ source }) => source).sort();
191
245
  return {
192
- version: 1,
246
+ version: 2,
247
+ artifact: { disposablePaths: [] },
193
248
  workdir: '.',
194
249
  runtimeRequirements: [],
195
250
  readiness: null,
package/src/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import { checkProject } from './index/check.js';
4
- import { buildProjectIndex } from './index/code-index.js';
4
+ import { buildProjectIndex, GENESIS_DERIVED_ARTIFACTS } from './index/code-index.js';
5
+ import { GENESIS_CONTRACTS } from './index/contracts.js';
5
6
  import { contextForProjectPaths } from './index/context.js';
6
7
  import { generateProjectPrompt } from './index/prompt.js';
7
8
  import { initializeProject } from './index/init.js';
@@ -17,7 +18,7 @@ import {
17
18
  prepareProjectWorkspace,
18
19
  } from './index/workspace-setup.js';
19
20
 
20
- export { GENESIS_CONTRACTS } from './index/contracts.js';
21
+ export { GENESIS_CONTRACTS };
21
22
 
22
23
  function withIndexResult(result, index) {
23
24
  const changedFiles = [...new Set([...result.changedFiles, ...index.changedFiles])].sort();
@@ -131,6 +132,13 @@ export function indexCodebase({
131
132
  return buildProjectIndex({ projectRoot, queries, stackPackages, write });
132
133
  }
133
134
 
135
+ export function inspectDerivedArtifacts() {
136
+ return {
137
+ contract: GENESIS_CONTRACTS.derivedArtifacts,
138
+ artifacts: GENESIS_DERIVED_ARTIFACTS.map((artifact) => ({ ...artifact })),
139
+ };
140
+ }
141
+
134
142
  export function verify(options) {
135
143
  return verifyProject(options);
136
144
  }