@mriekki/project-forge 4.4.1 → 4.5.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.
- package/assets/components.json +4 -6
- package/assets/generated/ASSEMBLY.json +7 -9
- package/assets/generated/core/core/instructions/project-forge-workflow.instructions.md +2 -2
- package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +4 -2
- package/assets/generated/core/core/skills/refine-requirements/SKILL.md +4 -7
- package/package.json +1 -1
- package/src/engine.mjs +27 -28
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.5.0",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"components": {
|
|
15
15
|
"core": {
|
|
16
|
-
"
|
|
17
|
-
"commit": "45139179c3d10ebd89702ac3aedec231364d8a34",
|
|
16
|
+
"revision": "d106b1834a8c44bdae0b16e4af5d8dab0711a120",
|
|
18
17
|
"version": "4.0.0",
|
|
19
18
|
"distribution": "greenfield-workspace",
|
|
20
19
|
"descriptor": "assets/core/distribution.json",
|
|
@@ -22,8 +21,7 @@
|
|
|
22
21
|
"canonicalDescriptor": "distributions/greenfield-workspace.json"
|
|
23
22
|
},
|
|
24
23
|
"deployment": {
|
|
25
|
-
"
|
|
26
|
-
"commit": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
|
|
24
|
+
"revision": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
|
|
27
25
|
"distribution": "greenfield-project",
|
|
28
26
|
"descriptor": "assets/deployment/distribution.json",
|
|
29
27
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.5.0",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
|
-
"
|
|
8
|
-
"commit": "45139179c3d10ebd89702ac3aedec231364d8a34",
|
|
7
|
+
"revision": "d106b1834a8c44bdae0b16e4af5d8dab0711a120",
|
|
9
8
|
"version": "4.0.0",
|
|
10
9
|
"distribution": "greenfield-workspace",
|
|
11
10
|
"files": [
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
{
|
|
23
22
|
"source": "core/instructions/project-forge-workflow.instructions.md",
|
|
24
23
|
"destination": ".github/instructions/project-forge-workflow.instructions.md",
|
|
25
|
-
"sha256": "
|
|
24
|
+
"sha256": "13379a56c65921756cdae8e1d2d711c3321b9d899b5d96a81dbf51ecaa9ce022"
|
|
26
25
|
},
|
|
27
26
|
{
|
|
28
27
|
"source": "core/instructions/project-forge-docs.instructions.md",
|
|
@@ -42,7 +41,7 @@
|
|
|
42
41
|
{
|
|
43
42
|
"source": "core/skills/refine-requirements/SKILL.md",
|
|
44
43
|
"destination": ".github/skills/refine-requirements/SKILL.md",
|
|
45
|
-
"sha256": "
|
|
44
|
+
"sha256": "9e76b9c3700a00af8852928d4e6bf1aeca9a24c6cc8fc9b2f1873e0198b63962"
|
|
46
45
|
},
|
|
47
46
|
{
|
|
48
47
|
"source": "core/skills/start-work/SKILL.md",
|
|
@@ -187,7 +186,7 @@
|
|
|
187
186
|
{
|
|
188
187
|
"source": "core/process/INTERACTIVE_SESSION_GUIDE.md",
|
|
189
188
|
"destination": "docs/process/INTERACTIVE_SESSION_GUIDE.md",
|
|
190
|
-
"sha256": "
|
|
189
|
+
"sha256": "08df5212fc6d08e46d3121dad3d076e05baa71b046a7e360a201b18916b7e574"
|
|
191
190
|
},
|
|
192
191
|
{
|
|
193
192
|
"source": "core/process/COACHING_GUIDE.md",
|
|
@@ -327,8 +326,7 @@
|
|
|
327
326
|
]
|
|
328
327
|
},
|
|
329
328
|
"deployment": {
|
|
330
|
-
"
|
|
331
|
-
"commit": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
|
|
329
|
+
"revision": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
|
|
332
330
|
"distribution": "greenfield-project",
|
|
333
331
|
"files": [
|
|
334
332
|
{
|
|
@@ -20,9 +20,9 @@ For implementation, establish the repository-appropriate short-lived work branch
|
|
|
20
20
|
|
|
21
21
|
Continuous modes such as `/grill-me`, `/refine-requirements`, `/coach`, `/learning-review`, and `/deployment-readiness` continue until stopped or naturally complete.
|
|
22
22
|
|
|
23
|
-
During interactive questioning, prefer concise choice-first options for genuinely bounded human decisions, including `Other / specify` and `Not sure — recommend one` when appropriate. When the host exposes native structured question controls, use them instead of a plain numbered chat list. In VS Code, use the host's structured question surface such as `vscode_askQuestions` when available: single-select for mutually exclusive decisions/actions and multi-select only when several simultaneous selections are valid. A textual numbered menu is only the fallback when structured choice UI is unavailable.
|
|
23
|
+
During interactive questioning, prefer concise choice-first options for genuinely bounded human decisions, including `Other / specify` and `Not sure — recommend one` when appropriate. When the host exposes native structured question controls, use them instead of a plain numbered chat list. In VS Code, use the host's structured question surface such as `vscode_askQuestions` when available: single-select for mutually exclusive decisions/actions and multi-select only when several simultaneous selections are valid. A textual numbered or lettered menu is only the fallback when structured choice UI is unavailable.
|
|
24
24
|
|
|
25
|
-
Make a recommendation only when current evidence, repository policy, or a clear engineering tradeoff supports one. Mark exactly one supported choice with the host's `recommended` indicator when available and give a brief rationale. If no option is clearly better, do not mark any option recommended. A recommendation is advice, not approval: record only the human's selected/confirmed answer. If the human asks for no recommendation on one decision, honor that for that decision only; later questions may still include evidence-backed recommendations.
|
|
25
|
+
Make a recommendation only when current evidence, repository policy, or a clear engineering tradeoff supports one. Mark exactly one supported choice with the host's `recommended` indicator when available and give a brief rationale. When the host does not expose recommendation metadata, append `[RECOMMENDED]` to exactly one option label and keep the rationale visible. A default/preselected option may match the recommendation, but a default alone must never communicate advice or human approval. If no option is clearly better, do not mark any option recommended and do not use a default that implies a preference. A recommendation is advice, not approval: record only the human's selected/confirmed answer. If the human asks for no recommendation on one decision, honor that for that decision only; later questions may still include evidence-backed recommendations.
|
|
26
26
|
|
|
27
27
|
Keep factual/environment-specific values and questions intended to elicit the human's own framing, reasoning, debugging approach, design rationale, or understanding open-ended unless options are later useful as scaffolding.
|
|
28
28
|
|
|
@@ -51,16 +51,18 @@ A recommendation is optional advice, not a required field and not an accepted de
|
|
|
51
51
|
|
|
52
52
|
- mark **exactly one** choice as `RECOMMENDED` only when current evidence, repository policy, or a clear engineering tradeoff supports it;
|
|
53
53
|
- use the host's `recommended` indicator/metadata when available and add a brief rationale in the choice description or surrounding prompt;
|
|
54
|
+
- when the host does not expose recommendation metadata, append `[RECOMMENDED]` to exactly one option label and keep the concise rationale visible in the prompt or option description;
|
|
55
|
+
- a default/preselected value may match the recommended option, but a default alone must never communicate that an option is recommended or human-approved;
|
|
54
56
|
- do not mark multiple choices recommended;
|
|
55
57
|
- do not invent a preference merely to populate recommendation metadata;
|
|
56
|
-
- when no option is clearly better, present the choices with **no recommendation
|
|
58
|
+
- when no option is clearly better, present the choices with **no recommendation** and do not use a default that implies a preference;
|
|
57
59
|
- if the human asks for no recommendation on a specific decision, omit the recommendation for that question only; later questions may still include evidence-backed recommendations;
|
|
58
60
|
- the human may select a different option or answer free-form;
|
|
59
61
|
- record only the human's selected/confirmed answer as the requirement, action, or decision. Never record the recommendation itself as accepted state.
|
|
60
62
|
|
|
61
63
|
A structured selector is presentation, not extra authorization. Selecting a choice authorizes only the action/session that the workflow says that choice authorizes.
|
|
62
64
|
|
|
63
|
-
When the host has no structured question UI, use a concise textual fallback. A numbered list is acceptable **only as this fallback**; keep the same labels/recommendation/consequence tags and accept the number, label, or a free-form answer. Do not use a plain numbered list as the normal VS Code interaction when native structured choice UI is available.
|
|
65
|
+
When the host has no structured question UI, use a concise textual fallback. A numbered or lettered list is acceptable **only as this fallback**; keep the same labels/recommendation/consequence tags, including `[RECOMMENDED]` when one option is evidence-backed, and accept the number/letter, label, or a free-form answer. Do not use a plain numbered list as the normal VS Code interaction when native structured choice UI is available.
|
|
64
66
|
|
|
65
67
|
Keep questions open-ended when predefined choices would weaken the purpose of the interaction. This includes:
|
|
66
68
|
|
|
@@ -124,9 +124,9 @@ Then present only relevant actions as a **single-select structured choice** when
|
|
|
124
124
|
- `Approve local requirements` `[REQUIREMENTS GATE]` when applicable
|
|
125
125
|
- `Stop` `[NO ACTION]`
|
|
126
126
|
|
|
127
|
-
Make a recommendation only when current requirement evidence or a clear engineering tradeoff supports one. If so, mark exactly one choice with the host's `recommended` indicator
|
|
127
|
+
Make a recommendation only when current requirement evidence or a clear engineering tradeoff supports one. If so, mark exactly one choice with the host's `recommended` indicator when that metadata exists; otherwise append `[RECOMMENDED]` to exactly one option label. Briefly explain why in the option description or surrounding prompt. A default/preselected value may match the recommendation, but a default alone must never communicate advice or approval. If there is no clear basis, show the choices without a recommendation and without a preference-implying default. The selected answer, not the recommendation, controls what the session does or records.
|
|
128
128
|
|
|
129
|
-
If the host has no structured choice UI, present the same relevant choices as a concise numbered text fallback and accept the number, label, or free-form intent. Do not use the
|
|
129
|
+
If the host has no structured choice UI, present the same relevant choices as a concise numbered or lettered text fallback, preserve `[RECOMMENDED]` on the evidence-backed option when applicable, and accept the number/letter, label, or free-form intent. Do not use the text fallback as the normal VS Code path.
|
|
130
130
|
|
|
131
131
|
## 5. Active refinement session
|
|
132
132
|
|
|
@@ -143,7 +143,7 @@ ask one material requirement question
|
|
|
143
143
|
|
|
144
144
|
For bounded requirement decisions such as lifecycle policy, validation behavior, destructive/reversible handling, or a small set of credible alternatives, use the choice-first interaction guidance from `INTERACTIVE_SESSION_GUIDE.md`. Render mutually exclusive alternatives through host-native **single-select** controls when available. Use multi-select only when the requirement genuinely permits multiple simultaneous selections. Keep `Other / specify` or free-text available when the option set may be incomplete. Keep outcome wording, workflow descriptions, acceptance intent, and other human-authored framing open-ended when predefined choices would constrain the requirement.
|
|
145
145
|
|
|
146
|
-
Recommendation behavior follows the shared guide: recommend at most one option, only when evidence/tradeoffs support it; give a concise rationale; never record the recommendation as accepted until the human selects or confirms it; and honor a `no recommendation` request for that question only.
|
|
146
|
+
Recommendation behavior follows the shared guide: recommend at most one option, only when evidence/tradeoffs support it; make the recommendation visibly distinguishable through native metadata or `[RECOMMENDED]`; give a concise rationale; never record the recommendation as accepted until the human selects or confirms it; and honor a `no recommendation` request for that question only.
|
|
147
147
|
|
|
148
148
|
Example of a supported recommendation:
|
|
149
149
|
|
|
@@ -153,11 +153,8 @@ Decision: How should a browser-to-real-SQLite path be proven for a narrowly scop
|
|
|
153
153
|
Choices:
|
|
154
154
|
- Integration test against real SQLite
|
|
155
155
|
- Browser smoke check through the API
|
|
156
|
-
- Both
|
|
156
|
+
- Both [RECOMMENDED] — proves persistence and browser/API paths without implying a broad end-to-end suite
|
|
157
157
|
- Other / specify
|
|
158
|
-
|
|
159
|
-
Recommendation: Both
|
|
160
|
-
Why: one real-SQLite integration test proves the persistence path and one browser smoke check proves the browser/API path without implying a broad end-to-end suite.
|
|
161
158
|
```
|
|
162
159
|
|
|
163
160
|
Treat this as advice for the human to accept, change, or replace. Do not record `Both` as a requirement until the human selects or confirms it.
|
package/package.json
CHANGED
package/src/engine.mjs
CHANGED
|
@@ -54,6 +54,12 @@ function baselineMap(baseline, filename) {
|
|
|
54
54
|
return map;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function componentRevision(component) {
|
|
58
|
+
const revision = component.revision ?? component.commit;
|
|
59
|
+
if (typeof revision !== 'string' || !/^[0-9a-f]{40}$/i.test(revision)) throw new Error('Component revision must be an exact 40-character Git revision');
|
|
60
|
+
return revision.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
export function componentEntries(packageData, componentId) {
|
|
58
64
|
const component = packageData.assets.components[componentId];
|
|
59
65
|
if (!component) throw new Error(`Package does not define component ${componentId}`);
|
|
@@ -95,22 +101,27 @@ function classifyDesired(componentId, src, dst, baselineItem) {
|
|
|
95
101
|
return { state: 'CONFLICT', desiredHash, currentHash };
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
function
|
|
99
|
-
|
|
104
|
+
function validateBaselineIdentity(baseline, componentId, component, filename) {
|
|
105
|
+
if (!Object.keys(baseline).length) return;
|
|
106
|
+
if (![1, 2].includes(baseline.schemaVersion)) throw new Error(`Unsupported ${filename} schemaVersion`);
|
|
107
|
+
if (baseline.schemaVersion === 2 && baseline.component !== componentId) throw new Error(`${filename} records a different component`);
|
|
108
|
+
if (baseline.distribution && baseline.distribution !== component.distribution) throw new Error(`${filename} records a different distribution`);
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
function
|
|
103
|
-
|
|
111
|
+
function componentMetadataCurrent(baseline, componentId, component) {
|
|
112
|
+
if (baseline.schemaVersion !== 2 || baseline.component !== componentId) return false;
|
|
113
|
+
if (baseline.revision !== componentRevision(component) || baseline.distribution !== component.distribution) return false;
|
|
114
|
+
if (componentId === 'core' && baseline.version !== component.version) return false;
|
|
115
|
+
return Array.isArray(baseline.managedPaths) && baseline.managedPaths.length > 0;
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
export function planComponent(packageData, componentId, targetRoot, acceptedRetired = new Set()) {
|
|
107
119
|
const component = packageData.assets.components[componentId];
|
|
120
|
+
if (!component) throw new Error(`Package does not define component ${componentId}`);
|
|
121
|
+
componentRevision(component);
|
|
108
122
|
const filename = baselineFile(componentId);
|
|
109
123
|
const baseline = loadOptionalJson(path.join(targetRoot, filename));
|
|
110
|
-
|
|
111
|
-
if (componentId === 'core' && baseline.coreRepository && baseline.coreRepository !== component.repository) throw new Error(`${filename} records a different Core repository`);
|
|
112
|
-
if (componentId === 'deployment' && baseline.deploymentKitRepository && baseline.deploymentKitRepository !== component.repository) throw new Error(`${filename} records a different Deployment Kit repository`);
|
|
113
|
-
if (baseline.distribution && baseline.distribution !== component.distribution) throw new Error(`${filename} records a different distribution`);
|
|
124
|
+
validateBaselineIdentity(baseline, componentId, component, filename);
|
|
114
125
|
|
|
115
126
|
const entries = componentEntries(packageData, componentId);
|
|
116
127
|
const baseMap = baselineMap(baseline, filename);
|
|
@@ -151,30 +162,19 @@ export function planComponent(packageData, componentId, targetRoot, acceptedReti
|
|
|
151
162
|
rows,
|
|
152
163
|
retired,
|
|
153
164
|
unsafe,
|
|
154
|
-
metadataCurrent:
|
|
165
|
+
metadataCurrent: componentMetadataCurrent(baseline, componentId, component)
|
|
155
166
|
};
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
function buildComponentBaseline(plan, consumer, consumerBaseline) {
|
|
159
170
|
const managedPaths = plan.rows.map((row) => ({ source: row.source, path: row.path, sha256: row.desiredHash }));
|
|
160
|
-
if (plan.componentId === 'core') {
|
|
161
|
-
return {
|
|
162
|
-
schemaVersion: 1,
|
|
163
|
-
consumer,
|
|
164
|
-
consumerBaseline,
|
|
165
|
-
coreRepository: plan.component.repository,
|
|
166
|
-
coreVersion: plan.component.version,
|
|
167
|
-
coreCommit: plan.component.commit,
|
|
168
|
-
distribution: plan.component.distribution,
|
|
169
|
-
managedPaths
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
171
|
return {
|
|
173
|
-
schemaVersion:
|
|
172
|
+
schemaVersion: 2,
|
|
173
|
+
component: plan.componentId,
|
|
174
174
|
consumer,
|
|
175
175
|
consumerBaseline,
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
revision: componentRevision(plan.component),
|
|
177
|
+
...(plan.component.version ? { version: plan.component.version } : {}),
|
|
178
178
|
distribution: plan.component.distribution,
|
|
179
179
|
managedPaths
|
|
180
180
|
};
|
|
@@ -213,15 +213,14 @@ export function applyProfile(packageData, profilePlan, targetRoot, consumer, con
|
|
|
213
213
|
fs.writeFileSync(path.join(targetRoot, plan.filename), JSON.stringify(baseline, null, 2) + '\n');
|
|
214
214
|
}
|
|
215
215
|
const projectForgeBaseline = {
|
|
216
|
-
schemaVersion:
|
|
216
|
+
schemaVersion: 2,
|
|
217
217
|
package: packageData.pkg.name,
|
|
218
218
|
version: packageData.pkg.version,
|
|
219
219
|
profile: profilePlan.profileName,
|
|
220
220
|
consumer,
|
|
221
221
|
consumerBaseline,
|
|
222
222
|
components: Object.fromEntries(profilePlan.plans.map((plan) => [plan.componentId, {
|
|
223
|
-
|
|
224
|
-
commit: plan.component.commit,
|
|
223
|
+
revision: componentRevision(plan.component),
|
|
225
224
|
...(plan.component.version ? { version: plan.component.version } : {}),
|
|
226
225
|
distribution: plan.component.distribution
|
|
227
226
|
}]))
|
|
@@ -233,7 +232,7 @@ export function applyProfile(packageData, profilePlan, targetRoot, consumer, con
|
|
|
233
232
|
export function checkProfile(packageData, profileName, targetRoot) {
|
|
234
233
|
const plan = planProfile(packageData, profileName, targetRoot);
|
|
235
234
|
const projectForgeBaseline = loadOptionalJson(path.join(targetRoot, 'PROJECT_FORGE_BASELINE.json'));
|
|
236
|
-
const projectForgeCurrent = projectForgeBaseline.schemaVersion ===
|
|
235
|
+
const projectForgeCurrent = projectForgeBaseline.schemaVersion === 2 && projectForgeBaseline.package === packageData.pkg.name && projectForgeBaseline.version === packageData.pkg.version && projectForgeBaseline.profile === profileName;
|
|
237
236
|
const componentClean = plan.plans.every((component) => component.metadataCurrent && component.rows.every((row) => row.state === 'SAME') && component.retired.length === 0);
|
|
238
237
|
return { clean: projectForgeCurrent && componentClean, projectForgeCurrent, plan, projectForgeBaseline };
|
|
239
238
|
}
|