genesis-compiler 1.7.8 → 1.8.0

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.
@@ -6,7 +6,7 @@ import { GenesisError } from './errors.js';
6
6
  import { isProjectContentPath, PROGRAM_ROOT } from './paths.js';
7
7
  import { normalizeRelative } from './utils.js';
8
8
 
9
- const SOURCE_LINE = /^- `([^`]+)`$/u;
9
+ const SOURCE_LINE = /^- (Main: )?`([^`]+)`$/u;
10
10
  const CONCEPT_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
11
11
 
12
12
  async function markdownFiles(projectRoot) {
@@ -90,7 +90,16 @@ function moduleContents(source, programPath) {
90
90
  const sourceLines = lines
91
91
  .slice(sourcesHeading[0].index + 1, contractHeading[0].index)
92
92
  .filter((line) => line.trim());
93
- const sources = sourceLines.map((line) => line.match(SOURCE_LINE)?.[1]);
93
+ const sourceMatches = sourceLines.map((line) => line.match(SOURCE_LINE));
94
+ const sources = sourceMatches.map((match) => match?.[2]);
95
+ const mainSources = sourceMatches.filter((match) => match?.[1]).map((match) => match[2]);
96
+ if (mainSources.length !== 1) {
97
+ throw new GenesisError(
98
+ 'PROGRAM_MAIN_REQUIRED',
99
+ `Program module needs exactly one Main source bullet (- Main: \`path/to/source\`): ${programPath}. Choose the reading anchor in its owning subsystem.`,
100
+ { path: programPath },
101
+ );
102
+ }
94
103
  const nextHeading = headings.find(({ index }) => index > contractHeading[0].index);
95
104
  const contractLines = lines
96
105
  .slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length);
@@ -128,6 +137,7 @@ function moduleContents(source, programPath) {
128
137
  title,
129
138
  description,
130
139
  sources: sources.map((sourcePath) => normalizeRelative(sourcePath)),
140
+ mainSource: normalizeRelative(mainSources[0]),
131
141
  publicContract: contractLines.join('\n').trim(),
132
142
  implementationMap,
133
143
  };
@@ -144,6 +154,10 @@ export async function inspectProgram(projectRoot) {
144
154
  return { status: subsystemMap.status === 'valid' ? 'valid' : 'missing', files: [], modules: [], subsystems: subsystemMap.subsystems.map(({ id }) => id), subsystemMap };
145
155
  }
146
156
 
157
+ if (subsystemMap.status !== 'valid') {
158
+ throw new GenesisError('SUBSYSTEMS_REQUIRED', 'Program needs an authored directory-bound subsystem map. Assign every module once in genesis/subsystems.md.');
159
+ }
160
+
147
161
  const modules = [];
148
162
  for (const programPath of files) {
149
163
  const source = await readFile(path.join(projectRoot, programPath), 'utf8');
@@ -154,7 +168,7 @@ export async function inspectProgram(projectRoot) {
154
168
  }
155
169
  const identity = moduleIdentity(programPath);
156
170
  const contents = moduleContents(source, programPath);
157
- if (subsystemMap.status === 'valid' && !assignments.has(programPath)) throw new GenesisError('SUBSYSTEMS_PROGRAM_UNASSIGNED', `Program module has no declared subsystem: ${programPath}.`);
171
+ if (!assignments.has(programPath)) throw new GenesisError('SUBSYSTEMS_PROGRAM_UNASSIGNED', `Program module has no declared subsystem: ${programPath}.`);
158
172
  const { sources } = contents;
159
173
  for (const sourcePath of sources) {
160
174
  if (!sourcePath || !isProjectContentPath(sourcePath) || !await sourceExists(projectRoot, sourcePath)) {
@@ -165,10 +179,21 @@ export async function inspectProgram(projectRoot) {
165
179
  );
166
180
  }
167
181
  }
182
+ const mainOwner = subsystemMap.subsystems
183
+ .filter((entry) => entry.sourceRoot === '.' || contents.mainSource.startsWith(`${entry.sourceRoot}/`))
184
+ .sort((left, right) => (right.sourceRoot === '.' ? 0 : right.sourceRoot.length) - (left.sourceRoot === '.' ? 0 : left.sourceRoot.length))[0];
185
+ if (mainOwner?.id !== assignments.get(programPath)) {
186
+ throw new GenesisError(
187
+ 'PROGRAM_MAIN_OWNER',
188
+ `Program main source must belong to its owning subsystem: ${contents.mainSource} in ${programPath}. Choose an owned source or correct the Program assignment.`,
189
+ { path: programPath, mainSource: contents.mainSource, subsystem: assignments.get(programPath) },
190
+ );
191
+ }
168
192
  modules.push({
169
193
  path: programPath,
170
194
  name: identity.name,
171
195
  sources,
196
+ mainSource: contents.mainSource,
172
197
  subsystem: assignments.get(programPath) ?? null,
173
198
  title: contents.title,
174
199
  description: contents.description,
@@ -46,7 +46,32 @@ export async function inspectProjectContent(projectRoot) {
46
46
  const paths = [...new Set([visible, ignored].flatMap(({ stdout }) => stdout.toString('utf8').split('\0').filter(Boolean)))]
47
47
  .filter((file) => !absent.has(file) && file !== '.git' && !file.startsWith('.git/'))
48
48
  .sort();
49
- const existingPaths = paths.filter((file) => !bootstrapPaths.has(file) && !file.split('/').includes('node_modules'));
49
+ // OpenCode installs its plugin dependency beside the generated guidance plugin.
50
+ // Recognize only its exact ignored scaffolding, never an authored config tree.
51
+ const opencodePaths = ['.opencode/.gitignore', '.opencode/package.json', '.opencode/package-lock.json'];
52
+ const ignoredPaths = new Set(ignored.stdout.toString('utf8').split('\0').filter(Boolean));
53
+ let opencodeBootstrap = [];
54
+ if (opencodePaths.every((file) => ignoredPaths.has(file))) {
55
+ try {
56
+ const [ignoreSource, packageSource, lockSource] = await Promise.all(
57
+ opencodePaths.map((file) => readFile(path.join(projectRoot, file), 'utf8')),
58
+ );
59
+ const manifest = JSON.parse(packageSource);
60
+ const lock = JSON.parse(lockSource);
61
+ const dependencies = manifest?.dependencies;
62
+ if (manifest && Object.keys(manifest).length === 1 && dependencies && typeof dependencies === 'object' &&
63
+ Object.keys(dependencies).length === 1 && typeof dependencies['@opencode-ai/plugin'] === 'string' &&
64
+ ignoreSource === 'node_modules\npackage.json\npackage-lock.json\nbun.lock\n.gitignore' &&
65
+ lock?.name === '.opencode' && lock.lockfileVersion === 3 &&
66
+ JSON.stringify(lock.packages?.['']?.dependencies) === JSON.stringify(dependencies)) {
67
+ opencodeBootstrap = opencodePaths;
68
+ }
69
+ } catch (error) {
70
+ if (!(error instanceof SyntaxError) && !['ENOENT', 'ENOTDIR'].includes(error.code)) throw error;
71
+ }
72
+ }
73
+ const existingPaths = paths.filter((file) => !bootstrapPaths.has(file) &&
74
+ !opencodeBootstrap.includes(file) && !file.split('/').includes('node_modules'));
50
75
  return {
51
76
  kind: existingPaths.length ? 'existing' : paths.length ? 'bootstrap' : 'empty',
52
77
  existingPaths,
@@ -150,7 +150,7 @@ export async function projectSessionContext({
150
150
  ...(skills.diagnostics.length > 0 ? ['- Skill inspection is read-only. Synchronization is an explicit source change; follow the current task authorization and host write boundary before running it.'] : []),
151
151
  '- After locating source, run `genesis context <path...>`; before adding a helper or public operation, run `genesis index <name-or-path...>` and reuse an existing owner.',
152
152
  '- If genesis/subsystems.md is missing or empty in an existing explained application, author it from source, schema and existing Program before implementation. This is ordinary explanatory adoption, not a startup blocker; do not run tests or application commands merely to create the map.',
153
- '- Read genesis/subsystems.md for declared subsystem responsibilities, Program membership, and data ownership/use. Maintain it in the same implementation turn when those facts change; reuse existing boundaries, verify source/schema, and never infer membership from folders.',
153
+ '- Read genesis/subsystems.md for declared subsystem responsibilities, Program membership, and data ownership/use. Maintain it in the same implementation turn when those facts change; reuse existing boundaries, verify source/schema, and require one Source Directory per subsystem and assign each Program module once; Program folder names do not assign ownership.',
154
154
  '- Program is fallible explanation; code, tests, and runtime behavior remain evidence.',
155
155
  '- Keep Blueprint and affected Program explanations aligned with intentional observable product behavior in the same implementation turn. Private restructuring may need only source citations or no explanatory change.',
156
156
  '- Before reporting completion, compare the requested observable behavior, required inputs and resources, declared project operations, and focused evidence with what actually exists. State anything not proven.',
@@ -0,0 +1,31 @@
1
+ // Optional, read-only interpretation. The consumer owns source selection,
2
+ // revision checks, Helper execution and caching; this is not Program material.
3
+ export const SOURCE_EXPLANATION_PROMPT_VERSION = 'informational-source-v1';
4
+
5
+ export function sourceExplanationGuidance({ presentation = 'description', scope = 'file' } = {}) {
6
+ if (!['description', 'steps'].includes(presentation) || !['file', 'selection'].includes(scope)) {
7
+ throw new TypeError('Choose description or steps for a file or declaration.');
8
+ }
9
+ return [
10
+ 'Create an informational interpretation of the supplied source, not a design contract or instructions for a coding agent.',
11
+ 'Use only the bounded source context. Do not use tools, inspect repositories, access the network or edit files. State uncertainty and distinguish observed behavior from inferred intent.',
12
+ scope === 'file'
13
+ ? 'Explain the file as a unit: responsibility, major operations and how its parts interact. Do not list imports or paraphrase every line.'
14
+ : 'Explain this declaration: what callers ask it to do, inputs, result, effects, dependencies and meaningful failure conditions. Exports can be internal to a subsystem; do not invent API intent.',
15
+ 'Start with one concrete overview sentence of at most 220 characters, suitable for a signature list. Name the purpose and outcome, not language syntax.',
16
+ presentation === 'description'
17
+ ? 'Then write a short plain-English summary of behavior, data flow and constraints. Do not emit pseudocode or pseudo-variables.'
18
+ : 'Then write concise numbered steps in English as pseudocode: meaningful data names, decisions, ordering, effects and failures. Do not reproduce language-specific source syntax.',
19
+ 'Components, callable operations, values and types need explanations appropriate to what they provide. Do not invent return types, callers or effects absent from the evidence.',
20
+ 'A consumer must compare actual source content and this prompt revision before reuse. Changed source invalidates the interpretation even if an older explanation sounds plausible. Authored architectural intent is reviewed separately.'
21
+ ].join('\n');
22
+ }
23
+
24
+ export function sourceExplanationReviewGuidance() {
25
+ return [
26
+ 'Review an optional informational source explanation against supplied current source evidence.',
27
+ 'Identify claims no longer supported, missing effects or failure conditions, and inferred intent presented as fact. Source or prompt changes invalidate cached freshness; do not certify reuse from prose similarity.',
28
+ 'Recommend regeneration when evidence changed. If source is incomplete, report the limit explicitly. Changes to intended responsibility or API boundaries require a separate human-reviewable design proposal.',
29
+ 'Do not edit source, create mandatory documentation, promote interpretations into Program or supply them as coding-agent context.'
30
+ ].join('\n');
31
+ }
@@ -0,0 +1,25 @@
1
+ import path from 'node:path';
2
+ import { GenesisError } from './errors.js';
3
+
4
+ export function parsePackageDiscovery(lines) {
5
+ if (lines === undefined) return { status: 'unconfigured', discoveries: [] };
6
+ if (lines.join('\n').trim() === '- Nothing.') return { status: 'configured', discoveries: [] };
7
+ const discoveries = []; let current;
8
+ const invalid = message => { throw new GenesisError('PACKAGE_DISCOVERY_INVALID', message); };
9
+ for (const raw of lines) {
10
+ const line = raw.trim(); if (!line) continue;
11
+ const heading = line.match(/^### Discovery `([a-z0-9]+(?:-[a-z0-9]+)*)`: (.+)$/u);
12
+ if (heading) { current = { id: heading[1], title: heading[2], manifests: [] }; discoveries.push(current); continue; }
13
+ const field = line.match(/^- (Adapter|Manifest|Package root): `([^`]+)`$/u);
14
+ if (!current || !field) invalid(`Invalid Package discovery entry: ${line}`);
15
+ const [, name, value] = field;
16
+ if (name === 'Adapter') { if (current.adapter || !/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u.test(value)) invalid('Invalid or duplicate package adapter.'); current.adapter = value; }
17
+ else {
18
+ if (!value || path.posix.normalize(value) !== value || path.posix.isAbsolute(value) || /[\\\0]/u.test(value) || value.split('/').includes('..')) invalid('Package discovery paths must be canonical project paths.');
19
+ if (name === 'Manifest') { if (value === '.' || current.manifests.includes(value)) invalid('Invalid or duplicate package manifest.'); current.manifests.push(value); }
20
+ else { if (value === '.' || current.packageRoot) invalid('Package root must be a distinct dependency directory.'); current.packageRoot = value; }
21
+ }
22
+ }
23
+ if (!discoveries.length || new Set(discoveries.map(entry => entry.id)).size !== discoveries.length || discoveries.some(entry => !entry.adapter || !entry.manifests.length || !entry.packageRoot)) invalid('Each unique package discovery needs Adapter, Manifest and Package root.');
24
+ return { status: 'configured', discoveries };
25
+ }
@@ -0,0 +1,38 @@
1
+ import { GenesisError } from './errors.js';
2
+
3
+ export function parseUiDiscovery(lines, { path = 'genesis/stack.md' } = {}) {
4
+ if (lines === undefined) return { status: 'unconfigured', discoveries: [] };
5
+ if (lines.join('\n').trim() === '- Nothing.') return { status: 'configured', discoveries: [] };
6
+ const discoveries = [];
7
+ let current;
8
+ const invalid = message => { throw new GenesisError('UI_DISCOVERY_INVALID', `${path}: ${message}`); };
9
+ for (const line of lines) {
10
+ if (!line.trim()) continue;
11
+ const heading = line.match(/^### Discovery `([a-z0-9]+(?:-[a-z0-9]+)*)`: (\S.*)$/u);
12
+ if (heading) {
13
+ current = { id: heading[1], title: heading[2], pageRoots: [], registrations: [], componentRoots: [] };
14
+ discoveries.push(current);
15
+ continue;
16
+ }
17
+ const field = line.match(/^- (Adapter|Surface|Mode|Page root|Registration|Component root): `([^`]+)`$/u);
18
+ if (!current || !field) invalid(`Invalid UI discovery entry: ${line}`);
19
+ const [, name, value] = field;
20
+ const key = { Adapter: 'adapter', Surface: 'surface', Mode: 'mode', 'Page root': 'pageRoots', Registration: 'registrations', 'Component root': 'componentRoots' }[name];
21
+ if (Array.isArray(current[key])) {
22
+ if (value.includes('\0') || value.includes('\\') || value.startsWith('/') || (value !== '.' && value.split('/').some(part => !part || part === '.' || part === '..'))) invalid(`Invalid ${name} path: ${value}`);
23
+ if (current[key].includes(value)) invalid(`Duplicate ${name}: ${value}`);
24
+ current[key].push(value);
25
+ } else {
26
+ if (Object.hasOwn(current, key)) invalid(`Duplicate ${name}.`);
27
+ if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u.test(value)) invalid(`Invalid ${name}: ${value}`);
28
+ current[key] = value;
29
+ }
30
+ }
31
+ if (!discoveries.length || new Set(discoveries.map(entry => entry.id)).size !== discoveries.length) invalid('Supply unique discovery entries or - Nothing.');
32
+ for (const entry of discoveries) {
33
+ if (!entry.adapter || !entry.surface || !['registered', 'filesystem'].includes(entry.mode)) invalid(`${entry.id} needs Adapter, Surface and registered/filesystem Mode.`);
34
+ if (entry.mode === 'registered' && !entry.registrations.length) invalid(`${entry.id} needs Registration.`);
35
+ if (entry.mode === 'filesystem' && !entry.pageRoots.length) invalid(`${entry.id} needs Page root.`);
36
+ }
37
+ return { status: 'configured', discoveries };
38
+ }
@@ -8,6 +8,8 @@ import {
8
8
  readStackCatalog,
9
9
  } from './stack-catalog.js';
10
10
  import { composeStackCityPresentation } from './stack-city-presentation.js';
11
+ import { parseUiDiscovery } from './stack-ui-discovery.js';
12
+ import { parsePackageDiscovery } from './stack-package-discovery.js';
11
13
  import { resolveStackPieces } from './stack-composition.js';
12
14
  import {
13
15
  parseStackEnvironmentDefaultLines,
@@ -358,6 +360,8 @@ async function readStackWithContractMode(projectRoot, {
358
360
  source: 'project',
359
361
  diagnostics: [],
360
362
  }));
363
+ const uiDiscovery = parseUiDiscovery(extensions.find(entry => entry.name === 'UI discovery')?.lines);
364
+ const packageDiscovery = parsePackageDiscovery(extensions.find(entry => entry.name === 'Package discovery')?.lines);
361
365
  return {
362
366
  path: STACK_PATH,
363
367
  identityHash: sha256(stableJson({
@@ -375,6 +379,8 @@ async function readStackWithContractMode(projectRoot, {
375
379
  stackPackages: declaredPackages,
376
380
  cityExclusions: cityPresentation.exclusions,
377
381
  cityRegions: cityPresentation.regions,
382
+ uiDiscovery,
383
+ packageDiscovery,
378
384
  verificationCommands,
379
385
  environmentDefaults,
380
386
  environmentFiles,
@@ -26,7 +26,8 @@ export async function inspectSubsystems({ projectRoot = process.cwd() } = {}) {
26
26
  const sections = new Set();
27
27
  const finish = () => {
28
28
  if (!current) return;
29
- if (!current.description.trim() || sections.size !== 3) invalid(`Subsystem ${current.id} needs a responsibility and Program, Data owned, Data used sections.`);
29
+ if (!current.description.trim() || !['source', 'program', 'dataOwned', 'dataUsed'].every(key => sections.has(key))) invalid(`Subsystem ${current.id} needs a responsibility and Source, Program, Data owned, Data used sections.`);
30
+ if (current.sourceRoot === undefined) invalid(`Subsystem ${current.id} Source requires one Directory.`);
30
31
  current.description = current.description.trim();
31
32
  subsystems.push(current);
32
33
  };
@@ -46,15 +47,22 @@ export async function inspectSubsystems({ projectRoot = process.cwd() } = {}) {
46
47
  continue;
47
48
  }
48
49
  if (!current) invalid('Expected a subsystem heading or - Nothing.');
49
- const sectionHeading = line.match(/^### (Program|Data owned|Data used)$/u);
50
+ const sectionHeading = line.match(/^### (Source|Program|Data owned|Data used)$/u);
50
51
  if (sectionHeading) {
51
- section = { Program: 'program', 'Data owned': 'dataOwned', 'Data used': 'dataUsed' }[sectionHeading[1]];
52
+ section = { Source: 'source', Program: 'program', 'Data owned': 'dataOwned', 'Data used': 'dataUsed' }[sectionHeading[1]];
52
53
  if (sections.has(section)) invalid(`Duplicate ${sectionHeading[1]} section.`);
53
54
  sections.add(section);
54
55
  continue;
55
56
  }
56
57
  if (line.startsWith('#')) invalid(`Unexpected heading: ${line}`);
57
58
  if (!section) { current.description += `${line}\n`; continue; }
59
+ if (section === 'source') {
60
+ const match = line.match(/^- Directory `([^`]+)`$/u);
61
+ const directory = match?.[1];
62
+ if (!directory || current.sourceRoot !== undefined || directory.includes('\0') || directory.includes('\\') || directory.startsWith('/') || (directory !== '.' && directory.split('/').some(part => !part || part === '.' || part === '..'))) invalid('Source requires exactly one canonical project-relative Directory.');
63
+ current.sourceRoot = directory;
64
+ continue;
65
+ }
58
66
  if (line === '- Nothing.') {
59
67
  if (current[section].length || current[`${section}Empty`]) invalid('Nothing must be the only section entry.');
60
68
  current[`${section}Empty`] = true;
@@ -75,9 +83,14 @@ export async function inspectSubsystems({ projectRoot = process.cwd() } = {}) {
75
83
  const ids = new Set();
76
84
  const programs = new Set();
77
85
  const owners = new Map();
86
+ const roots = new Set();
78
87
  for (const subsystem of subsystems) {
79
88
  if (ids.has(subsystem.id)) invalid(`Duplicate subsystem ${subsystem.id}.`);
80
89
  ids.add(subsystem.id);
90
+ if (subsystem.sourceRoot !== undefined) {
91
+ if (roots.has(subsystem.sourceRoot)) invalid(`Directory has multiple subsystem owners: ${subsystem.sourceRoot}.`);
92
+ roots.add(subsystem.sourceRoot);
93
+ }
81
94
  for (const key of ['program', 'dataOwned', 'dataUsed']) {
82
95
  if (!subsystem[key].length && !subsystem[`${key}Empty`]) invalid(`${subsystem.id}: empty sections must say - Nothing.`);
83
96
  delete subsystem[`${key}Empty`];
@@ -21,8 +21,8 @@ function stable(value) {
21
21
  return value;
22
22
  }
23
23
 
24
- export function stableJson(value) {
25
- return `${JSON.stringify(stable(value), null, 2)}\n`;
24
+ export function stableJson(value, { space = 2 } = {}) {
25
+ return `${JSON.stringify(stable(value), null, space)}\n`;
26
26
  }
27
27
 
28
28
  export function uniqueSorted(values = []) {
package/src/index.js CHANGED
@@ -31,6 +31,7 @@ import { projectSessionContext, projectTurnContext } from './index/session-conte
31
31
  import { withTrustedGitRepository } from './index/process.js';
32
32
 
33
33
  export { inspectProject };
34
+ export { SOURCE_EXPLANATION_PROMPT_VERSION, sourceExplanationGuidance, sourceExplanationReviewGuidance } from './index/source-explanation.js';
34
35
  export { inspectClaudeHooks, installClaudeHooks as syncClaudeHooks } from './index/claude-hooks.js';
35
36
  export { inspectAgentIntegrations, syncAgentIntegrations } from './index/agent-integrations.js';
36
37
  export { listParsers, installParsers, verifyParsers, parserEnvironment } from './index/code-indexers/parsers.js';
@@ -188,8 +189,10 @@ export function indexCodebase({
188
189
  queries = [],
189
190
  stackPackages = [],
190
191
  write = true,
192
+ observations = null,
193
+ signal,
191
194
  } = {}) {
192
- return buildProjectIndex({ projectRoot, queries, stackPackages, write });
195
+ return buildProjectIndex({ projectRoot, queries, stackPackages, write, observations, signal });
193
196
  }
194
197
 
195
198
  export function inspectDerivedArtifacts() {
@@ -229,3 +232,7 @@ export async function syncSkills(options) {
229
232
 
230
233
  export { inspectSubsystems } from './index/subsystems.js';
231
234
  export { inspectOpenCodePlugin, installOpenCodePlugin as syncOpenCodePlugin } from './index/opencode-plugin.js';
235
+
236
+ export { ARCHITECTURE_OBSERVATIONS_SCHEMA, validateArchitectureObservations, connectArchitectureObservations, mergeArchitectureObservations } from './index/architecture-observations.js';
237
+
238
+ export { validateArchitectureFacts } from './index/architecture.js';