@really-knows-ai/foundry 3.3.9 → 3.5.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/dist/.opencode/plugins/foundry-tools/artefact-tools.js +32 -43
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +9 -2
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +0 -13
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +8 -245
- package/dist/CHANGELOG.md +67 -0
- package/dist/docs/architecture.md +1 -1
- package/dist/docs/tools.md +5 -28
- package/dist/docs/work-spec.md +2 -2
- package/dist/scripts/appraise-module.js +464 -0
- package/dist/scripts/lib/artefacts.js +137 -122
- package/dist/scripts/lib/attestation/attest.js +10 -18
- package/dist/scripts/lib/attestation/payload.js +36 -10
- package/dist/scripts/lib/finalize.js +5 -5
- package/dist/scripts/lib/validation.js +244 -0
- package/dist/scripts/lib/workfile.js +0 -3
- package/dist/scripts/orchestrate-cycle.js +70 -34
- package/dist/scripts/orchestrate-phases.js +68 -38
- package/dist/scripts/orchestrate.js +169 -47
- package/dist/scripts/quench-module.js +162 -0
- package/dist/scripts/sort.js +0 -1
- package/dist/skills/appraise/SKILL.md +3 -3
- package/dist/skills/human-appraise/SKILL.md +6 -7
- package/dist/skills/orchestrate/SKILL.md +33 -6
- package/dist/skills/quench/SKILL.md +4 -4
- package/package.json +5 -5
|
@@ -1,151 +1,166 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Artefact discovery and branch change utilities.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Resolves the branch base SHA, collects changed files on the flow branch,
|
|
5
|
+
* filters by artefact type file-patterns, and returns the artefact change set.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
import { minimatch } from 'minimatch';
|
|
9
|
+
import { sortPaths } from './attestation/hash.js';
|
|
10
|
+
import { getArtefactType } from './config.js';
|
|
11
|
+
|
|
12
|
+
// --- Shared branch artefact discovery ---
|
|
13
|
+
|
|
14
|
+
const STATUS_HANDLERS = {
|
|
15
|
+
A: (parts) => [{ file: parts[1], state: 'new' }],
|
|
16
|
+
M: (parts) => [{ file: parts[1], state: 'modified' }],
|
|
17
|
+
T: (parts) => [{ file: parts[1], state: 'modified' }],
|
|
18
|
+
U: (parts) => [{ file: parts[1], state: 'modified' }],
|
|
19
|
+
D: (parts) => [{ file: parts[1], state: 'deleted' }],
|
|
20
|
+
R: (parts) => [
|
|
21
|
+
{ file: parts[1], state: 'deleted' },
|
|
22
|
+
{ file: parts[2], state: 'new' },
|
|
23
|
+
],
|
|
24
|
+
C: (parts) => [{ file: parts[2], state: 'new' }],
|
|
25
|
+
};
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseTableRow(line) {
|
|
22
|
-
const cols = line.split('|').slice(1, -1).map(c => c.trim());
|
|
23
|
-
return cols.length >= 4 ? cols : null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// --- Status validation ---
|
|
27
|
-
|
|
28
|
-
function validateStatus(newStatus) {
|
|
29
|
-
if (newStatus === 'draft') {
|
|
30
|
-
throw new Error('status draft not permitted; artefacts are registered automatically during orchestration');
|
|
31
|
-
}
|
|
32
|
-
if (!['done', 'blocked'].includes(newStatus)) {
|
|
33
|
-
throw new Error(`invalid status: ${newStatus}`);
|
|
34
|
-
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a single git diff --name-status line into one or more { file, state } entries.
|
|
29
|
+
* Uses a lookup table to map status codes to handlers.
|
|
30
|
+
* @param {string} line - A line from git diff --name-status output
|
|
31
|
+
* @returns {Array<{file: string, state: string}>}
|
|
32
|
+
*/
|
|
33
|
+
function parseDiffStatusLine(line) {
|
|
34
|
+
const parts = line.split('\t');
|
|
35
|
+
const handler = STATUS_HANDLERS[parts[0][0]];
|
|
36
|
+
return handler ? handler(parts) : [];
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Parse git diff --name-status output into an array of { file, state } entries.
|
|
41
|
+
* @param {string} output - Raw output from git diff --name-status
|
|
42
|
+
* @returns {Array<{file: string, state: string}>}
|
|
43
|
+
*/
|
|
44
|
+
function parseDiffOutput(output) {
|
|
45
|
+
const entries = [];
|
|
46
|
+
if (!output) return entries;
|
|
47
|
+
for (const line of output.trim().split('\n')) {
|
|
48
|
+
if (!line) continue;
|
|
49
|
+
for (const entry of parseDiffStatusLine(line)) {
|
|
50
|
+
entries.push(entry);
|
|
51
|
+
}
|
|
42
52
|
}
|
|
43
|
-
return
|
|
53
|
+
return entries;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Collect changed files from the branch since a given base SHA.
|
|
58
|
+
* Combines committed, unstaged, staged, and untracked changes.
|
|
59
|
+
*
|
|
60
|
+
* Sources are ordered by increasing priority: committed, unstaged, staged, untracked.
|
|
61
|
+
* This function does not deduplicate per-file entries; call dedupeArtefactChanges for that.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} branchBaseSha - The merge-base SHA for the branch
|
|
64
|
+
* @param {object} io - IO interface with exec
|
|
65
|
+
* @returns {Array<{file: string, state: string}>}
|
|
66
|
+
*/
|
|
67
|
+
function getBranchChangedFiles(branchBaseSha, io) {
|
|
68
|
+
const changes = [];
|
|
69
|
+
|
|
70
|
+
// Diff-based sources: committed, unstaged, staged
|
|
71
|
+
changes.push(...parseDiffOutput(io.exec(['git', 'diff', '--name-status', `${branchBaseSha}..HEAD`])));
|
|
72
|
+
changes.push(...parseDiffOutput(io.exec(['git', 'diff', '--name-status'])));
|
|
73
|
+
changes.push(...parseDiffOutput(io.exec(['git', 'diff', '--cached', '--name-status'])));
|
|
74
|
+
|
|
75
|
+
// Untracked files (not in git)
|
|
76
|
+
const untracked = io.exec(['git', 'ls-files', '--others', '--exclude-standard']);
|
|
77
|
+
if (untracked) {
|
|
78
|
+
for (const file of untracked.trim().split('\n')) {
|
|
79
|
+
if (!file) continue;
|
|
80
|
+
changes.push({ file, state: 'new' });
|
|
81
|
+
}
|
|
49
82
|
}
|
|
50
|
-
return -1;
|
|
51
|
-
}
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
const headerIdx = findTableHeader(lines);
|
|
55
|
-
if (headerIdx < 0) return null;
|
|
56
|
-
const sepIdx = findTableSeparator(lines, headerIdx);
|
|
57
|
-
if (sepIdx < 0) return null;
|
|
58
|
-
return { headerIdx, sepIdx };
|
|
84
|
+
return changes;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Deduplicate artefact changes, keeping the most recent state for each file.
|
|
89
|
+
* Because later git sources (staged, untracked) take precedence over earlier
|
|
90
|
+
* ones (committed), the last occurrence of a file wins.
|
|
91
|
+
*
|
|
92
|
+
* @param {Array<{file: string, state: string}>} changes
|
|
93
|
+
* @returns {Array<{file: string, state: string}>}
|
|
94
|
+
*/
|
|
95
|
+
function dedupeArtefactChanges(changes) {
|
|
96
|
+
const seen = new Map();
|
|
97
|
+
for (const { file, state } of changes) {
|
|
98
|
+
seen.set(file, state);
|
|
65
99
|
}
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function formatTableRow(cols) {
|
|
70
|
-
return '| ' + cols.join(' | ') + ' |';
|
|
100
|
+
return Array.from(seen.entries()).map(([file, state]) => ({ file, state }));
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
/**
|
|
74
|
-
*
|
|
75
|
-
* @param {
|
|
76
|
-
* @
|
|
104
|
+
* Resolve the merge-base SHA between HEAD and a base branch.
|
|
105
|
+
* @param {object} io - IO interface with exec
|
|
106
|
+
* @param {string} [baseBranch='main'] - Base branch name
|
|
107
|
+
* @returns {string} The merge-base commit SHA
|
|
77
108
|
*/
|
|
78
|
-
export function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!bounds) return [];
|
|
82
|
-
|
|
83
|
-
const artefacts = [];
|
|
84
|
-
const endIdx = findTableEnd(lines, bounds.sepIdx + 1);
|
|
85
|
-
|
|
86
|
-
for (let i = bounds.sepIdx + 1; i < endIdx; i++) {
|
|
87
|
-
const cols = parseTableRow(lines[i].trim());
|
|
88
|
-
if (cols) {
|
|
89
|
-
artefacts.push({
|
|
90
|
-
file: cols[0],
|
|
91
|
-
type: cols[1],
|
|
92
|
-
cycle: cols[2],
|
|
93
|
-
status: cols[3],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
109
|
+
export function resolveBranchBaseSha(io, baseBranch = 'main') {
|
|
110
|
+
if (!io.exec) {
|
|
111
|
+
throw new Error('io.exec is required for resolveBranchBaseSha');
|
|
96
112
|
}
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
const sha = io.exec(['git', 'merge-base', 'HEAD', baseBranch]).trim();
|
|
114
|
+
if (!sha) {
|
|
115
|
+
throw new Error(`Failed to resolve merge-base for HEAD and ${baseBranch}`);
|
|
116
|
+
}
|
|
117
|
+
return sha;
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* @param {
|
|
105
|
-
* @returns {string}
|
|
121
|
+
* Extract file-patterns from an artefact type definition frontmatter.
|
|
122
|
+
* Returns an empty array when frontmatter is absent or contains no patterns.
|
|
123
|
+
* @param {object} def - Artefact type definition with frontmatter
|
|
124
|
+
* @returns {Array<string>}
|
|
106
125
|
*/
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!bounds) {
|
|
112
|
-
throw new Error('Artefacts table not found');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const endIdx = findTableEnd(lines, bounds.sepIdx + 1);
|
|
116
|
-
const insertAt = endIdx > bounds.sepIdx + 1 ? endIdx - 1 : bounds.sepIdx;
|
|
117
|
-
const newRow = `| ${file} | ${type} | ${cycle} | ${status} |`;
|
|
118
|
-
lines.splice(insertAt + 1, 0, newRow);
|
|
119
|
-
return lines.join('\n');
|
|
126
|
+
function getFilePatterns(def) {
|
|
127
|
+
const fm = def.frontmatter;
|
|
128
|
+
return Array.isArray(fm && fm['file-patterns']) ? fm['file-patterns'] : [];
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
132
|
+
* Get artefact files for a given artefact type using shared branch discovery.
|
|
133
|
+
*
|
|
134
|
+
* Reads the artefact type definition, resolves the branch base, collects changed
|
|
135
|
+
* files on the flow branch, filters by the type's file-patterns, and returns a
|
|
136
|
+
* deterministically sorted list of { file, state } entries.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} foundryDir - Path to the foundry directory
|
|
139
|
+
* @param {string} typeId - Artefact type identifier
|
|
140
|
+
* @param {object} io - IO interface with exec, readFile, exists
|
|
141
|
+
* @param {object} [options={}] - Optional parameters
|
|
142
|
+
* @param {string} [options.baseBranch='main'] - Base branch for merge-base resolution
|
|
143
|
+
* @param {string} [options.branchBaseSha] - Pre-resolved merge-base SHA (takes precedence)
|
|
144
|
+
* @returns {Promise<Array<{file: string, state: string}>>}
|
|
128
145
|
*/
|
|
129
|
-
export function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
throw new Error(`File not found in artefacts table: ${file}`);
|
|
146
|
+
export async function getArtefactFiles(foundryDir, typeId, io, options = {}) {
|
|
147
|
+
const def = await getArtefactType(foundryDir, typeId, io);
|
|
148
|
+
const patterns = getFilePatterns(def);
|
|
149
|
+
|
|
150
|
+
if (patterns.length === 0) return [];
|
|
151
|
+
|
|
152
|
+
const baseBranch = options.baseBranch || 'main';
|
|
153
|
+
const branchBaseSha = options.branchBaseSha || resolveBranchBaseSha(io, baseBranch);
|
|
154
|
+
const changedFiles = getBranchChangedFiles(branchBaseSha, io);
|
|
155
|
+
const changes = dedupeArtefactChanges(changedFiles);
|
|
156
|
+
const matching = changes.filter(({ file }) =>
|
|
157
|
+
patterns.some(pattern => minimatch(file, pattern))
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Sort deterministically by file path
|
|
161
|
+
const sorted = sortPaths(matching.map(({ file }) => file));
|
|
162
|
+
const order = new Map(sorted.map((file, idx) => [file, idx]));
|
|
163
|
+
const result = [...matching].sort((a, b) => order.get(a.file) - order.get(b.file));
|
|
164
|
+
|
|
165
|
+
return result;
|
|
151
166
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { load as loadYaml } from 'js-yaml';
|
|
9
9
|
import { parseFrontmatter } from '../workfile.js';
|
|
10
|
-
import { parseArtefactsTable } from '../artefacts.js';
|
|
11
10
|
import { parseAllHistoryEntries } from '../history.js';
|
|
12
11
|
import { sha256Buffer } from './hash.js';
|
|
13
12
|
import { buildAttestationPayload } from './payload.js';
|
|
@@ -21,19 +20,11 @@ function readWorkFiles(cwd, io) {
|
|
|
21
20
|
|
|
22
21
|
return {
|
|
23
22
|
workText: io.readFile(workPath),
|
|
24
|
-
historyText: io.
|
|
25
|
-
feedbackText: io.
|
|
23
|
+
historyText: io.exists(historyPath) ? io.readFile(historyPath) : '',
|
|
24
|
+
feedbackText: io.exists(feedbackPath) ? io.readFile(feedbackPath) : '',
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
function checkBlockedArtefacts(artefacts) {
|
|
30
|
-
const blocked = artefacts.filter(a => a.status === 'blocked');
|
|
31
|
-
if (blocked.length > 0) {
|
|
32
|
-
return `foundry_attest: cycle has blocked artefact(s): ${blocked.map(a => a.file).join(', ')}`;
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
28
|
function checkMissingStages(frontmatter, historyText) {
|
|
38
29
|
const entries = parseAllHistoryEntries(historyText);
|
|
39
30
|
const completed = new Set(entries.map(e => e.stage));
|
|
@@ -54,10 +45,7 @@ function checkUnresolvedFeedback(feedbackText) {
|
|
|
54
45
|
return null;
|
|
55
46
|
}
|
|
56
47
|
|
|
57
|
-
function findCycleError(frontmatter,
|
|
58
|
-
const blockedError = checkBlockedArtefacts(artefacts);
|
|
59
|
-
if (blockedError) return blockedError;
|
|
60
|
-
|
|
48
|
+
function findCycleError(frontmatter, historyText, feedbackText) {
|
|
61
49
|
const missingError = checkMissingStages(frontmatter, historyText);
|
|
62
50
|
if (missingError) return missingError;
|
|
63
51
|
|
|
@@ -73,7 +61,9 @@ function computeDiffSha(execGit, baseBranch) {
|
|
|
73
61
|
|
|
74
62
|
export async function buildAttestation({
|
|
75
63
|
cwd,
|
|
64
|
+
foundryDir,
|
|
76
65
|
baseBranch,
|
|
66
|
+
branchBaseSha,
|
|
77
67
|
goalText,
|
|
78
68
|
archiveBranch,
|
|
79
69
|
archiveTipSha,
|
|
@@ -82,20 +72,22 @@ export async function buildAttestation({
|
|
|
82
72
|
}) {
|
|
83
73
|
const { workText, historyText, feedbackText } = readWorkFiles(cwd, io);
|
|
84
74
|
const frontmatter = parseFrontmatter(workText);
|
|
85
|
-
const artefacts = parseArtefactsTable(workText);
|
|
86
75
|
|
|
87
|
-
const cycleError = findCycleError(frontmatter,
|
|
76
|
+
const cycleError = findCycleError(frontmatter, historyText, feedbackText);
|
|
88
77
|
if (cycleError) {
|
|
89
78
|
return { ok: false, error: cycleError };
|
|
90
79
|
}
|
|
91
80
|
|
|
92
81
|
const diffSha = computeDiffSha(execGit, baseBranch);
|
|
93
82
|
|
|
94
|
-
const payload = buildAttestationPayload({
|
|
83
|
+
const payload = await buildAttestationPayload({
|
|
95
84
|
cwd,
|
|
85
|
+
foundryDir: foundryDir ?? 'foundry',
|
|
96
86
|
goalText,
|
|
97
87
|
archiveBranch,
|
|
98
88
|
archiveTipSha,
|
|
89
|
+
baseBranch,
|
|
90
|
+
branchBaseSha,
|
|
99
91
|
io,
|
|
100
92
|
});
|
|
101
93
|
|
|
@@ -5,26 +5,29 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { parseFrontmatter } from '../workfile.js';
|
|
10
|
-
import {
|
|
11
|
+
import { getArtefactFiles } from '../artefacts.js';
|
|
12
|
+
import { getCycleDefinition } from '../config.js';
|
|
11
13
|
import { parseAllHistoryEntries } from '../history.js';
|
|
12
14
|
import { sha256Text, sortPaths } from './hash.js';
|
|
13
15
|
|
|
14
|
-
function defaultIo() {
|
|
16
|
+
function defaultIo(cwd) {
|
|
15
17
|
return {
|
|
16
18
|
readFile: (filePath) => readFileSync(filePath, 'utf8'),
|
|
17
|
-
|
|
19
|
+
exists: (filePath) => existsSync(filePath),
|
|
20
|
+
exec: (args) => execFileSync(args[0], args.slice(1), { cwd, encoding: 'utf8' }),
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function readWorkFiles(cwd, io) {
|
|
22
|
-
const { readFile,
|
|
25
|
+
const { readFile, exists } = io ?? defaultIo(cwd);
|
|
23
26
|
|
|
24
27
|
return {
|
|
25
28
|
workText: readFile(path.join(cwd, 'WORK.md')),
|
|
26
|
-
historyText:
|
|
27
|
-
feedbackText:
|
|
29
|
+
historyText: exists(path.join(cwd, 'WORK.history.yaml')) ? readFile(path.join(cwd, 'WORK.history.yaml')) : '',
|
|
30
|
+
feedbackText: exists(path.join(cwd, 'WORK.feedback.yaml')) ? readFile(path.join(cwd, 'WORK.feedback.yaml')) : '',
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -85,11 +88,34 @@ function buildGovernance(frontmatter, workText, historyText, feedbackText) {
|
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
async function discoverCycleOutputs(resolvedFd, cycleId, resolvedIo, options) {
|
|
92
|
+
try {
|
|
93
|
+
const cfm = (await getCycleDefinition(resolvedFd, cycleId, resolvedIo)).frontmatter;
|
|
94
|
+
const outputType = cfm && cfm['output-type'];
|
|
95
|
+
if (outputType) return getArtefactFiles(resolvedFd, outputType, resolvedIo, options);
|
|
96
|
+
} catch {
|
|
97
|
+
// If cycle definition is missing, outputs remain empty
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function buildAttestationPayload(
|
|
103
|
+
{ cwd, foundryDir: fd, goalText, archiveBranch, archiveTipSha, baseBranch, branchBaseSha, io },
|
|
104
|
+
) {
|
|
105
|
+
const resolvedIo = io || defaultIo(cwd);
|
|
106
|
+
const resolvedFd = fd || 'foundry';
|
|
107
|
+
const { workText, historyText, feedbackText } = readWorkFiles(cwd, resolvedIo);
|
|
90
108
|
|
|
91
109
|
const frontmatter = parseFrontmatter(workText);
|
|
92
|
-
|
|
110
|
+
|
|
111
|
+
// Discover artefact outputs from branch changes
|
|
112
|
+
let outputs = [];
|
|
113
|
+
const cycleId = frontmatter.cycle;
|
|
114
|
+
if (cycleId) {
|
|
115
|
+
outputs = await discoverCycleOutputs(resolvedFd, cycleId, resolvedIo, { baseBranch, branchBaseSha });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const outputEntries = outputs.map(({ file, state }) => ({ path: file, state }));
|
|
93
119
|
|
|
94
120
|
const sortedEntries = parseAndSortHistoryEntries(historyText);
|
|
95
121
|
const stages = buildStagesFromEntries(sortedEntries);
|
|
@@ -97,7 +123,7 @@ export function buildAttestationPayload({ cwd, goalText, archiveBranch, archiveT
|
|
|
97
123
|
return {
|
|
98
124
|
contract: buildContract(frontmatter),
|
|
99
125
|
governance: buildGovernance(frontmatter, workText, historyText, feedbackText),
|
|
100
|
-
outputs:
|
|
126
|
+
outputs: outputEntries,
|
|
101
127
|
process: { stages },
|
|
102
128
|
request: { goal_text: goalText },
|
|
103
129
|
schema: 'foundry-attestation/v1',
|
|
@@ -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,
|
|
52
|
+
export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, io }) {
|
|
53
53
|
if (!io?.exec) {
|
|
54
54
|
throw new Error('finalizeStage: io.exec is required');
|
|
55
55
|
}
|
|
@@ -62,9 +62,9 @@ export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes
|
|
|
62
62
|
// For non-forge stages, matched files are tool-managed side effects
|
|
63
63
|
// (e.g. assay's memory writes) that should not become artefacts.
|
|
64
64
|
if (stageBase !== 'forge') return { ok: true, artefacts: [], changedFiles: sortedFiles };
|
|
65
|
-
const artefacts = sortedFiles.map(file => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
65
|
+
const artefacts = sortedFiles.map(file => ({
|
|
66
|
+
file,
|
|
67
|
+
type: cycleDef.outputArtefactType,
|
|
68
|
+
}));
|
|
69
69
|
return { ok: true, artefacts, changedFiles: sortedFiles };
|
|
70
70
|
}
|