genesis-compiler 1.8.6 → 1.9.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 +7 -3
- package/docs/stack-components.md +27 -7
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/src/index/check.js +1 -7
- package/src/index/code-index.js +3 -2
- package/src/index/init.js +10 -4
- package/src/index/migration.js +8 -1
- package/src/index/stack-project-contracts.js +72 -11
- package/src/index/stack-section.js +50 -8
- package/src/index/stack.js +33 -3
- package/src/index.js +10 -3
package/README.md
CHANGED
|
@@ -860,8 +860,10 @@ genesis check
|
|
|
860
860
|
- Blueprint, engineering approach, and Stack validity;
|
|
861
861
|
- selected Agent Skill presence and structural validity;
|
|
862
862
|
- Program presence and structural validity;
|
|
863
|
-
- environment declarations
|
|
864
|
-
`
|
|
863
|
+
- environment declarations as `ready`, `missing-inputs`, or honestly
|
|
864
|
+
`unconfigured`, and consumer-owned Stack extensions as `ready` or
|
|
865
|
+
`unconfigured` (an undecided competing section makes the Stack invalid with
|
|
866
|
+
`STACK_SECTION_AMBIGUOUS` until the project authors it);
|
|
865
867
|
- missing Stack-declared environment inputs (without claiming the service is
|
|
866
868
|
reachable);
|
|
867
869
|
- verification evidence as `current`, `stale`, `missing`, `invalid`, or
|
|
@@ -1124,7 +1126,9 @@ publishing access to the selected package. In Vibe64, save it in the project's
|
|
|
1124
1126
|
**Env → Development**, enable **Secret**, and choose **Add**. Managed shell
|
|
1125
1127
|
commands receive the saved value; credentials do not belong in source or chat.
|
|
1126
1128
|
|
|
1127
|
-
Update the selected package's version and lockfile
|
|
1129
|
+
Update the selected package's version and lockfile (for the compiler, also the
|
|
1130
|
+
packaged Codex plugin version in `plugins/genesis/.codex-plugin/plugin.json`,
|
|
1131
|
+
which must equal `package.json`), run the relevant checks,
|
|
1128
1132
|
then commit and push the release source to `main`. Publish from a clean checkout
|
|
1129
1133
|
whose `HEAD` matches `origin`'s actual `main`, with development dependencies
|
|
1130
1134
|
installed through `npm ci`:
|
package/docs/stack-components.md
CHANGED
|
@@ -157,9 +157,20 @@ validate a private schema, map runtimes, execute commands, or prove success.
|
|
|
157
157
|
Proposal composition is intentionally small:
|
|
158
158
|
|
|
159
159
|
1. An existing project section remains authoritative and is never overwritten.
|
|
160
|
-
2.
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
2. A component's proposal supersedes the proposals of selected components it
|
|
161
|
+
requires, directly or transitively. Laravel's proposal therefore replaces
|
|
162
|
+
the PHP and Node proposals for the same heading.
|
|
163
|
+
3. Without a project section, exactly one remaining proposal is copied
|
|
164
|
+
unchanged.
|
|
165
|
+
4. Several remaining proposals from unrelated components are a project
|
|
166
|
+
decision. `genesis stack add` still succeeds, materializes every other
|
|
167
|
+
section, leaves that heading unwritten, and returns a
|
|
168
|
+
`STACK_SECTION_AMBIGUOUS` diagnostic whose details list each competing
|
|
169
|
+
source and its exact lines. Its preparation prompt quotes the proposals and
|
|
170
|
+
asks the agent to author one complete project section first. Current-format
|
|
171
|
+
reads, `genesis check`, inspection and context reject the Stack with the
|
|
172
|
+
same message until that section exists. Genesis never merges the bodies or
|
|
173
|
+
guesses a winner.
|
|
163
174
|
|
|
164
175
|
After selection every effective opaque section is project-owned. The component
|
|
165
176
|
catalog may improve later without silently changing the application's setup,
|
|
@@ -270,6 +281,9 @@ Project format 2 removes runtime inheritance of operation declarations from
|
|
|
270
281
|
the catalog. `genesis migrate` upgrades format 1 projects by composing the
|
|
271
282
|
previously effective declarations once and writing them into
|
|
272
283
|
`genesis/stack.md`. Existing project sections win exactly as they did before.
|
|
284
|
+
Competing proposals that no requirement resolves do not fail the migration:
|
|
285
|
+
it completes, leaves that heading unwritten, and returns the same
|
|
286
|
+
`STACK_SECTION_AMBIGUOUS` diagnostic and decision prompt as `genesis stack add`.
|
|
273
287
|
Current-format reads reject a selected component whose required contract was
|
|
274
288
|
not materialized, so a tool upgrade cannot quietly substitute a new proposal.
|
|
275
289
|
|
|
@@ -279,10 +293,16 @@ existing `genesis stack add` operation for an already-selected component. For
|
|
|
279
293
|
example, if `nodejs` is selected and the missing heading is
|
|
280
294
|
`## Connected development`, run `genesis stack add nodejs` and review its
|
|
281
295
|
preparation prompt. This adds the Node `npm` default without choosing connected
|
|
282
|
-
repositories or installing dependencies.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`
|
|
296
|
+
repositories or installing dependencies. Every package-ecosystem language piece
|
|
297
|
+
proposes its own default the same way (PHP `composer`, Python `pip`, Ruby
|
|
298
|
+
`bundler`, Go `gomod`, Rust `cargo`, Java `maven`, Kotlin `gradle`, C#
|
|
299
|
+
`nuget`); Laravel proposes `composer`, superseding the PHP and Node proposals it
|
|
300
|
+
requires. C/C++ and shell propose none. When several unrelated languages are
|
|
301
|
+
selected, the add reports the competing defaults and the project authors one
|
|
302
|
+
`## Connected development` section choosing its default. Existing complete
|
|
303
|
+
project sections remain authoritative; reconcile any proposed operation against
|
|
304
|
+
the real source. Use the project's pinned compiler invocation when it declares
|
|
305
|
+
`genesis-compiler`: `npm exec --no -- genesis stack add nodejs`.
|
|
286
306
|
|
|
287
307
|
Before rolling out a catalog that adds operation proposals, operators should
|
|
288
308
|
run read-only project inspection against representative existing projects with
|
package/package.json
CHANGED
package/src/index/check.js
CHANGED
|
@@ -124,12 +124,10 @@ export async function checkProject({
|
|
|
124
124
|
projectRoot: root,
|
|
125
125
|
stackPackages,
|
|
126
126
|
});
|
|
127
|
-
const extensionDiagnostics = stack.extensions.flatMap(({ diagnostics }) => diagnostics);
|
|
128
127
|
const diagnostics = [
|
|
129
128
|
...(program.diagnostic ? [program.diagnostic] : []),
|
|
130
129
|
...skills.diagnostics,
|
|
131
130
|
...missingResources,
|
|
132
|
-
...extensionDiagnostics,
|
|
133
131
|
...(verification.status === 'invalid' ? [{
|
|
134
132
|
code: 'VERIFICATION_EVIDENCE_INVALID',
|
|
135
133
|
message: 'Saved verification evidence is malformed.',
|
|
@@ -145,7 +143,6 @@ export async function checkProject({
|
|
|
145
143
|
if (missingResources.length > 0) {
|
|
146
144
|
guidance.push(`${missingResources.map(({ message }) => message).join(' ')} Prompt generation remains available.`);
|
|
147
145
|
}
|
|
148
|
-
if (extensionDiagnostics.length > 0) guidance.push('Resolve the competing consumer-owned Stack sections.');
|
|
149
146
|
if (['missing', 'stale'].includes(verification.status)) guidance.push('Run genesis verify to refresh concrete evidence.');
|
|
150
147
|
if (verification.status === 'unconfigured') guidance.push('Add project verification commands to genesis/stack.md.');
|
|
151
148
|
guidance.push('Use genesis prompt --task review for a semantic, evidence-based comparison.');
|
|
@@ -154,7 +151,6 @@ export async function checkProject({
|
|
|
154
151
|
|| engineering.status === 'defaulted'
|
|
155
152
|
|| ['missing', 'outdated'].includes(skills.status)
|
|
156
153
|
|| missingResources.length > 0
|
|
157
|
-
|| extensionDiagnostics.length > 0
|
|
158
154
|
|| ['missing', 'stale', 'unconfigured'].includes(verification.status);
|
|
159
155
|
|
|
160
156
|
return {
|
|
@@ -169,9 +165,7 @@ export async function checkProject({
|
|
|
169
165
|
skills: skills.status,
|
|
170
166
|
program: program.status,
|
|
171
167
|
environment: projectEnvironment.status,
|
|
172
|
-
extensions:
|
|
173
|
-
? 'blocked'
|
|
174
|
-
: stack.extensions.length > 0 ? 'ready' : 'unconfigured',
|
|
168
|
+
extensions: stack.extensions.length > 0 ? 'ready' : 'unconfigured',
|
|
175
169
|
resources: missingResources.length > 0 ? 'missing' : 'inputs-present',
|
|
176
170
|
verification: verification.status,
|
|
177
171
|
programFiles: program.files,
|
package/src/index/code-index.js
CHANGED
|
@@ -253,6 +253,7 @@ function formatCount(value, singular) {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
export async function buildProjectIndex({
|
|
256
|
+
allowUnresolvedSections = false,
|
|
256
257
|
projectRoot,
|
|
257
258
|
queries = [],
|
|
258
259
|
stackPackages = [],
|
|
@@ -264,7 +265,7 @@ export async function buildProjectIndex({
|
|
|
264
265
|
const observationReader = typeof observations === 'function' ? observations : null;
|
|
265
266
|
if (observationReader) observations = null;
|
|
266
267
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
267
|
-
const stack = await readStack(root, { stackPackages });
|
|
268
|
+
const stack = await readStack(root, { allowUnresolvedSections, stackPackages });
|
|
268
269
|
const packageRoots = stack.packageDiscovery.discoveries.flatMap(discovery => discovery.manifests.map(manifest => path.posix.join(path.posix.dirname(manifest), discovery.packageRoot)));
|
|
269
270
|
const includePath = filePath => isProjectContentPath(filePath) && !packageRoots.some(directory => filePath === directory || filePath.startsWith(`${directory}/`));
|
|
270
271
|
const states = await gitVisibleFileStates(root, { includePath });
|
|
@@ -322,7 +323,7 @@ export async function buildProjectIndex({
|
|
|
322
323
|
// valid artifact after an inspector/parser read racing a source change.
|
|
323
324
|
const finalStates = await gitVisibleFileStates(root, { includePath });
|
|
324
325
|
const finalFiles = [...finalStates].filter(([filePath, state]) => state.exists && !state.symlink && !state.special && state.hash && !cityPathExcluded(filePath, stack.cityExclusions)).map(([filePath, state]) => ({ path: filePath, hash: state.hash, mode: state.mode }));
|
|
325
|
-
const finalStack = await readStack(root, { stackPackages });
|
|
326
|
+
const finalStack = await readStack(root, { allowUnresolvedSections, stackPackages });
|
|
326
327
|
const finalMap = await inspectSubsystems({ projectRoot: root }).catch(() => null);
|
|
327
328
|
if (stableJson(finalFiles) !== stableJson(files) || finalStack.identityHash !== stack.identityHash || (finalMap?.identity || null) !== (subsystemMap?.identity || null)) {
|
|
328
329
|
throw new GenesisError('ARCHITECTURE_SOURCE_CHANGED', 'Source or Stack changed during architecture analysis. The previous index is retained; refresh after the changes settle.');
|
package/src/index/init.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
inspectProjectFormatAtRoot,
|
|
22
22
|
requireCurrentProjectFormat,
|
|
23
23
|
} from './project-format.js';
|
|
24
|
-
import { EMPTY_STACK_SOURCE, readStack } from './stack.js';
|
|
24
|
+
import { EMPTY_STACK_SOURCE, readStack, stackDecisionResult } from './stack.js';
|
|
25
25
|
|
|
26
26
|
async function createIfMissing(projectRoot, relativePath, source) {
|
|
27
27
|
const location = path.join(projectRoot, relativePath);
|
|
@@ -35,7 +35,11 @@ async function createIfMissing(projectRoot, relativePath, source) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export async function initializeProject({
|
|
38
|
+
export async function initializeProject({
|
|
39
|
+
allowUnresolvedSections = false,
|
|
40
|
+
projectRoot,
|
|
41
|
+
stackPackages = [],
|
|
42
|
+
} = {}) {
|
|
39
43
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
40
44
|
const projectFormat = await inspectProjectFormatAtRoot(root);
|
|
41
45
|
requireCurrentProjectFormat(projectFormat, { allowUninitialized: true });
|
|
@@ -47,7 +51,8 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
|
|
|
47
51
|
createIfMissing(root, STACK_PATH, EMPTY_STACK_SOURCE),
|
|
48
52
|
])).filter(Boolean);
|
|
49
53
|
await mkdir(path.join(root, PROGRAM_ROOT), { recursive: true });
|
|
50
|
-
const stack = await readStack(root, { stackPackages });
|
|
54
|
+
const stack = await readStack(root, { allowUnresolvedSections, stackPackages });
|
|
55
|
+
const decisions = stackDecisionResult(stack);
|
|
51
56
|
const skills = await syncProjectSkills({ projectRoot: root, stack });
|
|
52
57
|
const integrations = await syncAgentIntegrations({ projectRoot: root });
|
|
53
58
|
const version = projectFormat.status === 'uninitialized'
|
|
@@ -67,7 +72,8 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
|
|
|
67
72
|
? 'Initialized Genesis, Agent Skills, and project guidance for Codex, Claude Code and OpenCode.'
|
|
68
73
|
: 'Genesis, its Agent Skills, and project guidance for Codex, Claude Code and OpenCode are already initialized.',
|
|
69
74
|
changedFiles,
|
|
70
|
-
diagnostics: skills.diagnostics,
|
|
75
|
+
diagnostics: [...decisions.diagnostics, ...skills.diagnostics],
|
|
76
|
+
...(decisions.prompt ? { prompt: decisions.prompt } : {}),
|
|
71
77
|
...(integrations.warnings ? { warnings: integrations.warnings } : {}),
|
|
72
78
|
guidance: [
|
|
73
79
|
'Open Codex and use /hooks to review and trust the project guidance hook.',
|
package/src/index/migration.js
CHANGED
|
@@ -79,7 +79,13 @@ export async function migrateProject({ projectRoot, stackPackages = [] } = {}) {
|
|
|
79
79
|
migratedFiles.push(PROJECT_VERSION_PATH);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
// Like selection, migration completes and reports competing Stack sections
|
|
83
|
+
// as project decisions; ordinary reads keep rejecting until they are authored.
|
|
84
|
+
const initialized = await initializeProject({
|
|
85
|
+
allowUnresolvedSections: true,
|
|
86
|
+
projectRoot: root,
|
|
87
|
+
stackPackages,
|
|
88
|
+
});
|
|
83
89
|
const changedFiles = uniqueSorted([...migratedFiles, ...initialized.changedFiles]);
|
|
84
90
|
const migrated = initial.status !== 'current';
|
|
85
91
|
return {
|
|
@@ -92,5 +98,6 @@ export async function migrateProject({ projectRoot, stackPackages = [] } = {}) {
|
|
|
92
98
|
projectFormat: await inspectProjectFormatAtRoot(root),
|
|
93
99
|
changedFiles,
|
|
94
100
|
diagnostics: initialized.diagnostics,
|
|
101
|
+
...(initialized.prompt ? { prompt: initialized.prompt } : {}),
|
|
95
102
|
};
|
|
96
103
|
}
|
|
@@ -112,10 +112,12 @@ function contractSections({ components, knownSectionNames, sections }) {
|
|
|
112
112
|
components,
|
|
113
113
|
opaqueStackSections(sections, knownSectionNames),
|
|
114
114
|
);
|
|
115
|
+
const unresolvedSections = [];
|
|
115
116
|
for (const extension of extensions) {
|
|
116
117
|
const [diagnostic] = extension.diagnostics;
|
|
117
118
|
if (diagnostic) {
|
|
118
|
-
|
|
119
|
+
unresolvedSections.push({ name: extension.name, diagnostic });
|
|
120
|
+
continue;
|
|
119
121
|
}
|
|
120
122
|
projectLines.set(extension.name, trimStackSectionLines(extension.lines));
|
|
121
123
|
if (!sections.has(extension.name)) materializedSections.push(extension.name);
|
|
@@ -130,15 +132,36 @@ function contractSections({ components, knownSectionNames, sections }) {
|
|
|
130
132
|
materializedSections,
|
|
131
133
|
projectContracts: [...projectLines].map(([name, lines]) => ({ name, lines })),
|
|
132
134
|
resourceLines: projectLines.get('Resources') ?? null,
|
|
135
|
+
unresolvedSections,
|
|
133
136
|
verificationLines: projectLines.get('Verification') ?? null,
|
|
134
137
|
};
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
function unresolvedSectionsError(unresolvedSections) {
|
|
141
|
+
if (unresolvedSections.length === 1) {
|
|
142
|
+
const [{ diagnostic }] = unresolvedSections;
|
|
143
|
+
return new GenesisError(diagnostic.code, diagnostic.message, diagnostic.details);
|
|
144
|
+
}
|
|
145
|
+
return new GenesisError(
|
|
146
|
+
'STACK_SECTION_AMBIGUOUS',
|
|
147
|
+
unresolvedSections.map(({ diagnostic }) => diagnostic.message).join('\n\n'),
|
|
148
|
+
{ sections: unresolvedSections.map(({ diagnostic }) => diagnostic.details) },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
export function materializeStackProjectContracts(options) {
|
|
138
153
|
return contractSections(options);
|
|
139
154
|
}
|
|
140
155
|
|
|
141
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Require every selected proposal to be project-owned. A competing proposal
|
|
158
|
+
* is a project decision; only the add operation that reports it may continue
|
|
159
|
+
* without it.
|
|
160
|
+
*/
|
|
161
|
+
export function requireMaterializedStackProjectContracts({
|
|
162
|
+
allowUnresolvedSections = false,
|
|
163
|
+
...options
|
|
164
|
+
}) {
|
|
142
165
|
const result = contractSections(options);
|
|
143
166
|
if (result.materializedSections.length > 0) {
|
|
144
167
|
const headings = result.materializedSections.map((name) => `\`## ${name}\``);
|
|
@@ -148,11 +171,15 @@ export function requireMaterializedStackProjectContracts(options) {
|
|
|
148
171
|
{ sections: result.materializedSections },
|
|
149
172
|
);
|
|
150
173
|
}
|
|
174
|
+
if (!allowUnresolvedSections && result.unresolvedSections.length > 0) {
|
|
175
|
+
throw unresolvedSectionsError(result.unresolvedSections);
|
|
176
|
+
}
|
|
151
177
|
return result;
|
|
152
178
|
}
|
|
153
179
|
|
|
154
|
-
function contractBlocks(projectContracts) {
|
|
180
|
+
function contractBlocks(projectContracts, hasDecisions) {
|
|
155
181
|
if (projectContracts.length === 0) {
|
|
182
|
+
if (hasDecisions) return ['No other operation contract was proposed by these components.'];
|
|
156
183
|
return [
|
|
157
184
|
'No operation contract was proposed by these components. Inspect the application you are creating and declare only its real setup, verification, output, and other consumer operations before calling it ready.',
|
|
158
185
|
];
|
|
@@ -167,14 +194,43 @@ function contractBlocks(projectContracts) {
|
|
|
167
194
|
]);
|
|
168
195
|
}
|
|
169
196
|
|
|
197
|
+
function decisionBlocks(unresolvedSections) {
|
|
198
|
+
return unresolvedSections.flatMap(({ name, diagnostic }) => [
|
|
199
|
+
`### \`## ${name}\``,
|
|
200
|
+
'',
|
|
201
|
+
...diagnostic.details.proposals.flatMap(({ source, lines }) => [
|
|
202
|
+
`Proposal from \`${source}\`:`,
|
|
203
|
+
'',
|
|
204
|
+
'~~~markdown',
|
|
205
|
+
...lines,
|
|
206
|
+
'~~~',
|
|
207
|
+
'',
|
|
208
|
+
]),
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
|
|
170
212
|
export function renderStackPreparationPrompt({
|
|
171
213
|
components,
|
|
172
214
|
materializedSections = [],
|
|
173
215
|
newlySelected = [],
|
|
174
216
|
projectContracts = [],
|
|
175
217
|
reviewSections = [],
|
|
218
|
+
unresolvedSections = [],
|
|
176
219
|
} = {}) {
|
|
177
|
-
if (newlySelected.length === 0 && materializedSections.length === 0
|
|
220
|
+
if (newlySelected.length === 0 && materializedSections.length === 0
|
|
221
|
+
&& unresolvedSections.length === 0) return '';
|
|
222
|
+
const decisionHeadings = unresolvedSections.map(({ name }) => `\`## ${name}\``).join(', ');
|
|
223
|
+
const steps = [
|
|
224
|
+
...(unresolvedSections.length > 0 ? [
|
|
225
|
+
`Decide ${decisionHeadings} first. Selected components propose competing bodies and none requires the other, so Genesis left ${unresolvedSections.length === 1 ? 'that heading' : 'those headings'} unwritten. Author one complete project-owned section per heading in \`genesis/stack.md\`, choosing between the proposals quoted under “Project decisions required” for this application. Until then current-format Genesis reads, including \`genesis check\` and \`genesis context\`, reject the Stack with \`STACK_SECTION_AMBIGUOUS\`.`,
|
|
226
|
+
] : []),
|
|
227
|
+
'Read `genesis/blueprint.md`, `genesis/engineering.md`, and the complete `genesis/stack.md`, then run `genesis context .` (or the relevant source paths).',
|
|
228
|
+
'Load every applicable installed Agent Skill and follow the selected technology’s authoritative guidance before creating dependencies or source.',
|
|
229
|
+
'For a new project, implement the source and commands so every project contract below is true. For existing source, inspect reality first and change a complete project section when the inherited proposal is not already true; do not add compatibility code merely to imitate a proposal.',
|
|
230
|
+
'Keep each consumer-owned section opaque to Genesis. Validate it with the consumer or framework that owns its grammar and behavior.',
|
|
231
|
+
'Establish or update the non-technical Blueprint and affected Program explanations as the product becomes concrete.',
|
|
232
|
+
'Run `genesis check`, resolve structural or incomplete-contract diagnostics, and run `genesis verify` only after the declared verification commands exist. Passing verification is evidence for those commands, not proof of every consumer operation.',
|
|
233
|
+
];
|
|
178
234
|
const selected = components.map(({ id }) => `\`${id}\``).join(', ');
|
|
179
235
|
return [
|
|
180
236
|
'# Prepare the selected Stack',
|
|
@@ -188,18 +244,23 @@ export function renderStackPreparationPrompt({
|
|
|
188
244
|
'',
|
|
189
245
|
`These existing project contracts remained authoritative while the new components were added: ${reviewSections.map((name) => `\`## ${name}\``).join(', ')}. Reconcile them against the newly selected technology using source evidence; Genesis deliberately did not overwrite them.`,
|
|
190
246
|
] : []),
|
|
247
|
+
...(unresolvedSections.length > 0 ? [
|
|
248
|
+
'',
|
|
249
|
+
`Unresolved project decisions: ${decisionHeadings}.`,
|
|
250
|
+
] : []),
|
|
191
251
|
'',
|
|
192
252
|
'Continue this same task now:',
|
|
193
253
|
'',
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
254
|
+
...steps.map((step, index) => `${index + 1}. ${step}`),
|
|
255
|
+
...(unresolvedSections.length > 0 ? [
|
|
256
|
+
'',
|
|
257
|
+
'## Project decisions required',
|
|
258
|
+
'',
|
|
259
|
+
...trimStackSectionLines(decisionBlocks(unresolvedSections)),
|
|
260
|
+
] : []),
|
|
200
261
|
'',
|
|
201
262
|
'## Project contracts to satisfy',
|
|
202
263
|
'',
|
|
203
|
-
...contractBlocks(projectContracts),
|
|
264
|
+
...contractBlocks(projectContracts, unresolvedSections.length > 0),
|
|
204
265
|
].join('\n').trimEnd();
|
|
205
266
|
}
|
|
@@ -29,19 +29,55 @@ export function opaqueStackSections(sections, knownNames) {
|
|
|
29
29
|
}));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function requirementClosures(components) {
|
|
33
|
+
const byId = new Map(components.map((component) => [component.id, component]));
|
|
34
|
+
const closures = new Map();
|
|
35
|
+
const closure = (id) => {
|
|
36
|
+
if (closures.has(id)) return closures.get(id);
|
|
37
|
+
const required = new Set();
|
|
38
|
+
closures.set(id, required);
|
|
39
|
+
for (const dependency of byId.get(id)?.requires || []) {
|
|
40
|
+
if (!byId.has(dependency) || dependency === id) continue;
|
|
41
|
+
required.add(dependency);
|
|
42
|
+
for (const transitive of closure(dependency)) required.add(transitive);
|
|
43
|
+
}
|
|
44
|
+
return required;
|
|
45
|
+
};
|
|
46
|
+
for (const { id } of components) closure(id);
|
|
47
|
+
return closures;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function quotedProposal({ source, lines }) {
|
|
51
|
+
return [`- \`${source}\` proposes:`, '', ' ~~~markdown', ...lines.map((line) => (line ? ` ${line}` : '')), ' ~~~'];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Render the actionable decision message for one unresolved section. */
|
|
55
|
+
export function unresolvedStackSectionMessage(name, proposals) {
|
|
56
|
+
return [
|
|
57
|
+
`Selected Stack components propose competing \`## ${name}\` sections and none supersedes the others: ${proposals.map(({ source }) => source).join(', ')}.`,
|
|
58
|
+
`Author one complete project \`## ${name}\` section in genesis/stack.md that chooses between these proposals; Genesis never merges them or picks one.`,
|
|
59
|
+
'',
|
|
60
|
+
...proposals.flatMap(quotedProposal),
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
/**
|
|
33
65
|
* Compose consumer-owned Stack sections without interpreting their contents.
|
|
34
|
-
* An existing project declaration wins
|
|
35
|
-
*
|
|
66
|
+
* An existing project declaration wins. Otherwise a component's proposal
|
|
67
|
+
* supersedes the proposals of selected components it transitively requires;
|
|
68
|
+
* exactly one remaining proposal is materialized, and several remaining
|
|
69
|
+
* proposals are an unresolved project decision.
|
|
36
70
|
*/
|
|
37
71
|
export function composeStackSections(components = [], projectSections = []) {
|
|
38
72
|
const projectByName = new Map(projectSections.map((section) => [section.name, section]));
|
|
73
|
+
const closures = requirementClosures(components);
|
|
39
74
|
const componentByName = new Map();
|
|
40
75
|
for (const component of components) {
|
|
41
76
|
for (const section of component.extensions || []) {
|
|
42
77
|
const declarations = componentByName.get(section.name) || [];
|
|
43
78
|
declarations.push({
|
|
44
79
|
...section,
|
|
80
|
+
component: component.id,
|
|
45
81
|
source: `component:${component.id}`,
|
|
46
82
|
});
|
|
47
83
|
componentByName.set(section.name, declarations);
|
|
@@ -59,23 +95,29 @@ export function composeStackSections(components = [], projectSections = []) {
|
|
|
59
95
|
};
|
|
60
96
|
}
|
|
61
97
|
const declarations = componentByName.get(name) || [];
|
|
62
|
-
|
|
98
|
+
const remaining = declarations.filter(({ component }) => !declarations.some((other) => (
|
|
99
|
+
other.component !== component && closures.get(other.component)?.has(component)
|
|
100
|
+
)));
|
|
101
|
+
if (remaining.length === 1) {
|
|
63
102
|
return {
|
|
64
103
|
name,
|
|
65
|
-
lines: [...
|
|
66
|
-
source:
|
|
104
|
+
lines: [...remaining[0].lines],
|
|
105
|
+
source: remaining[0].source,
|
|
67
106
|
diagnostics: [],
|
|
68
107
|
};
|
|
69
108
|
}
|
|
70
|
-
const
|
|
109
|
+
const proposals = remaining
|
|
110
|
+
.map(({ source, lines }) => ({ source, lines: trimStackSectionLines(lines) }))
|
|
111
|
+
.sort((left, right) => left.source.localeCompare(right.source));
|
|
112
|
+
const sources = proposals.map(({ source }) => source);
|
|
71
113
|
return {
|
|
72
114
|
name,
|
|
73
115
|
lines: [],
|
|
74
116
|
source: null,
|
|
75
117
|
diagnostics: [{
|
|
76
118
|
code: 'STACK_SECTION_AMBIGUOUS',
|
|
77
|
-
message:
|
|
78
|
-
details: { name, sources },
|
|
119
|
+
message: unresolvedStackSectionMessage(name, proposals),
|
|
120
|
+
details: { name, sources, proposals },
|
|
79
121
|
}],
|
|
80
122
|
};
|
|
81
123
|
});
|
package/src/index/stack.js
CHANGED
|
@@ -230,7 +230,11 @@ export async function addStackPieces({ pieces, projectRoot, stackPackages = [] }
|
|
|
230
230
|
});
|
|
231
231
|
const stackChanged = rendered !== source;
|
|
232
232
|
if (stackChanged) await writeFileAtomic(location, rendered);
|
|
233
|
-
const stack = await readStack(projectRoot, {
|
|
233
|
+
const stack = await readStack(projectRoot, {
|
|
234
|
+
allowUnresolvedSections: true,
|
|
235
|
+
stackPackages: availablePackages,
|
|
236
|
+
});
|
|
237
|
+
const { unresolvedSections } = stack;
|
|
234
238
|
const skills = await syncProjectSkills({
|
|
235
239
|
projectRoot,
|
|
236
240
|
stack,
|
|
@@ -246,7 +250,10 @@ export async function addStackPieces({ pieces, projectRoot, stackPackages = [] }
|
|
|
246
250
|
: 'Every requested Stack component and Agent Skill is already selected.',
|
|
247
251
|
components: selected,
|
|
248
252
|
changedFiles,
|
|
249
|
-
diagnostics:
|
|
253
|
+
diagnostics: [
|
|
254
|
+
...unresolvedSections.map(({ diagnostic }) => diagnostic),
|
|
255
|
+
...skills.diagnostics,
|
|
256
|
+
],
|
|
250
257
|
prompt: renderStackPreparationPrompt({
|
|
251
258
|
components: stack.components,
|
|
252
259
|
materializedSections: projectContracts.materializedSections,
|
|
@@ -254,6 +261,7 @@ export async function addStackPieces({ pieces, projectRoot, stackPackages = [] }
|
|
|
254
261
|
projectContracts: stack.projectContracts,
|
|
255
262
|
reviewSections: [...new Set(newlySelectedPieces.flatMap(stackPieceContractNames))]
|
|
256
263
|
.filter((name) => sections.has(name)),
|
|
264
|
+
unresolvedSections,
|
|
257
265
|
}),
|
|
258
266
|
};
|
|
259
267
|
}
|
|
@@ -295,6 +303,7 @@ async function customizePiece(projectRoot, piece) {
|
|
|
295
303
|
|
|
296
304
|
async function readStackWithContractMode(projectRoot, {
|
|
297
305
|
allowComponentContracts,
|
|
306
|
+
allowUnresolvedSections = false,
|
|
298
307
|
stackPackages = [],
|
|
299
308
|
}) {
|
|
300
309
|
const location = path.join(projectRoot, STACK_PATH);
|
|
@@ -324,6 +333,7 @@ async function readStackWithContractMode(projectRoot, {
|
|
|
324
333
|
const projectContracts = (allowComponentContracts
|
|
325
334
|
? materializeStackProjectContracts
|
|
326
335
|
: requireMaterializedStackProjectContracts)({
|
|
336
|
+
allowUnresolvedSections,
|
|
327
337
|
components,
|
|
328
338
|
knownSectionNames: STACK_SECTIONS,
|
|
329
339
|
sections,
|
|
@@ -386,6 +396,7 @@ async function readStackWithContractMode(projectRoot, {
|
|
|
386
396
|
environmentFiles,
|
|
387
397
|
extensions,
|
|
388
398
|
projectContracts: projectContracts.projectContracts,
|
|
399
|
+
...(allowUnresolvedSections ? { unresolvedSections: projectContracts.unresolvedSections } : {}),
|
|
389
400
|
resources,
|
|
390
401
|
guidance: composedProse(components, 'guidance'),
|
|
391
402
|
adoption: composedProse(components, 'adoption'),
|
|
@@ -394,13 +405,32 @@ async function readStackWithContractMode(projectRoot, {
|
|
|
394
405
|
};
|
|
395
406
|
}
|
|
396
407
|
|
|
397
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Read the current-format Stack. Only operations that report pending project
|
|
410
|
+
* decisions themselves (selection and migration) may allow unresolved
|
|
411
|
+
* competing sections; the returned Stack then lists them.
|
|
412
|
+
*/
|
|
413
|
+
export function readStack(projectRoot, { allowUnresolvedSections = false, stackPackages = [] } = {}) {
|
|
398
414
|
return readStackWithContractMode(projectRoot, {
|
|
399
415
|
allowComponentContracts: false,
|
|
416
|
+
allowUnresolvedSections,
|
|
400
417
|
stackPackages,
|
|
401
418
|
});
|
|
402
419
|
}
|
|
403
420
|
|
|
421
|
+
/** Diagnostics and the preparation prompt for a Stack read with unresolved sections allowed. */
|
|
422
|
+
export function stackDecisionResult(stack) {
|
|
423
|
+
const unresolvedSections = stack.unresolvedSections || [];
|
|
424
|
+
return {
|
|
425
|
+
diagnostics: unresolvedSections.map(({ diagnostic }) => diagnostic),
|
|
426
|
+
prompt: renderStackPreparationPrompt({
|
|
427
|
+
components: stack.components,
|
|
428
|
+
projectContracts: stack.projectContracts,
|
|
429
|
+
unresolvedSections,
|
|
430
|
+
}),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
404
434
|
export function readLegacyStack(projectRoot, { stackPackages = [] } = {}) {
|
|
405
435
|
return readStackWithContractMode(projectRoot, {
|
|
406
436
|
allowComponentContracts: true,
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
|
|
3
4
|
import { checkProject } from './index/check.js';
|
|
4
5
|
import { inspectProjectSkills, syncProjectSkills } from './index/agent-skills.js';
|
|
@@ -84,14 +85,16 @@ export async function initialize({ projectRoot = process.cwd(), stackPackages =
|
|
|
84
85
|
|
|
85
86
|
export async function migrate({ projectRoot = process.cwd(), stackPackages = [] } = {}) {
|
|
86
87
|
const migrated = await migrateProject({ projectRoot, stackPackages });
|
|
87
|
-
const index = await buildProjectIndex({ projectRoot, stackPackages });
|
|
88
|
+
const index = await buildProjectIndex({ allowUnresolvedSections: true, projectRoot, stackPackages });
|
|
88
89
|
const result = withIndexResult(migrated, index);
|
|
89
90
|
const checked = await checkProject({ projectRoot, stackPackages });
|
|
90
91
|
return {
|
|
91
92
|
...result,
|
|
92
93
|
projectFormat: checked.projectFormat,
|
|
93
94
|
check: checked,
|
|
94
|
-
diagnostics: [...result.diagnostics, ...checked.diagnostics],
|
|
95
|
+
diagnostics: [...result.diagnostics, ...checked.diagnostics].filter((diagnostic, index, all) => (
|
|
96
|
+
!all.slice(0, index).some((previous) => isDeepStrictEqual(previous, diagnostic))
|
|
97
|
+
)),
|
|
95
98
|
guidance: checked.guidance,
|
|
96
99
|
};
|
|
97
100
|
}
|
|
@@ -127,7 +130,11 @@ export function installCodex({ environment = process.env } = {}) {
|
|
|
127
130
|
export async function addStack({ pieces, projectRoot = process.cwd(), stackPackages = [] } = {}) {
|
|
128
131
|
const root = path.resolve(projectRoot);
|
|
129
132
|
const selected = await addStackPieces({ pieces, projectRoot: root, stackPackages });
|
|
130
|
-
const index = await buildProjectIndex({
|
|
133
|
+
const index = await buildProjectIndex({
|
|
134
|
+
allowUnresolvedSections: true,
|
|
135
|
+
projectRoot: root,
|
|
136
|
+
stackPackages,
|
|
137
|
+
});
|
|
131
138
|
return withIndexResult(selected, index);
|
|
132
139
|
}
|
|
133
140
|
|