@really-knows-ai/foundry 3.5.9 → 3.6.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/dist/.opencode/plugins/foundry-tools/feedback-tools.js +9 -5
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +3 -1
- package/dist/CHANGELOG.md +25 -0
- package/dist/scripts/appraise-module.js +69 -7
- package/dist/scripts/lib/artefacts.js +43 -1
- package/dist/scripts/lib/feedback-store.js +34 -8
- package/dist/scripts/lib/finalize.js +10 -2
- package/dist/scripts/lib/forge-contract.js +93 -0
- package/dist/scripts/lib/history.js +2 -1
- package/dist/scripts/lib/sort-reason.js +3 -1
- package/dist/scripts/lib/sort-routing.js +172 -128
- package/dist/scripts/lib/workfile.js +28 -0
- package/dist/scripts/orchestrate-phases.js +49 -39
- package/dist/scripts/orchestrate-terminals.js +37 -2
- package/dist/scripts/orchestrate.js +62 -5
- package/dist/scripts/quench-module.js +54 -12
- package/dist/scripts/sort.js +28 -11
- package/package.json +1 -1
|
@@ -70,13 +70,17 @@ async function executeFeedbackAdd(args, context) {
|
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
73
|
-
const
|
|
73
|
+
const params = {
|
|
74
74
|
file: args.file,
|
|
75
75
|
tag: args.tag,
|
|
76
76
|
text: args.text,
|
|
77
77
|
source: activeStage,
|
|
78
78
|
cycle,
|
|
79
|
-
}
|
|
79
|
+
};
|
|
80
|
+
if (args.artefact_version !== undefined) {
|
|
81
|
+
params.artefact_version = args.artefact_version;
|
|
82
|
+
}
|
|
83
|
+
const { id, deduped } = store.add(params);
|
|
80
84
|
return JSON.stringify({ ok: true, id, deduped });
|
|
81
85
|
} catch (err) {
|
|
82
86
|
return JSON.stringify({ error: `foundry_feedback_add: ${err.message}` });
|
|
@@ -201,6 +205,7 @@ export function createFeedbackTools({ tool }) {
|
|
|
201
205
|
file: tool.schema.string().describe('Artefact file path'),
|
|
202
206
|
text: tool.schema.string().describe('Feedback text'),
|
|
203
207
|
tag: tool.schema.string().describe('Tag for the feedback item'),
|
|
208
|
+
artefact_version: tool.schema.string().optional().describe('SHA-256 hash for version-aware sorting'),
|
|
204
209
|
},
|
|
205
210
|
execute: guarded('foundry_feedback_add', [flowBranchGuard, gateNotFailed], executeFeedbackAdd, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
206
211
|
}),
|
|
@@ -218,7 +223,7 @@ export function createFeedbackTools({ tool }) {
|
|
|
218
223
|
execute: guarded('foundry_feedback_wontfix', [flowBranchGuard, gateNotFailed], executeFeedbackWontfix, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
219
224
|
}),
|
|
220
225
|
foundry_feedback_resolve: tool({
|
|
221
|
-
description: 'Resolve a feedback item (approved or rejected).
|
|
226
|
+
description: 'Resolve a feedback item (approved or rejected). Human-appraise stages can override deadlock with a reason.',
|
|
222
227
|
args: {
|
|
223
228
|
id: tool.schema.string().describe('Feedback item id (ULID)'),
|
|
224
229
|
resolution: tool.schema.enum(['approved', 'rejected']).describe('Resolution type'),
|
|
@@ -230,7 +235,6 @@ export function createFeedbackTools({ tool }) {
|
|
|
230
235
|
args: {
|
|
231
236
|
file: tool.schema.string().optional().describe('Filter by artefact file path'),
|
|
232
237
|
},
|
|
233
|
-
execute: executeFeedbackList,
|
|
234
|
-
}),
|
|
238
|
+
execute: executeFeedbackList }),
|
|
235
239
|
};
|
|
236
240
|
}
|
|
@@ -38,7 +38,7 @@ function createGitBridge(cwd) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async function createFinalize(cwd, io) {
|
|
41
|
-
return async ({ cycleId, stage, baseSha }) => {
|
|
41
|
+
return async ({ cycleId, stage, baseSha, artefact_version, contractPassed }) => {
|
|
42
42
|
let cycleDoc;
|
|
43
43
|
try {
|
|
44
44
|
cycleDoc = await getCycleDefinition('foundry', cycleId, io);
|
|
@@ -66,6 +66,8 @@ async function createFinalize(cwd, io) {
|
|
|
66
66
|
cycleDef,
|
|
67
67
|
artefactTypes,
|
|
68
68
|
io,
|
|
69
|
+
artefact_version,
|
|
70
|
+
contractPassed,
|
|
69
71
|
});
|
|
70
72
|
return result;
|
|
71
73
|
};
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.6.0] - 2026-05-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- State-driven sort routing (R1–R4, R7): sort routes based on feedback item state (`unresolved` → forge, `addressed` → source stage) instead of stage position, with deadlock detection and iteration cap.
|
|
8
|
+
- Artefact version tracking: `computeArtefactVersion` hashes all artefact files after each forge run; the version is recorded on every feedback item and forge history entry.
|
|
9
|
+
- Forge contract enforcement (R8–R9): per-item response check and batch-level version consistency check. Contract violations revert items to `open` and post system feedback. Three consecutive contract failures block the cycle.
|
|
10
|
+
- Stale feedback detection (R6): when quench, appraise, or human-appraise re-enters after a forge run, feedback items with a mismatched artefact version are auto-resolved as superseded.
|
|
11
|
+
- Legacy feedback migration: items without a valid artefact version are auto-resolved on load and excluded from routing.
|
|
12
|
+
- Integration test suite: 40 integration tests covering the routing decision tree, forge contract enforcement, and the spec's 10-step worked example.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Forge contract enforcement wired into the orchestration post-dispatch path (previously it finalised without computing versions or checking the contract).
|
|
17
|
+
- New feedback items from quench, appraise, and the plugin feedback tool now carry the current artefact version, preventing false legacy detection.
|
|
18
|
+
- `computeArtefactVersion` scans the worktree root for file patterns (previously scanned the `foundry/` config directory).
|
|
19
|
+
- Version computation failures during stale resolution are surfaced to the orchestrator instead of being silently swallowed.
|
|
20
|
+
- Appraise stale feedback resolution runs during the gather phase (before appraiser dispatch), not only during consolidation.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Orchestration: forge post-dispatch extracts a dedicated `runForgePostDispatch` path that reads the forge context, computes versions, enforces the contract, and passes `contractPassed`/`postVersion` to `finaliseStage`.
|
|
25
|
+
- Quench and appraise stale resolution helpers gracefully degrade when the artefact type is not configured (best-effort, not fatal).
|
|
26
|
+
- Added `worktree` parameter to `computeArtefactVersion` for explicit worktree-root specification.
|
|
27
|
+
|
|
3
28
|
## [3.5.9] - 2026-05-24
|
|
4
29
|
|
|
5
30
|
### Changed
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* so the orchestrator can re-sort and determine the next action.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { getArtefactFiles } from './lib/artefacts.js';
|
|
14
|
+
import { getArtefactFiles, computeArtefactVersion } from './lib/artefacts.js';
|
|
15
15
|
import { selectAppraisers, getLaws, getCycleDefinition } from './lib/config.js';
|
|
16
|
+
import { openFeedbackStore } from './lib/feedback-store.js';
|
|
16
17
|
import yaml from 'js-yaml';
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +40,8 @@ export async function gatherAppraiseContext(ctx) {
|
|
|
39
40
|
return violation('cycleId is required', []);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
await resolveStaleAppraiseFeedback(ctx);
|
|
44
|
+
|
|
42
45
|
const cd = await getCycleDefinition(ctx.foundryDir, ctx.cycleId, ctx.io);
|
|
43
46
|
const outputType = cd.frontmatter['output-type'];
|
|
44
47
|
if (!outputType) {
|
|
@@ -147,11 +150,51 @@ function emptyDispatch(cycleId) {
|
|
|
147
150
|
// ---------------------------------------------------------------------------
|
|
148
151
|
|
|
149
152
|
/**
|
|
150
|
-
*
|
|
153
|
+
* Resolve stale feedback items whose artefact version does not match the
|
|
154
|
+
* current on-disk version. Items from this stage's source base with a
|
|
155
|
+
* mismatched artefact_version are auto-resolved as superseded.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveStaleFeedback(items, currentVersion, stageBase, feedback, cycle) {
|
|
158
|
+
for (const item of items) {
|
|
159
|
+
if (shouldSkipStaleResolve(item, currentVersion, stageBase)) continue;
|
|
160
|
+
const reason = `superseded by forge revision ${currentVersion}`;
|
|
161
|
+
feedback.autoResolve({ id: item.id, reason, cycle });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function shouldSkipStaleResolve(item, currentVersion, stageBase) {
|
|
166
|
+
if (item.history[0].state === 'resolved') return true;
|
|
167
|
+
const itemBase = typeof item.source === 'string' ? item.source.split(':')[0] : '';
|
|
168
|
+
if (itemBase !== stageBase) return true;
|
|
169
|
+
if (item.artefact_version === currentVersion) return true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve stale appraise-sourced feedback. Errors propagate to the caller
|
|
175
|
+
* which must handle them (e.g. by returning a violation).
|
|
176
|
+
*/
|
|
177
|
+
async function resolveStaleAppraiseFeedback(ctx) {
|
|
178
|
+
try {
|
|
179
|
+
const cycleDef = await getCycleDefinition(ctx.foundryDir, ctx.cycleId, ctx.io);
|
|
180
|
+
const outputType = cycleDef.frontmatter['output-type'];
|
|
181
|
+
if (outputType) {
|
|
182
|
+
const store = openFeedbackStore('WORK.feedback.yaml', ctx.io);
|
|
183
|
+
const currentVersion = await computeArtefactVersion(ctx.foundryDir, outputType, ctx.io, ctx.cwd);
|
|
184
|
+
resolveStaleFeedback(store.list(), currentVersion, 'appraise', store, ctx.cycleId);
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Graceful degrade — stale resolution is best-effort.
|
|
188
|
+
// The orchestrator handles IO failures at the cycle level.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Consolidate appraiser results and finalise the appraise stage.
|
|
151
194
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
195
|
+
* Called by orchestrator after all appraisers have completed. Parses results,
|
|
196
|
+
* posts combined feedback, resolves prior appraise feedback, and advances
|
|
197
|
+
* the cycle to the next stage via finalize.
|
|
155
198
|
*
|
|
156
199
|
* @param {object} ctx
|
|
157
200
|
* @param {Array<{ok: boolean, output?: string, error?: string}>} lastResults
|
|
@@ -170,10 +213,13 @@ export async function consolidateAppraise(ctx, lastResults) {
|
|
|
170
213
|
return violation('All appraisers failed to evaluate the artefact', []);
|
|
171
214
|
}
|
|
172
215
|
|
|
216
|
+
await resolveStaleAppraiseFeedback(ctx);
|
|
217
|
+
|
|
173
218
|
const consolidated = parseConsolidated(successful);
|
|
174
219
|
const stageId = `appraise:${ctx.cycleId}`;
|
|
175
220
|
|
|
176
|
-
|
|
221
|
+
const artefactVersion = await computeAppraiseArtefactVersion(ctx);
|
|
222
|
+
postConsolidatedFeedback(ctx, consolidated, artefactVersion);
|
|
177
223
|
resolvePriorAppraise(ctx, consolidated, stageId);
|
|
178
224
|
|
|
179
225
|
const summary = buildConsolidateSummary(consolidated.length);
|
|
@@ -219,15 +265,31 @@ function deduplicateIssues(issues) {
|
|
|
219
265
|
return result;
|
|
220
266
|
}
|
|
221
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Compute artefact version for the appraise cycle so feedback items carry
|
|
270
|
+
* a version hash and are not auto-resolved by sort as legacy items.
|
|
271
|
+
*/
|
|
272
|
+
async function computeAppraiseArtefactVersion(ctx) {
|
|
273
|
+
try {
|
|
274
|
+
const cycleDef = await getCycleDefinition(ctx.foundryDir, ctx.cycleId, ctx.io);
|
|
275
|
+
const outputType = cycleDef.frontmatter['output-type'];
|
|
276
|
+
if (outputType) {
|
|
277
|
+
return await computeArtefactVersion(ctx.foundryDir, outputType, ctx.io, ctx.cwd);
|
|
278
|
+
}
|
|
279
|
+
} catch { /* skip */ }
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
222
283
|
/**
|
|
223
284
|
* Post one feedback item per consolidated issue.
|
|
224
285
|
*/
|
|
225
|
-
function postConsolidatedFeedback(ctx, consolidated) {
|
|
286
|
+
function postConsolidatedFeedback(ctx, consolidated, artefactVersion) {
|
|
226
287
|
for (const issue of consolidated) {
|
|
227
288
|
ctx.feedback.add({
|
|
228
289
|
file: issue.file,
|
|
229
290
|
text: issue.issue,
|
|
230
291
|
tag: `law:${issue.law}`,
|
|
292
|
+
artefact_version: artefactVersion,
|
|
231
293
|
});
|
|
232
294
|
}
|
|
233
295
|
}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { minimatch } from 'minimatch';
|
|
9
|
-
import { sortPaths } from './attestation/hash.js';
|
|
9
|
+
import { sortPaths, sha256Text } from './attestation/hash.js';
|
|
10
10
|
import { getArtefactType } from './config.js';
|
|
11
|
+
import { expandPatterns } from './validation.js';
|
|
11
12
|
|
|
12
13
|
// --- Shared branch artefact discovery ---
|
|
13
14
|
|
|
@@ -164,3 +165,44 @@ export async function getArtefactFiles(foundryDir, typeId, io, options = {}) {
|
|
|
164
165
|
|
|
165
166
|
return result;
|
|
166
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compute the artefact version hash for a given artefact type.
|
|
171
|
+
*
|
|
172
|
+
* Reads the artefact type definition, expands its file patterns across the
|
|
173
|
+
* worktree, and computes a SHA-256 hash over all matching files. Each file
|
|
174
|
+
* contributes `sha256(filePath + ":" + content)` and the per-file hashes are
|
|
175
|
+
* joined with "\n" before the final SHA-256.
|
|
176
|
+
*
|
|
177
|
+
* When no patterns are defined or no files match, returns the SHA-256 of an
|
|
178
|
+
* empty input (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855).
|
|
179
|
+
*
|
|
180
|
+
* @param {string} foundryDir - Path to the foundry config directory
|
|
181
|
+
* @param {string} typeId - Artefact type identifier
|
|
182
|
+
* @param {object} io - IO interface with readFile(path, encoding)
|
|
183
|
+
* @param {string} [worktree] - Worktree root for pattern expansion (defaults to foundryDir)
|
|
184
|
+
* @returns {Promise<string>} SHA-256 hex string (64 characters)
|
|
185
|
+
* @throws {Error} On IO errors (unknown type, file read failure, glob error)
|
|
186
|
+
*/
|
|
187
|
+
export async function computeArtefactVersion(foundryDir, typeId, io, worktree = foundryDir) {
|
|
188
|
+
const def = await getArtefactType(foundryDir, typeId, io);
|
|
189
|
+
const patterns = getFilePatterns(def);
|
|
190
|
+
|
|
191
|
+
if (patterns.length === 0) {
|
|
192
|
+
return sha256Text('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const files = await expandPatterns(patterns, worktree);
|
|
196
|
+
|
|
197
|
+
if (files.length === 0) {
|
|
198
|
+
return sha256Text('');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const perFileHashes = await Promise.all(files.map(async file => {
|
|
202
|
+
const content = await io.readFile(file, 'utf-8');
|
|
203
|
+
return sha256Text(file + ':' + content);
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
const joined = perFileHashes.join('\n');
|
|
207
|
+
return sha256Text(joined);
|
|
208
|
+
}
|
|
@@ -5,11 +5,11 @@ import { validateTransition, hashText, canForgeWontFix } from './feedback-transi
|
|
|
5
5
|
|
|
6
6
|
const YAML_OPTS = { lineWidth: -1 };
|
|
7
7
|
|
|
8
|
-
const VALID_SOURCE_BASES = new Set(['forge', 'quench', 'appraise', 'human-appraise']);
|
|
8
|
+
const VALID_SOURCE_BASES = new Set(['forge', 'quench', 'appraise', 'human-appraise', 'system']);
|
|
9
9
|
|
|
10
10
|
function validateSourceBase(base) {
|
|
11
11
|
if (!VALID_SOURCE_BASES.has(base)) {
|
|
12
|
-
throw new Error(`unknown source base: ${base} (expected one of: forge, quench, appraise, human-appraise)`);
|
|
12
|
+
throw new Error(`unknown source base: ${base} (expected one of: forge, quench, appraise, human-appraise, system)`);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -96,6 +96,12 @@ export function openFeedbackStore(path, io) {
|
|
|
96
96
|
timestamp: nowIso, persist,
|
|
97
97
|
});
|
|
98
98
|
},
|
|
99
|
+
autoResolve({ id, reason, cycle }) {
|
|
100
|
+
return storeAutoResolve({ id, reason, cycle, items, persist, timestamp: nowIso });
|
|
101
|
+
},
|
|
102
|
+
forceState(id, state, cycle) {
|
|
103
|
+
return storeForceState({ id, state, cycle, items, persist });
|
|
104
|
+
},
|
|
99
105
|
resolveSystemItems(stage, cycle) {
|
|
100
106
|
resolveSystemItemsImpl({ items, stage, cycle, timestamp: nowIso, persist });
|
|
101
107
|
},
|
|
@@ -109,20 +115,21 @@ function isDuplicate(item, file, tag, textHash, stateOf) {
|
|
|
109
115
|
stateOf(item) !== 'resolved';
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
function assertAddParams(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
118
|
+
function assertAddParams(file, tag, text, source, cycle) {
|
|
119
|
+
const missing = typeof file !== 'string' || [tag, text, source, cycle].some(v => !v);
|
|
120
|
+
if (missing) throw new Error('add requires file, tag, text, source, cycle');
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
function storeAdd(params, items, deps) {
|
|
119
|
-
const { file, tag, text, source, cycle } = params;
|
|
124
|
+
const { file, tag, text, source, cycle, artefact_version } = params;
|
|
120
125
|
const { hashFn, stateOf, validateSrc, persist, makeUlid, timestamp } = deps;
|
|
121
126
|
assertAddParams(file, tag, text, source, cycle);
|
|
122
127
|
validateSrc(source);
|
|
123
128
|
|
|
124
129
|
const textHash = hashFn(text);
|
|
125
|
-
const existing = items.find(it =>
|
|
130
|
+
const existing = items.find(it =>
|
|
131
|
+
it.artefact_version === artefact_version && isDuplicate(it, file, tag, textHash, stateOf)
|
|
132
|
+
);
|
|
126
133
|
if (existing) return { id: existing.id, deduped: true };
|
|
127
134
|
|
|
128
135
|
const id = makeUlid();
|
|
@@ -130,6 +137,9 @@ function storeAdd(params, items, deps) {
|
|
|
130
137
|
id, file, tag, text, source,
|
|
131
138
|
history: [{ state: 'open', stage: source, cycle, timestamp: timestamp() }],
|
|
132
139
|
};
|
|
140
|
+
if (artefact_version !== undefined) {
|
|
141
|
+
item.artefact_version = artefact_version;
|
|
142
|
+
}
|
|
133
143
|
persist([...items, item]);
|
|
134
144
|
return { id, deduped: false };
|
|
135
145
|
}
|
|
@@ -202,4 +212,20 @@ function storeTransition(params, items, deps) {
|
|
|
202
212
|
return { ok: true };
|
|
203
213
|
}
|
|
204
214
|
|
|
215
|
+
function storeForceState({ id, state, cycle, items, persist }) {
|
|
216
|
+
const item = items.find(x => x.id === id);
|
|
217
|
+
if (!item) return { ok: false, error: `feedback item not found: ${id}` };
|
|
218
|
+
const snapshot = { state, stage: 'system:forge-contract-mismatch', cycle, timestamp: nowIso() };
|
|
219
|
+
persist(applyTransition(items, id, snapshot));
|
|
220
|
+
return { ok: true };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function storeAutoResolve({ id, reason, cycle, items, persist, timestamp }) {
|
|
224
|
+
const item = items.find(x => x.id === id);
|
|
225
|
+
if (!item) return { ok: false, error: `feedback item not found: ${id}` };
|
|
226
|
+
const snapshot = { state: 'resolved', stage: 'system', cycle, reason, timestamp: timestamp() };
|
|
227
|
+
persist(applyTransition(items, id, snapshot));
|
|
228
|
+
return { ok: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
205
231
|
|
|
@@ -49,7 +49,7 @@ function classifyFiles(files, allowedPatterns) {
|
|
|
49
49
|
return { matched, unexpected };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, io }) {
|
|
52
|
+
export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, io, artefact_version }) {
|
|
53
53
|
if (!io?.exec) {
|
|
54
54
|
throw new Error('finalizeStage: io.exec is required');
|
|
55
55
|
}
|
|
@@ -66,5 +66,13 @@ export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes
|
|
|
66
66
|
file,
|
|
67
67
|
type: cycleDef.outputArtefactType,
|
|
68
68
|
}));
|
|
69
|
-
return
|
|
69
|
+
return forgeResult(artefacts, sortedFiles, artefact_version);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function forgeResult(artefacts, files, artefact_version) {
|
|
73
|
+
const result = { ok: true, artefacts, changedFiles: files };
|
|
74
|
+
if (artefact_version !== undefined) {
|
|
75
|
+
result.artefact_version = artefact_version;
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
70
78
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge contract enforcement — validates that forge responded to every
|
|
3
|
+
* presented feedback item and that the batch-level artefact version
|
|
4
|
+
* semantics are satisfied.
|
|
5
|
+
*
|
|
6
|
+
* Per-item check: every item must end in 'actioned' or 'wont-fix'.
|
|
7
|
+
* Batch-level check: if any item is actioned, version must change.
|
|
8
|
+
* If no item is actioned, version must be unchanged.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function currentState(feedbackStore, id) {
|
|
12
|
+
const item = feedbackStore.get(id);
|
|
13
|
+
return item ? item.history[0].state : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function revertAll(items, feedbackStore, cycleId) {
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
feedbackStore.forceState(item.id, 'open', cycleId);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function postSystemFeedback(feedbackStore, cycleId, postVersion, text) {
|
|
23
|
+
feedbackStore.add({
|
|
24
|
+
file: '',
|
|
25
|
+
tag: 'system:forge-contract-mismatch',
|
|
26
|
+
text,
|
|
27
|
+
source: 'system:forge-contract-mismatch',
|
|
28
|
+
artefact_version: postVersion,
|
|
29
|
+
cycle: cycleId,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checkPerItemResponse(items, feedbackStore, cycleId, postVersion) {
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const state = currentState(feedbackStore, item.id);
|
|
36
|
+
if (state !== 'actioned' && state !== 'wont-fix') {
|
|
37
|
+
revertAll(items, feedbackStore, cycleId);
|
|
38
|
+
postSystemFeedback(
|
|
39
|
+
feedbackStore, cycleId, postVersion,
|
|
40
|
+
'forge did not respond to every presented feedback item',
|
|
41
|
+
);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasActionedItem(items, feedbackStore) {
|
|
49
|
+
return items.some(item => currentState(feedbackStore, item.id) === 'actioned');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkBatchVersion(items, feedbackStore, cycleId, postVersion, preVersion) {
|
|
53
|
+
const hasActioned = hasActionedItem(items, feedbackStore);
|
|
54
|
+
|
|
55
|
+
if (hasActioned && preVersion === postVersion) {
|
|
56
|
+
revertAll(items, feedbackStore, cycleId);
|
|
57
|
+
postSystemFeedback(
|
|
58
|
+
feedbackStore, cycleId, postVersion,
|
|
59
|
+
'forge marked feedback as actioned without changing artefacts',
|
|
60
|
+
);
|
|
61
|
+
return { contractPassed: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!hasActioned && preVersion !== postVersion) {
|
|
65
|
+
revertAll(items, feedbackStore, cycleId);
|
|
66
|
+
postSystemFeedback(
|
|
67
|
+
feedbackStore, cycleId, postVersion,
|
|
68
|
+
'forge changed artefacts but did not mark any feedback as actioned',
|
|
69
|
+
);
|
|
70
|
+
return { contractPassed: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { contractPassed: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Enforce the forge contract on a batch of items presented to forge.
|
|
78
|
+
*
|
|
79
|
+
* Two-level check:
|
|
80
|
+
* 1. Per-item: every item must end in 'actioned' or 'wont-fix'.
|
|
81
|
+
* 2. Batch-level: artefact version semantics must be consistent.
|
|
82
|
+
*
|
|
83
|
+
* @param {{ items: Array<{id: string}>, preVersion: string, postVersion: string,
|
|
84
|
+
* feedbackStore: object, cycleId: string }} params
|
|
85
|
+
* @returns {{ contractPassed: boolean }}
|
|
86
|
+
*/
|
|
87
|
+
export function enforceForgeContract({ items, preVersion, postVersion, feedbackStore, cycleId }) {
|
|
88
|
+
if (!checkPerItemResponse(items, feedbackStore, cycleId, postVersion)) {
|
|
89
|
+
return { contractPassed: false };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return checkBatchVersion(items, feedbackStore, cycleId, postVersion, preVersion);
|
|
93
|
+
}
|
|
@@ -78,7 +78,7 @@ function validateAppendArgs({ iteration, comment, route, stage }) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function buildEntry({ cycle, stage, iteration, comment, route, openFeedback, changedFiles }, seq) {
|
|
81
|
+
function buildEntry({ cycle, stage, iteration, comment, route, openFeedback, changedFiles, ...rest }, seq) {
|
|
82
82
|
const entry = {
|
|
83
83
|
cycle,
|
|
84
84
|
stage,
|
|
@@ -87,6 +87,7 @@ function buildEntry({ cycle, stage, iteration, comment, route, openFeedback, cha
|
|
|
87
87
|
timestamp: new Date().toISOString(),
|
|
88
88
|
seq,
|
|
89
89
|
open_feedback: openFeedback ?? 0,
|
|
90
|
+
...rest,
|
|
90
91
|
};
|
|
91
92
|
if (route !== undefined) entry.route = route;
|
|
92
93
|
if (changedFiles !== undefined) {
|
|
@@ -18,7 +18,9 @@ export function reasonForRoute(route, prep) {
|
|
|
18
18
|
|
|
19
19
|
function buildReasonData(route, prep) {
|
|
20
20
|
const base = baseStage(route);
|
|
21
|
-
const forgeCount = prep.history.filter(e =>
|
|
21
|
+
const forgeCount = prep.history.filter(e =>
|
|
22
|
+
baseStage(e.stage || '') === 'forge' && e.contract_passed !== false,
|
|
23
|
+
).length;
|
|
22
24
|
const maxIt = prep.defaults.maxIterations;
|
|
23
25
|
const feedback = prep.feedback || [];
|
|
24
26
|
const openCount = feedback.filter(f => f.state !== 'resolved').length;
|