yam-flow 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/COMMANDS.md +27 -1
- package/README.md +24 -2
- package/ROADMAP.md +42 -7
- package/dist/bin/yam.js +406 -23
- package/dist/lib/benchmark-promotion.d.ts +53 -0
- package/dist/lib/benchmark-promotion.js +204 -0
- package/dist/lib/bounded-input.d.ts +12 -0
- package/dist/lib/bounded-input.js +48 -0
- package/dist/lib/design-production.d.ts +306 -0
- package/dist/lib/design-production.js +1341 -0
- package/dist/lib/external-updates.d.ts +23 -0
- package/dist/lib/external-updates.js +344 -23
- package/dist/lib/gate-result.d.ts +44 -0
- package/dist/lib/gate-result.js +219 -0
- package/dist/lib/skill-installation.js +23 -5
- package/dist/lib/stable-order.d.ts +1 -0
- package/dist/lib/stable-order.js +5 -0
- package/dist/lib/trust-kernel.js +17 -1
- package/dist/lib/ueye-artifacts.js +3 -2
- package/package.json +6 -2
- package/references/ueye.md +14 -0
- package/skills/ueye/SKILL.md +11 -0
- package/src/bin/yam.ts +415 -19
- package/src/lib/benchmark-promotion.ts +249 -0
- package/src/lib/bounded-input.ts +59 -0
- package/src/lib/design-production.ts +1441 -0
- package/src/lib/external-updates.ts +397 -24
- package/src/lib/gate-result.ts +223 -0
- package/src/lib/skill-installation.ts +24 -5
- package/src/lib/stable-order.ts +4 -0
- package/src/lib/trust-kernel.ts +20 -1
- package/src/lib/ueye-artifacts.ts +3 -2
- package/yam.manifest.json +2 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export type GateBoundary = 'release' | 'update' | 'mission' | 'generic';
|
|
4
|
+
export type GateCheckStatus = 'passed' | 'failed' | 'skipped';
|
|
5
|
+
|
|
6
|
+
export interface GateCheckInput {
|
|
7
|
+
id?: string;
|
|
8
|
+
status?: GateCheckStatus;
|
|
9
|
+
note?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StrictGateInput {
|
|
14
|
+
gate_id?: string;
|
|
15
|
+
boundary?: GateBoundary;
|
|
16
|
+
checks?: GateCheckInput[];
|
|
17
|
+
blockers?: string[];
|
|
18
|
+
evidence?: string[];
|
|
19
|
+
next_action?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StrictGateResult {
|
|
23
|
+
schema: 'yam.gate-result.v1';
|
|
24
|
+
generated_at: string;
|
|
25
|
+
gate_id: string;
|
|
26
|
+
boundary: GateBoundary;
|
|
27
|
+
status: 'passed' | 'failed';
|
|
28
|
+
checks: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
status: GateCheckStatus;
|
|
31
|
+
note: string;
|
|
32
|
+
required: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
blockers: string[];
|
|
35
|
+
evidence: string[];
|
|
36
|
+
next_action: string;
|
|
37
|
+
contract_valid: boolean;
|
|
38
|
+
contract_errors: string[];
|
|
39
|
+
digest: string;
|
|
40
|
+
truth_status: 'verified' | 'blocked';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const GATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,95}$/;
|
|
44
|
+
const BOUNDARIES = new Set<GateBoundary>(['release', 'update', 'mission', 'generic']);
|
|
45
|
+
const CHECK_STATUSES = new Set<GateCheckStatus>(['passed', 'failed', 'skipped']);
|
|
46
|
+
|
|
47
|
+
export function buildStrictGateResult(input: StrictGateInput = {}): StrictGateResult {
|
|
48
|
+
const gateId = boundedText(input.gate_id || '', 96);
|
|
49
|
+
const boundary = BOUNDARIES.has(input.boundary as GateBoundary) ? input.boundary as GateBoundary : 'generic';
|
|
50
|
+
const contractErrors: string[] = [];
|
|
51
|
+
if (!GATE_ID_PATTERN.test(gateId)) contractErrors.push('gate_id_invalid');
|
|
52
|
+
if (!BOUNDARIES.has(input.boundary as GateBoundary)) contractErrors.push('boundary_invalid');
|
|
53
|
+
if (Array.isArray(input.checks) && input.checks.length > 128) contractErrors.push('check_limit_exceeded');
|
|
54
|
+
if (Array.isArray(input.blockers) && input.blockers.length > 64) contractErrors.push('blocker_limit_exceeded');
|
|
55
|
+
if (Array.isArray(input.evidence) && input.evidence.length > 128) contractErrors.push('evidence_limit_exceeded');
|
|
56
|
+
|
|
57
|
+
const checks = Array.isArray(input.checks) ? input.checks.slice(0, 128).map((check, index) => {
|
|
58
|
+
const id = boundedText(check?.id || '', 96);
|
|
59
|
+
const status = CHECK_STATUSES.has(check?.status as GateCheckStatus) ? check?.status as GateCheckStatus : 'failed';
|
|
60
|
+
if (!GATE_ID_PATTERN.test(id)) contractErrors.push(`check_id_invalid:${index}`);
|
|
61
|
+
if (!CHECK_STATUSES.has(check?.status as GateCheckStatus)) contractErrors.push(`check_status_invalid:${id || index}`);
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
status,
|
|
65
|
+
note: boundedText(check?.note || '', 512),
|
|
66
|
+
required: check?.required !== false
|
|
67
|
+
};
|
|
68
|
+
}) : [];
|
|
69
|
+
if (!checks.length) contractErrors.push('checks_missing');
|
|
70
|
+
const checkIds = checks.map((check) => check.id).filter(Boolean);
|
|
71
|
+
if (new Set(checkIds).size !== checkIds.length) contractErrors.push('duplicate_check_id');
|
|
72
|
+
|
|
73
|
+
const blockers = uniqueBounded(input.blockers, 64, 512);
|
|
74
|
+
const evidence = uniqueBounded(input.evidence, 128, 512);
|
|
75
|
+
if (!evidence.length) contractErrors.push('evidence_missing');
|
|
76
|
+
for (const check of checks) {
|
|
77
|
+
if (check.required && check.status !== 'passed') blockers.push(`${check.id || 'unnamed_check'}: ${check.note || 'required gate check did not pass'}`);
|
|
78
|
+
}
|
|
79
|
+
for (const error of contractErrors) blockers.push(`gate_contract_invalid: ${error}`);
|
|
80
|
+
const finalBlockers = [...new Set(blockers)];
|
|
81
|
+
const contractValid = contractErrors.length === 0;
|
|
82
|
+
const passed = contractValid && finalBlockers.length === 0;
|
|
83
|
+
const generatedAt = new Date().toISOString();
|
|
84
|
+
const nextAction = boundedText(input.next_action || finalBlockers[0] || 'gate passed; retain the evidence digest with the dependent completion claim', 512);
|
|
85
|
+
const uniqueContractErrors = [...new Set(contractErrors)];
|
|
86
|
+
const truthStatus = passed ? 'verified' as const : 'blocked' as const;
|
|
87
|
+
const canonical = {
|
|
88
|
+
schema: 'yam.gate-result.v1',
|
|
89
|
+
generated_at: generatedAt,
|
|
90
|
+
gate_id: gateId,
|
|
91
|
+
boundary,
|
|
92
|
+
status: passed ? 'passed' : 'failed',
|
|
93
|
+
checks,
|
|
94
|
+
blockers: finalBlockers,
|
|
95
|
+
evidence,
|
|
96
|
+
next_action: nextAction,
|
|
97
|
+
contract_valid: contractValid,
|
|
98
|
+
contract_errors: uniqueContractErrors,
|
|
99
|
+
truth_status: truthStatus
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
schema: 'yam.gate-result.v1',
|
|
103
|
+
generated_at: generatedAt,
|
|
104
|
+
gate_id: gateId,
|
|
105
|
+
boundary,
|
|
106
|
+
status: passed ? 'passed' : 'failed',
|
|
107
|
+
checks,
|
|
108
|
+
blockers: finalBlockers,
|
|
109
|
+
evidence,
|
|
110
|
+
next_action: nextAction,
|
|
111
|
+
contract_valid: contractValid,
|
|
112
|
+
contract_errors: uniqueContractErrors,
|
|
113
|
+
digest: `sha256:${createHash('sha256').update(stableJson(canonical)).digest('hex')}`,
|
|
114
|
+
truth_status: truthStatus
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function verifyStrictGateResult(value: unknown) {
|
|
119
|
+
const errors: string[] = [];
|
|
120
|
+
const record = value && typeof value === 'object' ? value as Record<string, unknown> : {};
|
|
121
|
+
if (record.schema !== 'yam.gate-result.v1') errors.push('schema_invalid');
|
|
122
|
+
if (typeof record.generated_at !== 'string' || !isIsoDate(record.generated_at)) errors.push('generated_at_invalid');
|
|
123
|
+
if (!GATE_ID_PATTERN.test(String(record.gate_id || ''))) errors.push('gate_id_invalid');
|
|
124
|
+
if (!BOUNDARIES.has(record.boundary as GateBoundary)) errors.push('boundary_invalid');
|
|
125
|
+
if (!['passed', 'failed'].includes(String(record.status || ''))) errors.push('status_invalid');
|
|
126
|
+
const checks = Array.isArray(record.checks) ? record.checks : [];
|
|
127
|
+
const blockers = Array.isArray(record.blockers) ? record.blockers : [];
|
|
128
|
+
const evidence = Array.isArray(record.evidence) ? record.evidence : [];
|
|
129
|
+
const contractErrors = Array.isArray(record.contract_errors) ? record.contract_errors : [];
|
|
130
|
+
if (!checks.length) errors.push('checks_missing');
|
|
131
|
+
if (checks.length > 128) errors.push('check_limit_exceeded');
|
|
132
|
+
if (blockers.length > 64) errors.push('blocker_limit_exceeded');
|
|
133
|
+
if (evidence.length > 128) errors.push('evidence_limit_exceeded');
|
|
134
|
+
if (!Array.isArray(record.blockers) || blockers.some((item) => typeof item !== 'string')) errors.push('blockers_invalid');
|
|
135
|
+
if (!Array.isArray(record.evidence) || !evidence.length || evidence.some((item) => typeof item !== 'string' || !item.trim())) errors.push('evidence_missing');
|
|
136
|
+
if (typeof record.contract_valid !== 'boolean') errors.push('contract_valid_missing');
|
|
137
|
+
if (!Array.isArray(record.contract_errors) || contractErrors.some((item) => typeof item !== 'string')) errors.push('contract_errors_invalid');
|
|
138
|
+
if (record.contract_valid === true && contractErrors.length) errors.push('contract_valid_with_errors');
|
|
139
|
+
if (record.contract_valid === false && !contractErrors.length) errors.push('contract_invalid_without_errors');
|
|
140
|
+
if (record.status === 'passed' && record.contract_valid !== true) errors.push('passed_with_invalid_contract');
|
|
141
|
+
if (typeof record.next_action !== 'string' || !record.next_action.trim()) errors.push('next_action_missing');
|
|
142
|
+
if (!['verified', 'blocked'].includes(String(record.truth_status || ''))) errors.push('truth_status_invalid');
|
|
143
|
+
if (record.status === 'passed' && record.truth_status !== 'verified') errors.push('passed_truth_mismatch');
|
|
144
|
+
if (record.status === 'failed' && record.truth_status !== 'blocked') errors.push('failed_truth_mismatch');
|
|
145
|
+
for (const [index, check] of checks.entries()) {
|
|
146
|
+
if (!check || typeof check !== 'object') {
|
|
147
|
+
errors.push(`check_invalid:${index}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const row = check as Record<string, unknown>;
|
|
151
|
+
if (!GATE_ID_PATTERN.test(String(row.id || ''))) errors.push(`check_id_invalid:${index}`);
|
|
152
|
+
if (!CHECK_STATUSES.has(row.status as GateCheckStatus)) errors.push(`check_status_invalid:${index}`);
|
|
153
|
+
if (typeof row.note !== 'string') errors.push(`check_note_invalid:${index}`);
|
|
154
|
+
if (typeof row.required !== 'boolean') errors.push(`check_required_invalid:${index}`);
|
|
155
|
+
}
|
|
156
|
+
const checkIds = checks
|
|
157
|
+
.filter((check) => check && typeof check === 'object')
|
|
158
|
+
.map((check) => String((check as Record<string, unknown>).id || ''))
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
if (new Set(checkIds).size !== checkIds.length) errors.push('duplicate_check_id');
|
|
161
|
+
if (record.status === 'passed' && Array.isArray(record.blockers) && record.blockers.length > 0) errors.push('passed_with_blockers');
|
|
162
|
+
if (record.status === 'passed' && checks.length) {
|
|
163
|
+
const failedRequired = checks.some((check) => {
|
|
164
|
+
if (!check || typeof check !== 'object') return true;
|
|
165
|
+
const row = check as Record<string, unknown>;
|
|
166
|
+
return row.required !== false && row.status !== 'passed';
|
|
167
|
+
});
|
|
168
|
+
if (failedRequired) errors.push('passed_with_failed_required_check');
|
|
169
|
+
}
|
|
170
|
+
if (record.status === 'failed') {
|
|
171
|
+
const hasFailedRequired = checks.some((check) => {
|
|
172
|
+
if (!check || typeof check !== 'object') return false;
|
|
173
|
+
const row = check as Record<string, unknown>;
|
|
174
|
+
return row.required !== false && row.status !== 'passed';
|
|
175
|
+
});
|
|
176
|
+
if (record.contract_valid === true && blockers.length === 0 && !hasFailedRequired) errors.push('failed_without_blocker_or_failed_check');
|
|
177
|
+
}
|
|
178
|
+
const canonical = {
|
|
179
|
+
schema: record.schema,
|
|
180
|
+
generated_at: record.generated_at,
|
|
181
|
+
gate_id: String(record.gate_id || ''),
|
|
182
|
+
boundary: record.boundary,
|
|
183
|
+
status: record.status,
|
|
184
|
+
checks,
|
|
185
|
+
blockers,
|
|
186
|
+
evidence,
|
|
187
|
+
next_action: record.next_action,
|
|
188
|
+
contract_valid: record.contract_valid,
|
|
189
|
+
contract_errors: contractErrors,
|
|
190
|
+
truth_status: record.truth_status
|
|
191
|
+
};
|
|
192
|
+
const expectedDigest = `sha256:${createHash('sha256').update(stableJson(canonical)).digest('hex')}`;
|
|
193
|
+
if (typeof record.digest !== 'string' || record.digest !== expectedDigest) errors.push('digest_invalid');
|
|
194
|
+
return {
|
|
195
|
+
schema: 'yam.gate-result-verification.v1' as const,
|
|
196
|
+
valid: errors.length === 0,
|
|
197
|
+
errors: [...new Set(errors)],
|
|
198
|
+
truth_status: errors.length ? 'blocked' as const : 'verified' as const,
|
|
199
|
+
next_action: errors.length ? `reject the gate result and repair: ${errors[0]}` : 'gate result contract is structurally valid'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isIsoDate(value: string) {
|
|
204
|
+
return !Number.isNaN(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function uniqueBounded(value: unknown, maxItems: number, maxChars: number) {
|
|
208
|
+
if (!Array.isArray(value)) return [];
|
|
209
|
+
return [...new Set(value.map((item) => boundedText(item, maxChars)).filter(Boolean))].slice(0, maxItems);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function boundedText(value: unknown, max: number) {
|
|
213
|
+
return String(value || '').replace(/[\r\n\u0000-\u001f\u007f]+/g, ' ').trim().slice(0, max);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stableJson(value: unknown): string {
|
|
217
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(',')}]`;
|
|
218
|
+
if (value && typeof value === 'object') {
|
|
219
|
+
const record = value as Record<string, unknown>;
|
|
220
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`;
|
|
221
|
+
}
|
|
222
|
+
return JSON.stringify(value);
|
|
223
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { compareStableText } from './stable-order.js';
|
|
4
5
|
|
|
5
6
|
export const INSTALL_RECEIPT_NAME = '.yam-flow-install-receipt.json';
|
|
6
7
|
export const INSTALL_LOCK_NAME = '.yam-flow-install.lock';
|
|
@@ -211,7 +212,7 @@ async function collectTreeManifest(root: string, prefix = ''): Promise<InstallFi
|
|
|
211
212
|
const files: InstallFileDigest[] = [];
|
|
212
213
|
async function walk(current: string, relative: string): Promise<void> {
|
|
213
214
|
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
214
|
-
entries.sort((left, right) => left.name
|
|
215
|
+
entries.sort((left, right) => compareStableText(left.name, right.name));
|
|
215
216
|
for (const entry of entries) {
|
|
216
217
|
const absolute = path.join(current, entry.name);
|
|
217
218
|
const nextRelative = relative ? path.join(relative, entry.name) : entry.name;
|
|
@@ -226,7 +227,7 @@ async function collectTreeManifest(root: string, prefix = ''): Promise<InstallFi
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
await walk(root, '');
|
|
229
|
-
return files.sort((left, right) => left.path
|
|
230
|
+
return files.sort((left, right) => compareStableText(left.path, right.path));
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
async function expectedInstallManifest(sourceRoot: string, skills: string[]) {
|
|
@@ -253,7 +254,7 @@ async function expectedInstallManifest(sourceRoot: string, skills: string[]) {
|
|
|
253
254
|
});
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
|
-
return files.sort((left, right) => left.path
|
|
257
|
+
return files.sort((left, right) => compareStableText(left.path, right.path));
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
async function copyTree(source: string, target: string): Promise<void> {
|
|
@@ -278,8 +279,12 @@ async function copyTree(source: string, target: string): Promise<void> {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
function manifestDigest(files: InstallFileDigest[]) {
|
|
282
|
+
return digestManifestFiles([...files].sort((left, right) => compareStableText(left.path, right.path)));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function digestManifestFiles(files: InstallFileDigest[]) {
|
|
281
286
|
const digest = createHash('sha256');
|
|
282
|
-
for (const file of
|
|
287
|
+
for (const file of files) {
|
|
283
288
|
digest.update(file.path);
|
|
284
289
|
digest.update('\0');
|
|
285
290
|
digest.update(String(file.bytes));
|
|
@@ -290,6 +295,20 @@ function manifestDigest(files: InstallFileDigest[]) {
|
|
|
290
295
|
return digest.digest('hex');
|
|
291
296
|
}
|
|
292
297
|
|
|
298
|
+
function allowsLegacyManifestOrder(version: unknown) {
|
|
299
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(version || ''));
|
|
300
|
+
if (!match) return false;
|
|
301
|
+
const major = Number(match[1]);
|
|
302
|
+
const minor = Number(match[2]);
|
|
303
|
+
const patch = Number(match[3]);
|
|
304
|
+
return major < 2 || (major === 2 && (minor < 5 || (minor === 5 && patch === 0)));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function receiptManifestDigestMatches(files: InstallFileDigest[], digest: unknown, version: unknown) {
|
|
308
|
+
if (digest === manifestDigest(files)) return true;
|
|
309
|
+
return allowsLegacyManifestOrder(version) && digest === digestManifestFiles(files);
|
|
310
|
+
}
|
|
311
|
+
|
|
293
312
|
function manifestIssues(expected: InstallFileDigest[], actual: InstallFileDigest[], label: string) {
|
|
294
313
|
const issues: string[] = [];
|
|
295
314
|
const expectedByPath = new Map(expected.map((file) => [file.path, file]));
|
|
@@ -379,7 +398,7 @@ function ownershipReceiptIssues(
|
|
|
379
398
|
if (value.integrity?.file_count !== files.length) {
|
|
380
399
|
issues.push('install receipt file count does not match its manifest');
|
|
381
400
|
}
|
|
382
|
-
if (value.integrity?.source_digest
|
|
401
|
+
if (!receiptManifestDigestMatches(files, value.integrity?.source_digest, value.package?.version)) {
|
|
383
402
|
issues.push('install receipt source digest does not match its manifest');
|
|
384
403
|
}
|
|
385
404
|
if (Array.isArray(value.skills)) {
|
package/src/lib/trust-kernel.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { verifyStrictGateResult } from './gate-result.js';
|
|
2
|
+
|
|
1
3
|
export const TRUTH_STATUSES = Object.freeze([
|
|
2
4
|
'verified',
|
|
3
5
|
'proven',
|
|
@@ -1395,7 +1397,24 @@ function missionCompletionCap(value: unknown = ''): TruthStatus {
|
|
|
1395
1397
|
const parsed = parseStructuredValue(item);
|
|
1396
1398
|
if (!parsed || typeof parsed !== 'object') return weakestTruth(weakest, 'blocked');
|
|
1397
1399
|
const gate = buildMissionCompletionGate(parsed as Record<string, unknown>);
|
|
1398
|
-
|
|
1400
|
+
const record = parsed as Record<string, unknown>;
|
|
1401
|
+
const strictGate = record.gate_result && typeof record.gate_result === 'object'
|
|
1402
|
+
? record.gate_result as Record<string, unknown>
|
|
1403
|
+
: null;
|
|
1404
|
+
const reportedContract = record.gate_contract && typeof record.gate_contract === 'object'
|
|
1405
|
+
? record.gate_contract as Record<string, unknown>
|
|
1406
|
+
: null;
|
|
1407
|
+
const strictVerification = verifyStrictGateResult(strictGate);
|
|
1408
|
+
const strictReady = Boolean(
|
|
1409
|
+
strictGate
|
|
1410
|
+
&& strictGate.boundary === 'mission'
|
|
1411
|
+
&& strictGate.gate_id === 'mission-completion'
|
|
1412
|
+
&& strictGate.status === 'passed'
|
|
1413
|
+
&& strictVerification.valid
|
|
1414
|
+
&& reportedContract?.valid === true
|
|
1415
|
+
&& reportedContract?.truth_status === 'verified'
|
|
1416
|
+
);
|
|
1417
|
+
return weakestTruth(weakest, strictReady ? gate.truth_status : 'blocked');
|
|
1399
1418
|
}, 'proven');
|
|
1400
1419
|
}
|
|
1401
1420
|
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
3
|
import fsp from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { compareStableText } from './stable-order.js';
|
|
5
6
|
|
|
6
7
|
export interface InspectedImageFile {
|
|
7
8
|
path: string;
|
|
@@ -102,7 +103,7 @@ export async function upsertUeyeAsset(input: {
|
|
|
102
103
|
};
|
|
103
104
|
if (existingIndex >= 0) manifest.assets[existingIndex] = entry;
|
|
104
105
|
else manifest.assets.push(entry);
|
|
105
|
-
manifest.assets.sort((left, right) => left.id
|
|
106
|
+
manifest.assets.sort((left, right) => compareStableText(left.id, right.id));
|
|
106
107
|
manifest.updated_at = new Date().toISOString();
|
|
107
108
|
await writeJsonAtomic(manifestPath, manifest);
|
|
108
109
|
const warnings = assetWarnings(entry);
|
|
@@ -205,7 +206,7 @@ export async function archiveUeyeRevision(input: {
|
|
|
205
206
|
} else {
|
|
206
207
|
history.revisions.push(entry);
|
|
207
208
|
}
|
|
208
|
-
history.revisions.sort((left, right) => left.round - right.round || left.artifact_id
|
|
209
|
+
history.revisions.sort((left, right) => left.round - right.round || compareStableText(left.artifact_id, right.artifact_id));
|
|
209
210
|
history.updated_at = new Date().toISOString();
|
|
210
211
|
await writeJsonAtomic(manifestPath, history);
|
|
211
212
|
return {
|
package/yam.manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yam",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"principles": [
|
|
5
5
|
"Direction before execution.",
|
|
6
6
|
"Start fast.",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
{
|
|
30
30
|
"id": "ueye",
|
|
31
31
|
"stage": "v0.4",
|
|
32
|
-
"purpose": "Tight UI/UX/design implementation and visual review with visual provenance, local asset integrity, revision history, reference-read proof, visual comparison, design quality review,
|
|
32
|
+
"purpose": "Tight UI/UX/design implementation and visual review with visual provenance, local asset integrity, revision history, reference-read proof, visual comparison, design quality review, P0-P3 issue tracking, and an opt-in demand-gated Canvas/two-revision/final-gallery production phase."
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"id": "question",
|