yam-flow 2.2.0 → 2.3.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/CHANGELOG.md +23 -0
- package/COMMANDS.md +8 -1
- package/README.md +12 -0
- package/ROADMAP.md +25 -0
- package/dist/bin/yam.js +237 -62
- package/dist/lib/trust-kernel.d.ts +54 -0
- package/dist/lib/trust-kernel.js +194 -0
- package/dist/lib/ueye-artifacts.d.ts +98 -0
- package/dist/lib/ueye-artifacts.js +317 -0
- package/package.json +1 -1
- package/references/final-report.md +2 -0
- package/references/honest-completion.md +1 -0
- package/references/mission.md +16 -0
- package/references/scout-wide-scan.md +150 -0
- package/references/scout.md +9 -0
- package/references/tool-trust-layer.md +3 -0
- package/references/trust-kernel.md +3 -0
- package/references/truth-matrix.md +2 -0
- package/references/ueye-proof.md +6 -0
- package/references/ueye.md +12 -0
- package/references/verification-levels.md +2 -1
- package/skills/mission/SKILL.md +19 -4
- package/skills/scout/SKILL.md +18 -5
- package/skills/ueye/SKILL.md +11 -0
- package/src/bin/yam.ts +227 -56
- package/src/lib/trust-kernel.ts +239 -0
- package/src/lib/ueye-artifacts.ts +354 -0
- package/templates/mission-plan.md +25 -2
- package/templates/ueye-comparison.md +12 -0
- package/templates/ueye-review.md +26 -0
- package/yam.manifest.json +7 -7
package/dist/lib/trust-kernel.js
CHANGED
|
@@ -162,6 +162,142 @@ export function buildMissionPatchEnvelope(input = {}) {
|
|
|
162
162
|
truth_status: isTruthStatus(input.truth_status) ? input.truth_status : 'partial'
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
+
export function buildMissionSubagentReceipt(input = {}) {
|
|
166
|
+
const role = normalizeMissionLaneRole(input.role);
|
|
167
|
+
const accessMode = normalizeMissionAccessMode(input.access_mode, role);
|
|
168
|
+
const lifecycleStatus = normalizeMissionLifecycleStatus(input.lifecycle_status);
|
|
169
|
+
const outcome = normalizeMissionOutcome(input.outcome);
|
|
170
|
+
const threadId = String(input.thread_id || '');
|
|
171
|
+
const assignedScope = String(input.assigned_scope || '');
|
|
172
|
+
const changedFiles = asList(input.changed_files);
|
|
173
|
+
const verificationEvidence = asList(input.verification_evidence);
|
|
174
|
+
const remainingRisks = asList(input.remaining_risks);
|
|
175
|
+
const readOnlyRole = role === 'reviewer' || role === 'doctor';
|
|
176
|
+
const checks = [
|
|
177
|
+
missionContractCheck('thread_id', 'Thread id is recorded', Boolean(threadId), 'record --thread-id for every spawned thread'),
|
|
178
|
+
missionContractCheck('assigned_scope', 'Assigned scope is recorded', Boolean(assignedScope), 'record the bounded lane scope'),
|
|
179
|
+
missionContractCheck('read_only_role', 'Reviewer and doctor lanes stay read-only', !readOnlyRole || (accessMode === 'read_only' && changedFiles.length === 0), 'rerun reviewer/doctor without write access or changed files'),
|
|
180
|
+
missionContractCheck('terminal_lifecycle', 'Lifecycle reached a terminal state', ['stopped', 'failed', 'cancelled'].includes(lifecycleStatus), 'wait for a terminal lifecycle event before finalization'),
|
|
181
|
+
missionContractCheck('explicit_outcome', 'Outcome is explicit and unambiguous', outcome !== 'ambiguous', 'record passed, failed, or blocked; a stop event alone is not success evidence'),
|
|
182
|
+
missionContractCheck('passed_evidence', 'Passed outcome has verification evidence', outcome !== 'passed' || verificationEvidence.length > 0, 'attach at least one concrete verification item for a passed outcome')
|
|
183
|
+
];
|
|
184
|
+
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
185
|
+
if (outcome === 'failed' || outcome === 'blocked')
|
|
186
|
+
blockers.push(`outcome_${outcome}: resolve or hand off this thread before mission completion`);
|
|
187
|
+
if ((lifecycleStatus === 'failed' || lifecycleStatus === 'cancelled') && outcome === 'passed') {
|
|
188
|
+
blockers.push('lifecycle_outcome_conflict: a failed or cancelled lifecycle cannot claim a passed outcome');
|
|
189
|
+
}
|
|
190
|
+
const warnings = remainingRisks.length ? ['remaining risks are recorded and must be included in the mission handoff'] : [];
|
|
191
|
+
const completionEligible = blockers.length === 0 && lifecycleStatus === 'stopped' && outcome === 'passed';
|
|
192
|
+
const truth = completionEligible ? 'verified' : blockers.length ? 'blocked' : 'partial';
|
|
193
|
+
return {
|
|
194
|
+
schema: 'yam.mission-subagent-receipt.v1',
|
|
195
|
+
receipt_id: String(input.receipt_id || `receipt-${threadId || 'unassigned'}`),
|
|
196
|
+
generated_at: String(input.generated_at || new Date().toISOString()),
|
|
197
|
+
thread_id: threadId,
|
|
198
|
+
lane_id: String(input.lane_id || threadId),
|
|
199
|
+
agent_id: String(input.agent_id || ''),
|
|
200
|
+
role,
|
|
201
|
+
access_mode: accessMode,
|
|
202
|
+
lifecycle_status: lifecycleStatus,
|
|
203
|
+
outcome,
|
|
204
|
+
assigned_scope: assignedScope,
|
|
205
|
+
changed_files: changedFiles,
|
|
206
|
+
verification_evidence: verificationEvidence,
|
|
207
|
+
remaining_risks: remainingRisks,
|
|
208
|
+
checks,
|
|
209
|
+
blockers: uniqueStrings(blockers),
|
|
210
|
+
warnings,
|
|
211
|
+
completion_eligible: completionEligible,
|
|
212
|
+
truth_status: truth
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function buildMissionCompletionGate(input = {}) {
|
|
216
|
+
const expectedThreadIds = uniqueStrings(asList(input.expected_thread_ids));
|
|
217
|
+
const receipts = asObjectList(input.receipts).map((receipt) => buildMissionSubagentReceipt(receipt));
|
|
218
|
+
const receiptCounts = new Map();
|
|
219
|
+
for (const receipt of receipts) {
|
|
220
|
+
receiptCounts.set(receipt.thread_id, (receiptCounts.get(receipt.thread_id) || 0) + 1);
|
|
221
|
+
}
|
|
222
|
+
const missingThreadIds = expectedThreadIds.filter((threadId) => !receiptCounts.has(threadId));
|
|
223
|
+
const duplicateThreadIds = [...receiptCounts.entries()].filter(([, count]) => count > 1).map(([threadId]) => threadId).filter(Boolean);
|
|
224
|
+
const unexpectedThreadIds = uniqueStrings(receipts.map((receipt) => receipt.thread_id).filter((threadId) => threadId && !expectedThreadIds.includes(threadId)));
|
|
225
|
+
const expectedReceipts = receipts.filter((receipt) => expectedThreadIds.includes(receipt.thread_id));
|
|
226
|
+
const invalidThreadIds = uniqueStrings([
|
|
227
|
+
...expectedReceipts.filter((receipt) => !receipt.completion_eligible).map((receipt) => receipt.thread_id),
|
|
228
|
+
...receipts.filter((receipt) => !receipt.thread_id).map((receipt) => `missing:${receipt.receipt_id}`)
|
|
229
|
+
]);
|
|
230
|
+
const readOnlyViolations = uniqueStrings(receipts
|
|
231
|
+
.filter((receipt) => receipt.checks.some((check) => check.id === 'read_only_role' && check.status === 'fail'))
|
|
232
|
+
.map((receipt) => receipt.thread_id || receipt.lane_id));
|
|
233
|
+
const blockers = [];
|
|
234
|
+
if (!expectedThreadIds.length)
|
|
235
|
+
blockers.push('expected_thread_inventory_missing: record every spawned thread before finalization');
|
|
236
|
+
if (missingThreadIds.length)
|
|
237
|
+
blockers.push(`missing_receipts: ${missingThreadIds.join(', ')}`);
|
|
238
|
+
if (duplicateThreadIds.length)
|
|
239
|
+
blockers.push(`duplicate_receipts: ${duplicateThreadIds.join(', ')}`);
|
|
240
|
+
if (unexpectedThreadIds.length)
|
|
241
|
+
blockers.push(`unexpected_receipts: ${unexpectedThreadIds.join(', ')}`);
|
|
242
|
+
if (invalidThreadIds.length)
|
|
243
|
+
blockers.push(`invalid_or_incomplete_receipts: ${invalidThreadIds.join(', ')}`);
|
|
244
|
+
if (readOnlyViolations.length)
|
|
245
|
+
blockers.push(`read_only_contract_violations: ${readOnlyViolations.join(', ')}`);
|
|
246
|
+
blockers.push(...asList(input.blockers));
|
|
247
|
+
const warnings = uniqueStrings(asList(input.warnings));
|
|
248
|
+
const ready = blockers.length === 0 && expectedReceipts.length === expectedThreadIds.length;
|
|
249
|
+
return {
|
|
250
|
+
schema: 'yam.mission-completion-gate.v1',
|
|
251
|
+
generated_at: String(input.generated_at || new Date().toISOString()),
|
|
252
|
+
expected_thread_ids: expectedThreadIds,
|
|
253
|
+
receipt_count: receipts.length,
|
|
254
|
+
receipts,
|
|
255
|
+
missing_thread_ids: missingThreadIds,
|
|
256
|
+
duplicate_thread_ids: duplicateThreadIds,
|
|
257
|
+
unexpected_thread_ids: unexpectedThreadIds,
|
|
258
|
+
invalid_thread_ids: invalidThreadIds,
|
|
259
|
+
read_only_violations: readOnlyViolations,
|
|
260
|
+
blockers: uniqueStrings(blockers),
|
|
261
|
+
warnings,
|
|
262
|
+
ready_to_claim_complete: ready,
|
|
263
|
+
next_action: ready ? 'attach this gate to the mission proof summary' : blockers[0] || 'resolve mission receipt warnings',
|
|
264
|
+
truth_status: ready ? 'verified' : 'blocked'
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function missionContractCheck(id, label, passed, nextAction) {
|
|
268
|
+
return {
|
|
269
|
+
id,
|
|
270
|
+
label,
|
|
271
|
+
status: passed ? 'pass' : 'fail',
|
|
272
|
+
next_action: nextAction
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function normalizeMissionLaneRole(value = '') {
|
|
276
|
+
const text = String(value || '').toLowerCase().replace(/[- ]/g, '_');
|
|
277
|
+
if (['implementer', 'reviewer', 'ux_verifier', 'doctor'].includes(text))
|
|
278
|
+
return text;
|
|
279
|
+
return 'other';
|
|
280
|
+
}
|
|
281
|
+
function normalizeMissionAccessMode(value = '', role = 'other') {
|
|
282
|
+
const text = String(value || '').toLowerCase().replace(/[- ]/g, '_');
|
|
283
|
+
if (text === 'read_only' || text === 'readonly')
|
|
284
|
+
return 'read_only';
|
|
285
|
+
if (text === 'write')
|
|
286
|
+
return 'write';
|
|
287
|
+
return role === 'reviewer' || role === 'doctor' ? 'read_only' : 'write';
|
|
288
|
+
}
|
|
289
|
+
function normalizeMissionLifecycleStatus(value = '') {
|
|
290
|
+
const text = String(value || '').toLowerCase();
|
|
291
|
+
if (['pending', 'running', 'stopped', 'failed', 'cancelled'].includes(text))
|
|
292
|
+
return text;
|
|
293
|
+
return 'pending';
|
|
294
|
+
}
|
|
295
|
+
function normalizeMissionOutcome(value = '') {
|
|
296
|
+
const text = String(value || '').toLowerCase();
|
|
297
|
+
if (['passed', 'failed', 'blocked'].includes(text))
|
|
298
|
+
return text;
|
|
299
|
+
return 'ambiguous';
|
|
300
|
+
}
|
|
165
301
|
export function buildRuntimeBackendEvidence(input = {}) {
|
|
166
302
|
const backend = normalizeRuntimeBackend(input.backend);
|
|
167
303
|
const claim = normalizeRuntimeClaim(input.claim);
|
|
@@ -661,6 +797,12 @@ export function classifyEvidenceTruth(summary = {}, options = {}) {
|
|
|
661
797
|
for (const envelope of asList(summary.missionEnvelope)) {
|
|
662
798
|
rows.push(evidenceRow('evidence', envelope, metadataTruth(envelope, 'partial')));
|
|
663
799
|
}
|
|
800
|
+
for (const receipt of asList(summary.missionReceipt)) {
|
|
801
|
+
rows.push(evidenceRow('evidence', receipt, metadataTruth(receipt, 'partial')));
|
|
802
|
+
}
|
|
803
|
+
for (const completion of asList(summary.missionCompletion)) {
|
|
804
|
+
rows.push(evidenceRow('evidence', completion, metadataTruth(completion, 'partial')));
|
|
805
|
+
}
|
|
664
806
|
for (const rollback of asList(summary.rollbackHint)) {
|
|
665
807
|
rows.push(evidenceRow('evidence', rollback, metadataTruth(rollback, 'partial')));
|
|
666
808
|
}
|
|
@@ -715,6 +857,22 @@ export function applyProofTruthCaps(summary = {}, options = {}) {
|
|
|
715
857
|
unverified.push('ueye_design_completion_not_verified');
|
|
716
858
|
}
|
|
717
859
|
}
|
|
860
|
+
const missionRoute = String(summary.route || '').replace(/^\$/, '').toLowerCase() === 'mission';
|
|
861
|
+
const missionCompletionValues = asValueList(summary.missionCompletion);
|
|
862
|
+
if (missionRoute && ['verified', 'proven'].includes(requested)) {
|
|
863
|
+
if (!missionCompletionValues.length) {
|
|
864
|
+
cappedTruth = weakestTruth(cappedTruth, 'partial');
|
|
865
|
+
unverified.push('mission_completion_gate_missing');
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
const missionTruth = missionCompletionCap(missionCompletionValues);
|
|
869
|
+
cappedTruth = weakestTruth(cappedTruth, missionTruth);
|
|
870
|
+
if (missionTruth === 'blocked')
|
|
871
|
+
blockers.push('mission_completion_gate_blocked');
|
|
872
|
+
if (!['verified', 'proven'].includes(missionTruth))
|
|
873
|
+
unverified.push('mission_completion_not_verified');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
718
876
|
if (requested !== cappedTruth) {
|
|
719
877
|
caps.push(`requested ${requested} capped to ${cappedTruth} by evidence`);
|
|
720
878
|
}
|
|
@@ -769,6 +927,8 @@ export function buildYamCompletionProof(summary = {}, options = {}) {
|
|
|
769
927
|
runtime: asList(summary.runtime),
|
|
770
928
|
visualProvenance: asList(summary.visualProvenance),
|
|
771
929
|
missionEnvelope: asList(summary.missionEnvelope),
|
|
930
|
+
missionReceipt: asList(summary.missionReceipt),
|
|
931
|
+
missionCompletion: asList(summary.missionCompletion),
|
|
772
932
|
rollbackHint: asList(summary.rollbackHint),
|
|
773
933
|
runtimeBackendEvidence: asList(summary.runtimeBackendEvidence),
|
|
774
934
|
mediaProof: asList(summary.mediaProof),
|
|
@@ -854,6 +1014,18 @@ function designCompletionCap(value = '') {
|
|
|
854
1014
|
return null;
|
|
855
1015
|
return items.reduce((weakest, item) => weakestTruth(weakest, readStructuredTruth(item, 'partial')), 'proven');
|
|
856
1016
|
}
|
|
1017
|
+
function missionCompletionCap(value = '') {
|
|
1018
|
+
const items = asValueList(value);
|
|
1019
|
+
if (!items.length)
|
|
1020
|
+
return 'partial';
|
|
1021
|
+
return items.reduce((weakest, item) => {
|
|
1022
|
+
const parsed = parseStructuredValue(item);
|
|
1023
|
+
if (!parsed || typeof parsed !== 'object')
|
|
1024
|
+
return weakestTruth(weakest, 'blocked');
|
|
1025
|
+
const gate = buildMissionCompletionGate(parsed);
|
|
1026
|
+
return weakestTruth(weakest, gate.truth_status);
|
|
1027
|
+
}, 'proven');
|
|
1028
|
+
}
|
|
857
1029
|
function strongestSupportedTruth(rows = []) {
|
|
858
1030
|
if (rows.some((row) => row.truth === 'blocked'))
|
|
859
1031
|
return 'blocked';
|
|
@@ -977,6 +1149,28 @@ function asList(value) {
|
|
|
977
1149
|
return [];
|
|
978
1150
|
return Array.isArray(value) ? value.filter(Boolean).map(String) : [String(value)].filter(Boolean);
|
|
979
1151
|
}
|
|
1152
|
+
function asValueList(value) {
|
|
1153
|
+
if (value === undefined || value === null || value === '')
|
|
1154
|
+
return [];
|
|
1155
|
+
return Array.isArray(value) ? value.filter(Boolean) : [value];
|
|
1156
|
+
}
|
|
1157
|
+
function parseStructuredValue(value) {
|
|
1158
|
+
if (value && typeof value === 'object')
|
|
1159
|
+
return value;
|
|
1160
|
+
try {
|
|
1161
|
+
const parsed = JSON.parse(String(value || ''));
|
|
1162
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function asObjectList(value) {
|
|
1169
|
+
if (!value)
|
|
1170
|
+
return [];
|
|
1171
|
+
const items = Array.isArray(value) ? value : [value];
|
|
1172
|
+
return items.filter((item) => Boolean(item) && typeof item === 'object');
|
|
1173
|
+
}
|
|
980
1174
|
function uniqueStrings(values = []) {
|
|
981
1175
|
return [...new Set(values.filter(Boolean).map(String))];
|
|
982
1176
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface InspectedImageFile {
|
|
2
|
+
path: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
bytes: number;
|
|
5
|
+
dimensions: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UeyeAssetEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
file_path: string;
|
|
10
|
+
source_url: string;
|
|
11
|
+
source_page_url: string;
|
|
12
|
+
retrieved_at: string;
|
|
13
|
+
license_note: string;
|
|
14
|
+
operator_provided: boolean;
|
|
15
|
+
do_not_replace: boolean;
|
|
16
|
+
allowed_for_edit: boolean;
|
|
17
|
+
sha256: string;
|
|
18
|
+
bytes: number;
|
|
19
|
+
dimensions: string;
|
|
20
|
+
}
|
|
21
|
+
export interface UeyeAssetManifest {
|
|
22
|
+
schema: 'yam.ueye-asset-manifest.v1';
|
|
23
|
+
updated_at: string;
|
|
24
|
+
assets: UeyeAssetEntry[];
|
|
25
|
+
}
|
|
26
|
+
export interface UeyeRevisionEntry {
|
|
27
|
+
artifact_id: string;
|
|
28
|
+
round: number;
|
|
29
|
+
source_path: string;
|
|
30
|
+
archived_path: string;
|
|
31
|
+
archived_at: string;
|
|
32
|
+
sha256: string;
|
|
33
|
+
bytes: number;
|
|
34
|
+
dimensions: string;
|
|
35
|
+
}
|
|
36
|
+
export interface UeyeRevisionHistory {
|
|
37
|
+
schema: 'yam.ueye-revision-history.v1';
|
|
38
|
+
updated_at: string;
|
|
39
|
+
revisions: UeyeRevisionEntry[];
|
|
40
|
+
}
|
|
41
|
+
export declare function inspectImageFile(file: string): Promise<InspectedImageFile>;
|
|
42
|
+
export declare function upsertUeyeAsset(input: {
|
|
43
|
+
manifest_path: string;
|
|
44
|
+
id: string;
|
|
45
|
+
file: string;
|
|
46
|
+
source_url?: string;
|
|
47
|
+
source_page_url?: string;
|
|
48
|
+
retrieved_at?: string;
|
|
49
|
+
license_note?: string;
|
|
50
|
+
operator_provided?: boolean;
|
|
51
|
+
do_not_replace?: boolean;
|
|
52
|
+
allowed_for_edit?: boolean;
|
|
53
|
+
replace?: boolean;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
schema: string;
|
|
56
|
+
action: string;
|
|
57
|
+
manifest_path: string;
|
|
58
|
+
asset: UeyeAssetEntry;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
next_action: string;
|
|
61
|
+
truth_status: string;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function verifyUeyeAssetManifest(manifestFile: string): Promise<{
|
|
64
|
+
schema: string;
|
|
65
|
+
manifest_path: string;
|
|
66
|
+
asset_count: number;
|
|
67
|
+
protected_count: number;
|
|
68
|
+
editable_count: number;
|
|
69
|
+
errors: string[];
|
|
70
|
+
warnings: string[];
|
|
71
|
+
ready: boolean;
|
|
72
|
+
next_action: string;
|
|
73
|
+
truth_status: string;
|
|
74
|
+
}>;
|
|
75
|
+
export declare function archiveUeyeRevision(input: {
|
|
76
|
+
file: string;
|
|
77
|
+
root: string;
|
|
78
|
+
manifest_path?: string;
|
|
79
|
+
round: number;
|
|
80
|
+
artifact_id?: string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
schema: string;
|
|
83
|
+
action: string;
|
|
84
|
+
manifest_path: string;
|
|
85
|
+
revision: UeyeRevisionEntry;
|
|
86
|
+
next_action: string;
|
|
87
|
+
truth_status: string;
|
|
88
|
+
}>;
|
|
89
|
+
export declare function verifyUeyeRevisionHistory(manifestFile: string): Promise<{
|
|
90
|
+
schema: string;
|
|
91
|
+
manifest_path: string;
|
|
92
|
+
revision_count: number;
|
|
93
|
+
rounds: number[];
|
|
94
|
+
errors: string[];
|
|
95
|
+
ready: boolean;
|
|
96
|
+
next_action: string;
|
|
97
|
+
truth_status: string;
|
|
98
|
+
}>;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export async function inspectImageFile(file) {
|
|
6
|
+
const absolute = path.resolve(file);
|
|
7
|
+
const buffer = await fsp.readFile(absolute);
|
|
8
|
+
const stat = await fsp.stat(absolute);
|
|
9
|
+
return {
|
|
10
|
+
path: absolute,
|
|
11
|
+
sha256: createHash('sha256').update(buffer).digest('hex'),
|
|
12
|
+
bytes: stat.size,
|
|
13
|
+
dimensions: imageDimensions(buffer)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export async function upsertUeyeAsset(input) {
|
|
17
|
+
const manifestPath = path.resolve(input.manifest_path);
|
|
18
|
+
const id = safeId(input.id, 'asset id');
|
|
19
|
+
const info = await inspectImageFile(input.file);
|
|
20
|
+
validateOptionalHttpUrl(input.source_url, 'source_url');
|
|
21
|
+
validateOptionalHttpUrl(input.source_page_url, 'source_page_url');
|
|
22
|
+
const manifest = await readAssetManifest(manifestPath);
|
|
23
|
+
const existingIndex = manifest.assets.findIndex((asset) => asset.id === id);
|
|
24
|
+
const existing = existingIndex >= 0 ? manifest.assets[existingIndex] : null;
|
|
25
|
+
if (existing?.do_not_replace && existing.sha256 !== info.sha256 && !input.replace) {
|
|
26
|
+
throw new Error(`asset ${id} is protected by do_not_replace; pass --replace only after explicit review`);
|
|
27
|
+
}
|
|
28
|
+
const duplicate = manifest.assets.find((asset) => asset.id !== id && asset.sha256 === info.sha256);
|
|
29
|
+
if (duplicate)
|
|
30
|
+
throw new Error(`asset content already exists as ${duplicate.id}; reuse that asset id`);
|
|
31
|
+
const entry = {
|
|
32
|
+
id,
|
|
33
|
+
file_path: relativePortablePath(path.dirname(manifestPath), info.path),
|
|
34
|
+
source_url: String(input.source_url ?? existing?.source_url ?? ''),
|
|
35
|
+
source_page_url: String(input.source_page_url ?? existing?.source_page_url ?? ''),
|
|
36
|
+
retrieved_at: String(input.retrieved_at ?? existing?.retrieved_at ?? new Date().toISOString()),
|
|
37
|
+
license_note: String(input.license_note ?? existing?.license_note ?? ''),
|
|
38
|
+
operator_provided: input.operator_provided ?? existing?.operator_provided ?? (!input.source_url && !input.source_page_url),
|
|
39
|
+
do_not_replace: input.do_not_replace ?? existing?.do_not_replace ?? false,
|
|
40
|
+
allowed_for_edit: input.allowed_for_edit ?? existing?.allowed_for_edit ?? false,
|
|
41
|
+
sha256: info.sha256,
|
|
42
|
+
bytes: info.bytes,
|
|
43
|
+
dimensions: info.dimensions
|
|
44
|
+
};
|
|
45
|
+
if (existingIndex >= 0)
|
|
46
|
+
manifest.assets[existingIndex] = entry;
|
|
47
|
+
else
|
|
48
|
+
manifest.assets.push(entry);
|
|
49
|
+
manifest.assets.sort((left, right) => left.id.localeCompare(right.id));
|
|
50
|
+
manifest.updated_at = new Date().toISOString();
|
|
51
|
+
await writeJsonAtomic(manifestPath, manifest);
|
|
52
|
+
const warnings = assetWarnings(entry);
|
|
53
|
+
return {
|
|
54
|
+
schema: 'yam.ueye-asset-update.v1',
|
|
55
|
+
action: existing ? 'updated' : 'added',
|
|
56
|
+
manifest_path: manifestPath,
|
|
57
|
+
asset: entry,
|
|
58
|
+
warnings,
|
|
59
|
+
next_action: warnings[0] || 'run `yam ueye asset verify` before relying on this manifest for a done claim',
|
|
60
|
+
truth_status: warnings.length ? 'partial' : 'verified'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function verifyUeyeAssetManifest(manifestFile) {
|
|
64
|
+
const manifestPath = path.resolve(manifestFile);
|
|
65
|
+
const manifest = await readAssetManifest(manifestPath, false);
|
|
66
|
+
const errors = [];
|
|
67
|
+
const warnings = [];
|
|
68
|
+
const seenHashes = new Map();
|
|
69
|
+
for (const asset of manifest.assets) {
|
|
70
|
+
try {
|
|
71
|
+
validateOptionalHttpUrl(asset.source_url, `${asset.id}.source_url`);
|
|
72
|
+
validateOptionalHttpUrl(asset.source_page_url, `${asset.id}.source_page_url`);
|
|
73
|
+
const file = resolveManifestFile(manifestPath, asset.file_path);
|
|
74
|
+
const info = await inspectImageFile(file);
|
|
75
|
+
if (info.sha256 !== asset.sha256)
|
|
76
|
+
errors.push(`${asset.id}: sha256 mismatch; the asset changed after it was recorded`);
|
|
77
|
+
if (asset.bytes && info.bytes !== asset.bytes)
|
|
78
|
+
errors.push(`${asset.id}: byte size mismatch`);
|
|
79
|
+
if (asset.dimensions && info.dimensions !== asset.dimensions)
|
|
80
|
+
errors.push(`${asset.id}: dimensions mismatch`);
|
|
81
|
+
const duplicate = seenHashes.get(info.sha256);
|
|
82
|
+
if (duplicate && duplicate !== asset.id)
|
|
83
|
+
errors.push(`${asset.id}: duplicates ${duplicate} by sha256`);
|
|
84
|
+
else
|
|
85
|
+
seenHashes.set(info.sha256, asset.id);
|
|
86
|
+
warnings.push(...assetWarnings(asset));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
errors.push(`${asset.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!manifest.assets.length)
|
|
93
|
+
errors.push('asset manifest is empty');
|
|
94
|
+
const uniqueWarnings = uniqueStrings(warnings);
|
|
95
|
+
return {
|
|
96
|
+
schema: 'yam.ueye-asset-verification.v1',
|
|
97
|
+
manifest_path: manifestPath,
|
|
98
|
+
asset_count: manifest.assets.length,
|
|
99
|
+
protected_count: manifest.assets.filter((asset) => asset.do_not_replace).length,
|
|
100
|
+
editable_count: manifest.assets.filter((asset) => asset.allowed_for_edit).length,
|
|
101
|
+
errors: uniqueStrings(errors),
|
|
102
|
+
warnings: uniqueWarnings,
|
|
103
|
+
ready: errors.length === 0,
|
|
104
|
+
next_action: errors[0] || uniqueWarnings[0] || 'attach this manifest verification to the Ueye report when asset provenance matters',
|
|
105
|
+
truth_status: errors.length ? 'blocked' : uniqueWarnings.length ? 'partial' : 'verified'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export async function archiveUeyeRevision(input) {
|
|
109
|
+
const round = Number(input.round);
|
|
110
|
+
if (!Number.isInteger(round) || round < 1)
|
|
111
|
+
throw new Error('revision round must be a positive integer');
|
|
112
|
+
const source = await inspectImageFile(input.file);
|
|
113
|
+
const root = path.resolve(input.root);
|
|
114
|
+
const manifestPath = path.resolve(input.manifest_path || path.join(root, 'manifest.json'));
|
|
115
|
+
const extension = path.extname(source.path);
|
|
116
|
+
const artifactId = safeId(input.artifact_id || path.basename(source.path, extension), 'artifact id');
|
|
117
|
+
const roundDir = path.join(root, `r${round}`);
|
|
118
|
+
const archivedPath = path.join(roundDir, `${artifactId}${extension}`);
|
|
119
|
+
await fsp.mkdir(roundDir, { recursive: true });
|
|
120
|
+
let action = 'archived';
|
|
121
|
+
try {
|
|
122
|
+
await fsp.copyFile(source.path, archivedPath, fsConstants.COPYFILE_EXCL);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error?.code !== 'EEXIST')
|
|
126
|
+
throw error;
|
|
127
|
+
const existing = await inspectImageFile(archivedPath);
|
|
128
|
+
if (existing.sha256 !== source.sha256) {
|
|
129
|
+
throw new Error(`revision r${round}/${artifactId} already exists with different content; choose a new round`);
|
|
130
|
+
}
|
|
131
|
+
action = 'already_archived';
|
|
132
|
+
}
|
|
133
|
+
const history = await readRevisionHistory(manifestPath);
|
|
134
|
+
const existingIndex = history.revisions.findIndex((entry) => entry.round === round && entry.artifact_id === artifactId);
|
|
135
|
+
const entry = {
|
|
136
|
+
artifact_id: artifactId,
|
|
137
|
+
round,
|
|
138
|
+
source_path: source.path,
|
|
139
|
+
archived_path: relativePortablePath(path.dirname(manifestPath), archivedPath),
|
|
140
|
+
archived_at: new Date().toISOString(),
|
|
141
|
+
sha256: source.sha256,
|
|
142
|
+
bytes: source.bytes,
|
|
143
|
+
dimensions: source.dimensions
|
|
144
|
+
};
|
|
145
|
+
if (existingIndex >= 0) {
|
|
146
|
+
if (history.revisions[existingIndex].sha256 !== entry.sha256) {
|
|
147
|
+
throw new Error(`revision history already records different content for r${round}/${artifactId}`);
|
|
148
|
+
}
|
|
149
|
+
entry.archived_at = history.revisions[existingIndex].archived_at;
|
|
150
|
+
history.revisions[existingIndex] = entry;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
history.revisions.push(entry);
|
|
154
|
+
}
|
|
155
|
+
history.revisions.sort((left, right) => left.round - right.round || left.artifact_id.localeCompare(right.artifact_id));
|
|
156
|
+
history.updated_at = new Date().toISOString();
|
|
157
|
+
await writeJsonAtomic(manifestPath, history);
|
|
158
|
+
return {
|
|
159
|
+
schema: 'yam.ueye-revision-archive.v1',
|
|
160
|
+
action,
|
|
161
|
+
manifest_path: manifestPath,
|
|
162
|
+
revision: entry,
|
|
163
|
+
next_action: 'edit the live artifact, then use a later round for the next preserved version',
|
|
164
|
+
truth_status: 'verified'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export async function verifyUeyeRevisionHistory(manifestFile) {
|
|
168
|
+
const manifestPath = path.resolve(manifestFile);
|
|
169
|
+
const history = await readRevisionHistory(manifestPath, false);
|
|
170
|
+
const errors = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const revision of history.revisions) {
|
|
173
|
+
const key = `${revision.round}:${revision.artifact_id}`;
|
|
174
|
+
if (seen.has(key))
|
|
175
|
+
errors.push(`${key}: duplicate revision entry`);
|
|
176
|
+
seen.add(key);
|
|
177
|
+
try {
|
|
178
|
+
const file = resolveManifestFile(manifestPath, revision.archived_path);
|
|
179
|
+
const info = await inspectImageFile(file);
|
|
180
|
+
if (info.sha256 !== revision.sha256)
|
|
181
|
+
errors.push(`${key}: sha256 mismatch; archived content changed`);
|
|
182
|
+
if (revision.bytes && info.bytes !== revision.bytes)
|
|
183
|
+
errors.push(`${key}: byte size mismatch`);
|
|
184
|
+
if (revision.dimensions && info.dimensions !== revision.dimensions)
|
|
185
|
+
errors.push(`${key}: dimensions mismatch`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
errors.push(`${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!history.revisions.length)
|
|
192
|
+
errors.push('revision history is empty');
|
|
193
|
+
return {
|
|
194
|
+
schema: 'yam.ueye-revision-verification.v1',
|
|
195
|
+
manifest_path: manifestPath,
|
|
196
|
+
revision_count: history.revisions.length,
|
|
197
|
+
rounds: [...new Set(history.revisions.map((entry) => entry.round))].sort((left, right) => left - right),
|
|
198
|
+
errors: uniqueStrings(errors),
|
|
199
|
+
ready: errors.length === 0,
|
|
200
|
+
next_action: errors[0] || 'attach this revision history to the Ueye report when iterative visual work matters',
|
|
201
|
+
truth_status: errors.length ? 'blocked' : 'verified'
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function readAssetManifest(file, allowMissing = true) {
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
207
|
+
if (data.schema !== 'yam.ueye-asset-manifest.v1' || !Array.isArray(data.assets))
|
|
208
|
+
throw new Error('invalid Ueye asset manifest schema');
|
|
209
|
+
return data;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (allowMissing && error?.code === 'ENOENT')
|
|
213
|
+
return { schema: 'yam.ueye-asset-manifest.v1', updated_at: '', assets: [] };
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function readRevisionHistory(file, allowMissing = true) {
|
|
218
|
+
try {
|
|
219
|
+
const data = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
220
|
+
if (data.schema !== 'yam.ueye-revision-history.v1' || !Array.isArray(data.revisions))
|
|
221
|
+
throw new Error('invalid Ueye revision history schema');
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (allowMissing && error?.code === 'ENOENT')
|
|
226
|
+
return { schema: 'yam.ueye-revision-history.v1', updated_at: '', revisions: [] };
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function writeJsonAtomic(file, value) {
|
|
231
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
232
|
+
const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
233
|
+
try {
|
|
234
|
+
await fsp.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
235
|
+
await fsp.rename(temporary, file);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await fsp.rm(temporary, { force: true }).catch(() => { });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function validateOptionalHttpUrl(value, label) {
|
|
242
|
+
const text = String(value || '');
|
|
243
|
+
if (!text)
|
|
244
|
+
return;
|
|
245
|
+
const url = new URL(text);
|
|
246
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
247
|
+
throw new Error(`${label} must use http or https`);
|
|
248
|
+
}
|
|
249
|
+
function assetWarnings(asset) {
|
|
250
|
+
const warnings = [];
|
|
251
|
+
if (!asset.source_url && !asset.source_page_url && !asset.operator_provided)
|
|
252
|
+
warnings.push(`${asset.id}: source provenance is missing`);
|
|
253
|
+
if (!asset.license_note)
|
|
254
|
+
warnings.push(`${asset.id}: license_note is missing; usage rights were not recorded`);
|
|
255
|
+
if (asset.dimensions === 'unknown')
|
|
256
|
+
warnings.push(`${asset.id}: image dimensions are unknown`);
|
|
257
|
+
return warnings;
|
|
258
|
+
}
|
|
259
|
+
function safeId(value, label) {
|
|
260
|
+
const text = String(value || '');
|
|
261
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(text))
|
|
262
|
+
throw new Error(`${label} must match [A-Za-z0-9._-] and be at most 64 characters`);
|
|
263
|
+
return text;
|
|
264
|
+
}
|
|
265
|
+
function relativePortablePath(base, target) {
|
|
266
|
+
return path.relative(base, target).split(path.sep).join('/') || path.basename(target);
|
|
267
|
+
}
|
|
268
|
+
function resolveManifestFile(manifestPath, file) {
|
|
269
|
+
return path.isAbsolute(file) ? file : path.resolve(path.dirname(manifestPath), file);
|
|
270
|
+
}
|
|
271
|
+
function imageDimensions(buffer) {
|
|
272
|
+
const png = pngDimensions(buffer);
|
|
273
|
+
if (png)
|
|
274
|
+
return png;
|
|
275
|
+
const jpeg = jpegDimensions(buffer);
|
|
276
|
+
if (jpeg)
|
|
277
|
+
return jpeg;
|
|
278
|
+
const gif = gifDimensions(buffer);
|
|
279
|
+
if (gif)
|
|
280
|
+
return gif;
|
|
281
|
+
return 'unknown';
|
|
282
|
+
}
|
|
283
|
+
function pngDimensions(buffer) {
|
|
284
|
+
if (buffer.length < 24)
|
|
285
|
+
return '';
|
|
286
|
+
if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a)
|
|
287
|
+
return '';
|
|
288
|
+
return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
|
|
289
|
+
}
|
|
290
|
+
function gifDimensions(buffer) {
|
|
291
|
+
if (buffer.length < 10)
|
|
292
|
+
return '';
|
|
293
|
+
const signature = buffer.toString('ascii', 0, 6);
|
|
294
|
+
if (signature !== 'GIF87a' && signature !== 'GIF89a')
|
|
295
|
+
return '';
|
|
296
|
+
return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
|
|
297
|
+
}
|
|
298
|
+
function jpegDimensions(buffer) {
|
|
299
|
+
if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8)
|
|
300
|
+
return '';
|
|
301
|
+
let offset = 2;
|
|
302
|
+
while (offset + 9 < buffer.length) {
|
|
303
|
+
if (buffer[offset] !== 0xff)
|
|
304
|
+
return '';
|
|
305
|
+
const marker = buffer[offset + 1];
|
|
306
|
+
const length = buffer.readUInt16BE(offset + 2);
|
|
307
|
+
if (marker >= 0xc0 && marker <= 0xc3)
|
|
308
|
+
return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
|
|
309
|
+
if (length < 2)
|
|
310
|
+
return '';
|
|
311
|
+
offset += 2 + length;
|
|
312
|
+
}
|
|
313
|
+
return '';
|
|
314
|
+
}
|
|
315
|
+
function uniqueStrings(values) {
|
|
316
|
+
return [...new Set(values.filter(Boolean))];
|
|
317
|
+
}
|
package/package.json
CHANGED
|
@@ -13,6 +13,8 @@ Always keep final reports short. Include these when they apply:
|
|
|
13
13
|
- Truth status, when verification or runtime claims matter.
|
|
14
14
|
- Security notes when auth, payment, DB, secrets, permissions, deployment, dependency, or public-release surfaces are relevant.
|
|
15
15
|
- Source boundaries when reference material shaped the decision.
|
|
16
|
+
- Mission receipt/gate status when real subagents were used and the report claims verified/proven completion.
|
|
17
|
+
- Ueye asset/revision integrity status when protected references or iterative artifact replacement materially affected a visual done claim.
|
|
16
18
|
- Context pressure or cleanup findings when they could affect the next run.
|
|
17
19
|
- Publish blocker evidence when npm/release work fails.
|
|
18
20
|
- Structured diagnostic next action, when a check found a concrete next step.
|
|
@@ -64,6 +64,7 @@ Runtime work needs stronger evidence because long-running processes can create f
|
|
|
64
64
|
- No automatic tmux for ordinary work.
|
|
65
65
|
- No release-blocking runtime proof unless the user chooses `$deep` or `$mission`.
|
|
66
66
|
- No full `$mission` claim without real subagent/team evidence; downgrade to `$deep`, or mark mission partial/blocked.
|
|
67
|
+
- No verified/proven Mission claim without a passing completion gate covering every expected thread; lifecycle stop alone is not success.
|
|
67
68
|
|
|
68
69
|
Design baseline:
|
|
69
70
|
|