yam-flow 2.5.1 → 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.
@@ -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,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
- return weakestTruth(weakest, gate.truth_status);
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
 
package/yam.manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yam",
3
- "version": "2.5.1",
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, and P0-P3 issue tracking."
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",