@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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.4.1",
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
- "repository": "mriekki/project-forge-core",
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
- "repository": "mriekki/project-forge-deployment-kit",
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": 1,
2
+ "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.4.1",
4
+ "version": "4.5.0",
5
5
  "components": {
6
6
  "core": {
7
- "repository": "mriekki/project-forge-core",
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": "85568ba56681831e34c63aa14f91702522a451e8b2fa883da631d609440d7a7c"
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": "ee5085ac6bbf8fb33ab5f6b52e0ac954c164fd3ea51f90965c9bde86adc88def"
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": "8cd74fbd7433969923299849eb89bdd774fd4af1b9694639f0b9871a1047cacf"
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
- "repository": "mriekki/project-forge-deployment-kit",
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 and briefly explain why. If there is no clear basis, show the choices without a recommendation. The selected answer, not the recommendation, controls what the session does or records.
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 numbered fallback as the normal VS Code path.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.4.1",
3
+ "version": "4.5.0",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {
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 coreMetadataCurrent(baseline, component) {
99
- return baseline.schemaVersion === 1 && baseline.coreRepository === component.repository && baseline.coreVersion === component.version && baseline.coreCommit === component.commit && baseline.distribution === component.distribution && Array.isArray(baseline.managedPaths) && baseline.managedPaths.length > 0;
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 deploymentMetadataCurrent(baseline, component) {
103
- return baseline.schemaVersion === 1 && baseline.deploymentKitRepository === component.repository && baseline.deploymentKitCommit === component.commit && baseline.distribution === component.distribution && Array.isArray(baseline.managedPaths) && baseline.managedPaths.length > 0;
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
- if (Object.keys(baseline).length && baseline.schemaVersion !== 1) throw new Error(`Unsupported ${filename} schemaVersion`);
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: componentId === 'core' ? coreMetadataCurrent(baseline, component) : deploymentMetadataCurrent(baseline, component)
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: 1,
172
+ schemaVersion: 2,
173
+ component: plan.componentId,
174
174
  consumer,
175
175
  consumerBaseline,
176
- deploymentKitRepository: plan.component.repository,
177
- deploymentKitCommit: plan.component.commit,
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: 1,
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
- repository: plan.component.repository,
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 === 1 && projectForgeBaseline.package === packageData.pkg.name && projectForgeBaseline.version === packageData.pkg.version && projectForgeBaseline.profile === profileName;
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
  }