@really-knows-ai/foundry 2.3.2 → 3.0.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/README.md +180 -369
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +393 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +46 -24
- package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -100
- package/docs/concepts.md +0 -122
- package/docs/getting-started.md +0 -187
- package/docs/work-spec.md +0 -207
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -111
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Extractor-level permission checks. Two scopes:
|
|
2
|
+
// 1. Cycle-load: extractor.memoryWrite ⊆ cycle.memory.write
|
|
3
|
+
// 2. Runtime per-row: a given entity or edge row is within the extractor's scope.
|
|
4
|
+
//
|
|
5
|
+
// The edge rule mirrors scripts/lib/memory/permissions.js: an edge is permitted
|
|
6
|
+
// if either of its endpoint entity types is in the extractor's memoryWrite.
|
|
7
|
+
|
|
8
|
+
export function checkExtractorAgainstCycle(extractor, cyclePerms) {
|
|
9
|
+
const missing = extractor.memoryWrite.filter((t) => !cyclePerms.writeTypes.has(t));
|
|
10
|
+
if (missing.length === 0) return { ok: true };
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: `extractor '${extractor.name}' declares memory.write types not permitted by the cycle: ${missing.join(', ')}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function checkEntityRowAgainstExtractor(extractor, entityType) {
|
|
18
|
+
if (extractor.memoryWrite.includes(entityType)) return { ok: true };
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: `extractor '${extractor.name}': entity type '${entityType}' is not in memory.write (${extractor.memoryWrite.join(', ')})`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function lookupInVocabulary(collection, typeName, extractorName, edgeType, label) {
|
|
26
|
+
if (collection?.[typeName]) return null;
|
|
27
|
+
return { ok: false, error: `extractor '${extractorName}': edge '${edgeType}' ${label} '${typeName}' not declared in project vocabulary` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateEdgeTypesInVocabulary(extractor, edge, vocabulary) {
|
|
31
|
+
if (!vocabulary.edges?.[edge.edge_type]) {
|
|
32
|
+
return { ok: false, error: `extractor '${extractor.name}': edge type '${edge.edge_type}' not declared in project vocabulary` };
|
|
33
|
+
}
|
|
34
|
+
const fromError = lookupInVocabulary(vocabulary.entities, edge.from_type, extractor.name, edge.edge_type, 'from_type');
|
|
35
|
+
if (fromError) return fromError;
|
|
36
|
+
return lookupInVocabulary(vocabulary.entities, edge.to_type, extractor.name, edge.edge_type, 'to_type');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isEdgeWritable(extractor, edge) {
|
|
40
|
+
const writable = new Set(extractor.memoryWrite);
|
|
41
|
+
return writable.has(edge.from_type) || writable.has(edge.to_type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function checkEdgeRowAgainstExtractor(extractor, edge, vocabulary) {
|
|
45
|
+
const validationError = validateEdgeTypesInVocabulary(extractor, edge, vocabulary);
|
|
46
|
+
if (validationError) return validationError;
|
|
47
|
+
if (isEdgeWritable(extractor, edge)) return { ok: true };
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: `extractor '${extractor.name}': edge '${edge.edge_type}' has neither endpoint in memory.write (${extractor.memoryWrite.join(', ')})`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { loadExtractor } from './loader.js';
|
|
2
|
+
import { parseExtractorOutput } from './parse-jsonl.js';
|
|
3
|
+
import { spawnWithTimeout as defaultSpawn } from './spawn-with-timeout.js';
|
|
4
|
+
import {
|
|
5
|
+
checkEntityRowAgainstExtractor,
|
|
6
|
+
checkEdgeRowAgainstExtractor,
|
|
7
|
+
} from './permissions.js';
|
|
8
|
+
|
|
9
|
+
function spawnFailureReason(spawnResult) {
|
|
10
|
+
if (spawnResult.timedOut) return 'timedOut';
|
|
11
|
+
if (spawnResult.tooMuchOutput) return 'tooMuchOutput';
|
|
12
|
+
if (!spawnResult.ok) return 'exited';
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function spawnFailureMessage(reason, ext, spawnResult) {
|
|
17
|
+
if (reason === 'timedOut') {
|
|
18
|
+
return `extractor timed out after ${ext.timeoutMs}ms`;
|
|
19
|
+
}
|
|
20
|
+
if (reason === 'tooMuchOutput') {
|
|
21
|
+
return 'extractor produced too much output'
|
|
22
|
+
+ ' (stdout >50MB or stderr >1MB)';
|
|
23
|
+
}
|
|
24
|
+
const detail = spawnResult.signal
|
|
25
|
+
? `signal ${spawnResult.signal} (exit code ${spawnResult.exitCode})`
|
|
26
|
+
: `exit code ${spawnResult.exitCode}`;
|
|
27
|
+
return `extractor exited with ${detail}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadAndRunExtractor({
|
|
31
|
+
foundryDir, name, cwd, io, spawn, perExtractor,
|
|
32
|
+
}) {
|
|
33
|
+
const startedAt = Date.now();
|
|
34
|
+
let ext;
|
|
35
|
+
try {
|
|
36
|
+
ext = await loadExtractor(foundryDir, name, io);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { abort: abort(perExtractor, name,
|
|
39
|
+
`failed to load extractor: ${err.message}`) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const spawnResult = await spawn({
|
|
43
|
+
command: ext.command,
|
|
44
|
+
cwd,
|
|
45
|
+
timeoutMs: ext.timeoutMs,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const reason = spawnFailureReason(spawnResult);
|
|
49
|
+
if (reason) {
|
|
50
|
+
const msg = spawnFailureMessage(reason, ext, spawnResult);
|
|
51
|
+
return { abort: abort(perExtractor, name, msg, spawnResult.stderr) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let rows;
|
|
55
|
+
try {
|
|
56
|
+
rows = parseExtractorOutput(spawnResult.stdout);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { abort: abort(perExtractor, name, err.message,
|
|
59
|
+
spawnResult.stderr) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { ext, rows, spawnResult, startedAt };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateEntityRow({ ext, row, vocabulary, perExtractor, name, stderr }) {
|
|
66
|
+
const r = checkEntityRowAgainstExtractor(ext, row.type);
|
|
67
|
+
if (!r.ok) return abort(perExtractor, name, r.error, stderr);
|
|
68
|
+
if (!vocabulary.entities?.[row.type]) {
|
|
69
|
+
return abort(perExtractor, name,
|
|
70
|
+
`entity type '${row.type}' not declared in project vocabulary`, stderr);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateEdgeRow({ row, ext, vocabulary, perExtractor, name, stderr }) {
|
|
76
|
+
const r = checkEdgeRowAgainstExtractor(ext, {
|
|
77
|
+
edge_type: row.edge_type,
|
|
78
|
+
from_type: row.from_type,
|
|
79
|
+
to_type: row.to_type,
|
|
80
|
+
}, vocabulary);
|
|
81
|
+
if (!r.ok) return abort(perExtractor, name, r.error, stderr);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateRows({ ext, rows, vocabulary, perExtractor, name, spawnResult }) {
|
|
86
|
+
const stderr = spawnResult.stderr;
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const error = row.kind === 'entity'
|
|
89
|
+
? validateEntityRow({ ext, row, vocabulary, perExtractor, name, stderr })
|
|
90
|
+
: validateEdgeRow({ row, ext, vocabulary, perExtractor, name, stderr });
|
|
91
|
+
if (error) return error;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function upsertEntityRow(row, store, vocabulary, putEntity, writeEmbedder) {
|
|
97
|
+
await putEntity(
|
|
98
|
+
store,
|
|
99
|
+
{ type: row.type, name: row.name, value: row.value },
|
|
100
|
+
vocabulary,
|
|
101
|
+
writeEmbedder ? { embedder: writeEmbedder } : undefined,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function upsertEdgeRow(row, store, vocabulary, relate) {
|
|
106
|
+
await relate(store, {
|
|
107
|
+
edge_type: row.edge_type,
|
|
108
|
+
from_type: row.from_type, from_name: row.from_name,
|
|
109
|
+
to_type: row.to_type, to_name: row.to_name,
|
|
110
|
+
}, vocabulary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function upsertRows({
|
|
114
|
+
rows, store, vocabulary, putEntity, relate,
|
|
115
|
+
writeEmbedder, perExtractor, name, spawnResult,
|
|
116
|
+
}) {
|
|
117
|
+
let rowsUpserted = 0;
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
try {
|
|
120
|
+
if (row.kind === 'entity') {
|
|
121
|
+
await upsertEntityRow(row, store, vocabulary, putEntity, writeEmbedder);
|
|
122
|
+
} else {
|
|
123
|
+
await upsertEdgeRow(row, store, vocabulary, relate);
|
|
124
|
+
}
|
|
125
|
+
rowsUpserted += 1;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return { abort: abort(perExtractor, name,
|
|
128
|
+
`upsert failed: ${err.message}`, spawnResult.stderr) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { rowsUpserted };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function syncStoreIfNeeded({
|
|
135
|
+
syncStore, store, io, perExtractor, name, rowsUpserted, spawnResult,
|
|
136
|
+
}) {
|
|
137
|
+
if (!syncStore) return null;
|
|
138
|
+
try {
|
|
139
|
+
await syncStore({ store, io });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return abort(perExtractor, name,
|
|
142
|
+
`memory sync failed after upserting ${rowsUpserted} rows:`
|
|
143
|
+
+ ` ${err.message}`, spawnResult.stderr);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function processExtractor({
|
|
149
|
+
foundryDir, name, cwd, io, spawn, store, vocabulary,
|
|
150
|
+
putEntity, relate, writeEmbedder, syncStore, perExtractor,
|
|
151
|
+
}) {
|
|
152
|
+
const loadResult = await loadAndRunExtractor({
|
|
153
|
+
foundryDir, name, cwd, io, spawn, perExtractor,
|
|
154
|
+
});
|
|
155
|
+
if (loadResult.abort) return loadResult;
|
|
156
|
+
|
|
157
|
+
const { ext, rows, spawnResult, startedAt } = loadResult;
|
|
158
|
+
|
|
159
|
+
const validationError = validateRows({
|
|
160
|
+
ext, rows, vocabulary, perExtractor, name, spawnResult,
|
|
161
|
+
});
|
|
162
|
+
if (validationError) return { abort: validationError };
|
|
163
|
+
|
|
164
|
+
const upsertResult = await upsertRows({
|
|
165
|
+
rows, store, vocabulary, putEntity, relate,
|
|
166
|
+
writeEmbedder, perExtractor, name, spawnResult,
|
|
167
|
+
});
|
|
168
|
+
if (upsertResult.abort) return upsertResult;
|
|
169
|
+
|
|
170
|
+
const syncError = await syncStoreIfNeeded({
|
|
171
|
+
syncStore, store, io, perExtractor, name,
|
|
172
|
+
rowsUpserted: upsertResult.rowsUpserted, spawnResult,
|
|
173
|
+
});
|
|
174
|
+
if (syncError) return { abort: syncError };
|
|
175
|
+
|
|
176
|
+
perExtractor.push({
|
|
177
|
+
name,
|
|
178
|
+
rowsUpserted: upsertResult.rowsUpserted,
|
|
179
|
+
durationMs: Date.now() - startedAt,
|
|
180
|
+
});
|
|
181
|
+
return { summary: perExtractor[perExtractor.length - 1] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function runAssay({
|
|
185
|
+
foundryDir,
|
|
186
|
+
cwd,
|
|
187
|
+
io,
|
|
188
|
+
extractors,
|
|
189
|
+
store,
|
|
190
|
+
vocabulary,
|
|
191
|
+
putEntity,
|
|
192
|
+
relate,
|
|
193
|
+
writeEmbedder,
|
|
194
|
+
syncStore,
|
|
195
|
+
spawn = defaultSpawn,
|
|
196
|
+
}) {
|
|
197
|
+
const perExtractor = [];
|
|
198
|
+
|
|
199
|
+
for (const name of extractors) {
|
|
200
|
+
const result = await processExtractor({
|
|
201
|
+
foundryDir, name, cwd, io, spawn, store, vocabulary,
|
|
202
|
+
putEntity, relate, writeEmbedder, syncStore, perExtractor,
|
|
203
|
+
});
|
|
204
|
+
if (result.abort) return result.abort;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { ok: true, perExtractor };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function abort(perExtractor, failedExtractor, reason, stderr) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
aborted: true,
|
|
214
|
+
failedExtractor,
|
|
215
|
+
reason,
|
|
216
|
+
stderr: stderr ?? '',
|
|
217
|
+
perExtractor,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
|
|
4
|
+
// Output byte limits
|
|
5
|
+
const MAX_STDOUT = 50 * 1024 * 1024; // 50MB
|
|
6
|
+
const MAX_STDERR = 1 * 1024 * 1024; // 1MB
|
|
7
|
+
|
|
8
|
+
function killGroup(child, signal) {
|
|
9
|
+
// G25: Guard against undefined pid (spawn failure)
|
|
10
|
+
if (child.pid === null || child.pid === undefined) return;
|
|
11
|
+
// Negative pid targets the process group. Wrap in try/catch because
|
|
12
|
+
// the group may already be gone (ESRCH) by the time we signal.
|
|
13
|
+
try { process.kill(-child.pid, signal); } catch {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createStreamHandler(decoder, state, prop, maxBytes, onOverflow) {
|
|
17
|
+
return (b) => {
|
|
18
|
+
// G28: Check output limit
|
|
19
|
+
if (state[prop].length >= maxBytes) {
|
|
20
|
+
if (!state.tooMuchOutput) {
|
|
21
|
+
state.tooMuchOutput = true;
|
|
22
|
+
onOverflow();
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const decoded = decoder.write(b);
|
|
27
|
+
// Check again after decoding
|
|
28
|
+
if (state[prop].length + decoded.length >= maxBytes) {
|
|
29
|
+
const remaining = maxBytes - state[prop].length;
|
|
30
|
+
state[prop] += decoded.slice(0, remaining);
|
|
31
|
+
state.tooMuchOutput = true;
|
|
32
|
+
onOverflow();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
state[prop] += decoded;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Runs a command (via /bin/sh -c) with a hard timeout. Never throws.
|
|
40
|
+
// Returns:
|
|
41
|
+
// { ok, exitCode, signal, stdout, stderr, timedOut, tooMuchOutput }
|
|
42
|
+
//
|
|
43
|
+
// On timeout: sends SIGTERM to the child's process group immediately; if the
|
|
44
|
+
// process is still alive 500ms later, sends SIGKILL to the group. `timedOut:
|
|
45
|
+
// true` in the result.
|
|
46
|
+
//
|
|
47
|
+
// Output limits: stdout capped at 50MB, stderr at 1MB. On overflow, kills the
|
|
48
|
+
// process group and returns `tooMuchOutput: true`.
|
|
49
|
+
//
|
|
50
|
+
// The child is spawned with `detached: true` so it becomes the leader of its
|
|
51
|
+
// own process group. We signal the whole group (via `process.kill(-pid, ...)`)
|
|
52
|
+
// to terminate all descendants in the process group tree, ensuring
|
|
53
|
+
// shell-spawned subprocesses (e.g. a `sleep` launched by a shell script)
|
|
54
|
+
// release the inherited stdout/stderr pipes promptly and Node can emit the
|
|
55
|
+
// `close` event as soon as the group exits.
|
|
56
|
+
//
|
|
57
|
+
// Security: this intentionally uses a shell, matching how `foundry_validate_run`
|
|
58
|
+
// expands validation commands today. Extractors are project-authored and
|
|
59
|
+
// committed to the repo; they are trusted code paths, not untrusted input.
|
|
60
|
+
export async function spawnWithTimeout({ command, cwd, timeoutMs, env }) {
|
|
61
|
+
return await new Promise((resolve) => {
|
|
62
|
+
const child = spawn('/bin/sh', ['-c', command], {
|
|
63
|
+
cwd,
|
|
64
|
+
env: env ?? process.env,
|
|
65
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
66
|
+
detached: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const state = { stdout: '', stderr: '', timedOut: false, tooMuchOutput: false, settled: false, hardTimer: null };
|
|
70
|
+
const stdoutDecoder = new StringDecoder('utf-8');
|
|
71
|
+
const stderrDecoder = new StringDecoder('utf-8');
|
|
72
|
+
|
|
73
|
+
child.stdout.on('data', createStreamHandler(stdoutDecoder, state, 'stdout', MAX_STDOUT, () => killGroup(child, 'SIGKILL')));
|
|
74
|
+
child.stderr.on('data', createStreamHandler(stderrDecoder, state, 'stderr', MAX_STDERR, () => killGroup(child, 'SIGKILL')));
|
|
75
|
+
|
|
76
|
+
const softTimer = setTimeout(() => {
|
|
77
|
+
state.timedOut = true;
|
|
78
|
+
killGroup(child, 'SIGTERM');
|
|
79
|
+
// G24: Capture the hard timer so we can clear it on natural exit
|
|
80
|
+
state.hardTimer = setTimeout(() => {
|
|
81
|
+
if (!state.settled) { killGroup(child, 'SIGKILL'); }
|
|
82
|
+
}, 500);
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
|
|
85
|
+
child.on('error', makeErrorHandler(state, softTimer, resolve));
|
|
86
|
+
child.on('close', makeCloseHandler(state, softTimer, stdoutDecoder, stderrDecoder, resolve));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeErrorHandler(state, softTimer, resolve) {
|
|
91
|
+
return (err) => {
|
|
92
|
+
if (state.settled) return;
|
|
93
|
+
state.settled = true;
|
|
94
|
+
clearTimeout(softTimer);
|
|
95
|
+
// G24: Clear hard timer if it was set
|
|
96
|
+
if (state.hardTimer) clearTimeout(state.hardTimer);
|
|
97
|
+
|
|
98
|
+
// G26: Spawn error should never set timedOut to true.
|
|
99
|
+
// The error handler fires when spawn fails (ENOENT, EMFILE, etc.),
|
|
100
|
+
// which happens before the child even starts. timedOut requires
|
|
101
|
+
// the timeout to have actually elapsed AND a child to have existed.
|
|
102
|
+
const errDetail = (state.stderr.endsWith('\n') || state.stderr === '' ? '' : '\n') + `spawn error: ${err.message}`;
|
|
103
|
+
resolve({
|
|
104
|
+
ok: false,
|
|
105
|
+
exitCode: null,
|
|
106
|
+
signal: null,
|
|
107
|
+
stdout: state.stdout,
|
|
108
|
+
stderr: state.stderr + errDetail,
|
|
109
|
+
timedOut: false, // G26: Explicitly false, not inheriting timedOut flag
|
|
110
|
+
tooMuchOutput: false,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function makeCloseHandler(state, softTimer, stdoutDecoder, stderrDecoder, resolve) {
|
|
116
|
+
return (code, signal) => {
|
|
117
|
+
if (state.settled) return;
|
|
118
|
+
state.settled = true;
|
|
119
|
+
clearTimeout(softTimer);
|
|
120
|
+
// G24: Clear the SIGKILL timer to prevent event loop from hanging
|
|
121
|
+
if (state.hardTimer) clearTimeout(state.hardTimer);
|
|
122
|
+
|
|
123
|
+
// Flush any remaining bytes in the decoders
|
|
124
|
+
state.stdout += stdoutDecoder.end();
|
|
125
|
+
state.stderr += stderrDecoder.end();
|
|
126
|
+
|
|
127
|
+
const ok = !state.timedOut && !state.tooMuchOutput && code === 0;
|
|
128
|
+
resolve({
|
|
129
|
+
ok,
|
|
130
|
+
exitCode: code,
|
|
131
|
+
signal,
|
|
132
|
+
stdout: state.stdout,
|
|
133
|
+
stderr: state.stderr,
|
|
134
|
+
timedOut: state.timedOut,
|
|
135
|
+
tooMuchOutput: state.tooMuchOutput,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attestation builder.
|
|
3
|
+
*
|
|
4
|
+
* Verifies cycle completeness and produces the ATTEST.md content string.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { load as loadYaml } from 'js-yaml';
|
|
9
|
+
import { parseFrontmatter } from '../workfile.js';
|
|
10
|
+
import { parseArtefactsTable } from '../artefacts.js';
|
|
11
|
+
import { parseAllHistoryEntries } from '../history.js';
|
|
12
|
+
import { sha256Buffer } from './hash.js';
|
|
13
|
+
import { buildAttestationPayload } from './payload.js';
|
|
14
|
+
import { canonicalJson } from './canonical-json.js';
|
|
15
|
+
import { renderAttestedCommitMessage } from './render.js';
|
|
16
|
+
|
|
17
|
+
function readWorkFiles(cwd, io) {
|
|
18
|
+
const workPath = path.join(cwd, 'WORK.md');
|
|
19
|
+
const historyPath = path.join(cwd, 'WORK.history.yaml');
|
|
20
|
+
const feedbackPath = path.join(cwd, 'WORK.feedback.yaml');
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
workText: io.readFile(workPath),
|
|
24
|
+
historyText: io.fileExists(historyPath) ? io.readFile(historyPath) : '',
|
|
25
|
+
feedbackText: io.fileExists(feedbackPath) ? io.readFile(feedbackPath) : '',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
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
|
+
function checkMissingStages(frontmatter, historyText) {
|
|
38
|
+
const entries = parseAllHistoryEntries(historyText);
|
|
39
|
+
const completed = new Set(entries.map(e => e.stage));
|
|
40
|
+
const missing = (frontmatter.stages ?? []).filter(s => !completed.has(s));
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
return `foundry_attest: required stage(s) not completed: ${missing.join(', ')}`;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkUnresolvedFeedback(feedbackText) {
|
|
48
|
+
const doc = feedbackText.trim() ? (loadYaml(feedbackText) ?? {}) : {};
|
|
49
|
+
const items = doc.items ?? [];
|
|
50
|
+
const unresolved = items.filter(item => item?.history?.[0]?.state !== 'resolved');
|
|
51
|
+
if (unresolved.length > 0) {
|
|
52
|
+
return `foundry_attest: ${unresolved.length} unresolved feedback item(s): ${unresolved.map(i => i.id).join(', ')}`;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findCycleError(frontmatter, artefacts, historyText, feedbackText) {
|
|
58
|
+
const blockedError = checkBlockedArtefacts(artefacts);
|
|
59
|
+
if (blockedError) return blockedError;
|
|
60
|
+
|
|
61
|
+
const missingError = checkMissingStages(frontmatter, historyText);
|
|
62
|
+
if (missingError) return missingError;
|
|
63
|
+
|
|
64
|
+
return checkUnresolvedFeedback(feedbackText);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function computeDiffSha(execGit, baseBranch) {
|
|
68
|
+
const mergeBase = execGit(['merge-base', 'HEAD', baseBranch]).trim();
|
|
69
|
+
const diffOutput = execGit(['diff', mergeBase, 'HEAD']);
|
|
70
|
+
const diffBuf = Buffer.isBuffer(diffOutput) ? diffOutput : Buffer.from(diffOutput, 'utf8');
|
|
71
|
+
return sha256Buffer(diffBuf);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function buildAttestation({
|
|
75
|
+
cwd,
|
|
76
|
+
baseBranch,
|
|
77
|
+
goalText,
|
|
78
|
+
archiveBranch,
|
|
79
|
+
archiveTipSha,
|
|
80
|
+
io,
|
|
81
|
+
execGit,
|
|
82
|
+
}) {
|
|
83
|
+
const { workText, historyText, feedbackText } = readWorkFiles(cwd, io);
|
|
84
|
+
const frontmatter = parseFrontmatter(workText);
|
|
85
|
+
const artefacts = parseArtefactsTable(workText);
|
|
86
|
+
|
|
87
|
+
const cycleError = findCycleError(frontmatter, artefacts, historyText, feedbackText);
|
|
88
|
+
if (cycleError) {
|
|
89
|
+
return { ok: false, error: cycleError };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const diffSha = computeDiffSha(execGit, baseBranch);
|
|
93
|
+
|
|
94
|
+
const payload = buildAttestationPayload({
|
|
95
|
+
cwd,
|
|
96
|
+
goalText,
|
|
97
|
+
archiveBranch,
|
|
98
|
+
archiveTipSha,
|
|
99
|
+
io,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const payloadJson = canonicalJson(payload);
|
|
103
|
+
const commitMessage = renderAttestedCommitMessage({ humanSummary: goalText, payloadJson });
|
|
104
|
+
|
|
105
|
+
const content = commitMessage.replace(
|
|
106
|
+
'-----BEGIN FOUNDRY ATTESTATION-----',
|
|
107
|
+
`diff-sha256: ${diffSha}\n\n-----BEGIN FOUNDRY ATTESTATION-----`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { ok: true, content, diffSha };
|
|
111
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function tryStringifyBool(value) {
|
|
2
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function tryStringifyNumber(value) {
|
|
7
|
+
if (typeof value === 'number') return JSON.stringify(value);
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function tryStringifyString(value) {
|
|
12
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stringifyPrimitive(value) {
|
|
17
|
+
if (value === null) return 'null';
|
|
18
|
+
if (value === undefined) return undefined;
|
|
19
|
+
const bool = tryStringifyBool(value);
|
|
20
|
+
if (bool !== undefined) return bool;
|
|
21
|
+
const num = tryStringifyNumber(value);
|
|
22
|
+
if (num !== undefined) return num;
|
|
23
|
+
return tryStringifyString(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stringifyArray(value, seen) {
|
|
27
|
+
if (seen.has(value)) {
|
|
28
|
+
throw new TypeError('Converting circular structure to JSON');
|
|
29
|
+
}
|
|
30
|
+
seen.add(value);
|
|
31
|
+
|
|
32
|
+
const items = [];
|
|
33
|
+
for (let i = 0; i < value.length; i++) {
|
|
34
|
+
const result = stringifyCanonical(value[i], seen);
|
|
35
|
+
items.push(result === undefined ? 'null' : result);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
seen.delete(value);
|
|
39
|
+
return '[' + items.join(',') + ']';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isBoxedBigInt(value) {
|
|
43
|
+
return Object.prototype.toString.call(value) === '[object BigInt]';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stringifyUnboxed(value) {
|
|
47
|
+
if (value instanceof Number) return JSON.stringify(value.valueOf());
|
|
48
|
+
if (value instanceof String) return JSON.stringify(value.valueOf());
|
|
49
|
+
if (value instanceof Boolean) return value.valueOf() ? 'true' : 'false';
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildObjectPairs(value, seen) {
|
|
54
|
+
const keys = Object.keys(value).sort();
|
|
55
|
+
const pairs = [];
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const val = stringifyCanonical(value[key], seen);
|
|
58
|
+
if (val !== undefined) {
|
|
59
|
+
pairs.push(JSON.stringify(key) + ':' + val);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return pairs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function stringifyPlainObject(value, seen) {
|
|
66
|
+
if (seen.has(value)) {
|
|
67
|
+
throw new TypeError('Converting circular structure to JSON');
|
|
68
|
+
}
|
|
69
|
+
seen.add(value);
|
|
70
|
+
const pairs = buildObjectPairs(value, seen);
|
|
71
|
+
seen.delete(value);
|
|
72
|
+
return '{' + pairs.join(',') + '}';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stringifyObject(value, seen) {
|
|
76
|
+
if (isBoxedBigInt(value)) {
|
|
77
|
+
throw new TypeError('Do not know how to serialise a BigInt');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const unboxed = stringifyUnboxed(value);
|
|
81
|
+
if (unboxed !== undefined) return unboxed;
|
|
82
|
+
|
|
83
|
+
if (typeof value.toJSON === 'function') {
|
|
84
|
+
return stringifyCanonical(value.toJSON(), seen);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return stringifyPlainObject(value, seen);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function dispatchType(value, seen) {
|
|
91
|
+
if (Array.isArray(value)) return stringifyArray(value, seen);
|
|
92
|
+
if (typeof value === 'object') return stringifyObject(value, seen);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stringifyCanonical(value, seen = new Set()) {
|
|
97
|
+
if (typeof value === 'bigint') {
|
|
98
|
+
throw new TypeError('Do not know how to serialise a BigInt');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const primitive = stringifyPrimitive(value);
|
|
102
|
+
if (primitive !== undefined) return primitive;
|
|
103
|
+
|
|
104
|
+
return dispatchType(value, seen);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function canonicalJson(value) {
|
|
108
|
+
return stringifyCanonical(value);
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function sha256Text(text) {
|
|
4
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function sha256Buffer(buf) {
|
|
8
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sortPaths(paths) {
|
|
12
|
+
return [...paths].sort((a, b) => {
|
|
13
|
+
if (a < b) return -1;
|
|
14
|
+
if (a > b) return 1;
|
|
15
|
+
return 0;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const BEGIN = '-----BEGIN FOUNDRY ATTESTATION-----';
|
|
2
|
+
const END = '-----END FOUNDRY ATTESTATION-----';
|
|
3
|
+
|
|
4
|
+
export function extractAttestationBlock(message) {
|
|
5
|
+
const start = message.indexOf(BEGIN);
|
|
6
|
+
if (start === -1) {
|
|
7
|
+
throw new Error('attestation block not found');
|
|
8
|
+
}
|
|
9
|
+
const end = message.indexOf(END, start + BEGIN.length);
|
|
10
|
+
if (end === -1 || end <= start) {
|
|
11
|
+
throw new Error('attestation block not found');
|
|
12
|
+
}
|
|
13
|
+
return message.slice(start + BEGIN.length, end).trim();
|
|
14
|
+
}
|