genesis-compiler 1.7.8 → 1.8.1
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 +61 -9
- package/docs/assurance-model.md +4 -2
- package/docs/templates.md +11 -0
- package/package.json +5 -3
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/describe.txt +5 -0
- package/prompts/program.txt +9 -1
- package/skills/genesis-program/SKILL.md +41 -4
- package/skills/genesis-project/SKILL.md +2 -2
- package/src/index/architecture-observations.js +167 -0
- package/src/index/architecture.js +227 -0
- package/src/index/code-index.js +66 -6
- package/src/index/code-indexers/ast-grep.js +223 -19
- package/src/index/code-indexers/npm-packages.js +204 -0
- package/src/index/context.js +1 -1
- package/src/index/program.js +28 -3
- package/src/index/project-inspection.js +26 -1
- package/src/index/session-context.js +1 -1
- package/src/index/source-explanation.js +31 -0
- package/src/index/stack-package-discovery.js +25 -0
- package/src/index/stack-ui-discovery.js +38 -0
- package/src/index/stack.js +6 -0
- package/src/index/subsystems.js +16 -3
- package/src/index/utils.js +2 -2
- package/src/index.js +8 -1
package/src/index/program.js
CHANGED
|
@@ -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 = /^-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/index/stack.js
CHANGED
|
@@ -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,
|
package/src/index/subsystems.js
CHANGED
|
@@ -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() ||
|
|
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`];
|
package/src/index/utils.js
CHANGED
|
@@ -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,
|
|
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';
|