yam-flow 2.7.0 → 2.8.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/AGENTS.md +1 -0
- package/CHANGELOG.md +29 -0
- package/COMMANDS.md +42 -5
- package/README.md +40 -8
- package/ROADMAP.md +17 -6
- package/dist/bin/yam.js +441 -70
- package/dist/lib/external-updates.d.ts +9 -0
- package/dist/lib/external-updates.js +232 -4
- package/dist/lib/next-step.d.ts +61 -0
- package/dist/lib/next-step.js +340 -0
- package/dist/lib/release-auth-readiness.d.ts +81 -0
- package/dist/lib/release-auth-readiness.js +106 -0
- package/dist/lib/release-registry-status.d.ts +18 -0
- package/dist/lib/release-registry-status.js +37 -0
- package/dist/lib/scout-receipt.d.ts +89 -0
- package/dist/lib/scout-receipt.js +542 -0
- package/dist/lib/skill-installation.d.ts +30 -0
- package/dist/lib/skill-installation.js +470 -82
- package/dist/lib/trust-kernel.d.ts +2 -0
- package/dist/lib/trust-kernel.js +21 -2
- package/package.json +22 -3
- package/references/final-report.md +15 -1
- package/references/next-step.md +67 -0
- package/references/scout-wide-scan.md +16 -0
- package/references/study-note.md +4 -2
- package/references/tool-trust-layer.md +2 -0
- package/skills/deep/SKILL.md +2 -0
- package/skills/mission/SKILL.md +2 -0
- package/skills/quick/SKILL.md +1 -0
- package/skills/scout/SKILL.md +19 -9
- package/skills/ueye/SKILL.md +1 -0
- package/src/bin/yam.ts +445 -68
- package/src/lib/external-updates.ts +262 -4
- package/src/lib/next-step.ts +397 -0
- package/src/lib/release-auth-readiness.ts +148 -0
- package/src/lib/release-registry-status.ts +55 -0
- package/src/lib/scout-receipt.ts +593 -0
- package/src/lib/skill-installation.ts +542 -87
- package/src/lib/trust-kernel.ts +25 -2
- package/yam.manifest.json +4 -3
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const MAX_STEPS = 12;
|
|
3
|
+
const MAX_LIST_ITEMS = 8;
|
|
4
|
+
const MAX_CONTRACT_ERRORS = 64;
|
|
5
|
+
const EVIDENCE_LEVELS = new Set(['L0', 'L1', 'L2', 'L3', 'L4', 'L5']);
|
|
6
|
+
const EVIDENCE_RANK = { L0: 0, L1: 1, L2: 2, L3: 3, L4: 4, L5: 5 };
|
|
7
|
+
const STEP_KINDS = new Set(['fix_first', 'planned']);
|
|
8
|
+
const TRUTH_STATUSES = new Set([
|
|
9
|
+
'proven',
|
|
10
|
+
'verified',
|
|
11
|
+
'partial',
|
|
12
|
+
'fixture_only',
|
|
13
|
+
'fixture_instrumented_real',
|
|
14
|
+
'integration_optional',
|
|
15
|
+
'real_required_missing',
|
|
16
|
+
'skipped',
|
|
17
|
+
'blocked',
|
|
18
|
+
'assumed'
|
|
19
|
+
]);
|
|
20
|
+
const OWNER_ROUTES = new Set(['$quick', '$ueye', '$question', '$scout', '$deep', '$mission']);
|
|
21
|
+
const INPUT_KEYS = ['current_situation', 'forward_outlook', 'critical_opinion', 'improvement_recommendations', 'steps', 'evidence_level', 'evidence_stamp', 'truth_status'];
|
|
22
|
+
const STEP_INPUT_KEYS = ['kind', 'action', 'why', 'owner_route', 'owner_scope', 'blocked_by', 'safe_retry', 'side_effects'];
|
|
23
|
+
const TOP_LEVEL_KEYS = ['schema', 'scan', 'steps', 'evidence', 'contract_valid', 'contract_errors', 'truth_status', 'digest'];
|
|
24
|
+
const SCAN_KEYS = ['current_situation', 'forward_outlook', 'critical_opinion', 'improvement_recommendations'];
|
|
25
|
+
const STEP_KEYS = ['order', 'kind', 'action', 'why', 'owner_route', 'owner_scope', 'blocked_by', 'safe_retry', 'side_effects'];
|
|
26
|
+
const EVIDENCE_KEYS = ['level', 'stamp'];
|
|
27
|
+
export function buildNextStep(input = {}) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
const source = isRecord(input) ? input : {};
|
|
30
|
+
if (!isRecord(input))
|
|
31
|
+
errors.push('input_invalid');
|
|
32
|
+
else
|
|
33
|
+
unexpectedKeys(input, INPUT_KEYS, 'input', errors);
|
|
34
|
+
const currentSituation = requiredText(source.current_situation, 'current_situation', 1200, errors);
|
|
35
|
+
const forwardOutlook = requiredText(source.forward_outlook, 'forward_outlook', 1200, errors);
|
|
36
|
+
const criticalOpinion = requiredText(source.critical_opinion, 'critical_opinion', 1200, errors);
|
|
37
|
+
const improvementRecommendations = textList(source.improvement_recommendations, 'improvement_recommendations', MAX_LIST_ITEMS, 600, true, errors);
|
|
38
|
+
const rawSteps = Array.isArray(source.steps) ? source.steps : [];
|
|
39
|
+
if (!Array.isArray(source.steps) || !rawSteps.length)
|
|
40
|
+
errors.push('steps_missing');
|
|
41
|
+
if (rawSteps.length > MAX_STEPS)
|
|
42
|
+
errors.push('step_limit_exceeded');
|
|
43
|
+
let plannedSeen = false;
|
|
44
|
+
const steps = rawSteps.slice(0, MAX_STEPS).map((value, index) => {
|
|
45
|
+
const step = isRecord(value) ? value : {};
|
|
46
|
+
if (!isRecord(value))
|
|
47
|
+
errors.push(`steps[${index}]_invalid`);
|
|
48
|
+
else
|
|
49
|
+
unexpectedKeys(value, STEP_INPUT_KEYS, `steps[${index}]`, errors);
|
|
50
|
+
const rawKind = normalizedText(step.kind, 32);
|
|
51
|
+
const kind = STEP_KINDS.has(rawKind) ? rawKind : 'planned';
|
|
52
|
+
if (!STEP_KINDS.has(rawKind))
|
|
53
|
+
errors.push(`steps[${index}].kind_invalid`);
|
|
54
|
+
if (kind === 'planned')
|
|
55
|
+
plannedSeen = true;
|
|
56
|
+
if (kind === 'fix_first' && plannedSeen)
|
|
57
|
+
errors.push(`steps[${index}].fix_first_out_of_order`);
|
|
58
|
+
const rawRoute = normalizedText(step.owner_route, 32).toLowerCase();
|
|
59
|
+
const routeWithSigil = rawRoute && !rawRoute.startsWith('$') ? `$${rawRoute}` : rawRoute;
|
|
60
|
+
const ownerRoute = OWNER_ROUTES.has(routeWithSigil)
|
|
61
|
+
? routeWithSigil
|
|
62
|
+
: '$quick';
|
|
63
|
+
if (!OWNER_ROUTES.has(routeWithSigil))
|
|
64
|
+
errors.push(`steps[${index}].owner_route_invalid`);
|
|
65
|
+
const blockedBy = textList(step.blocked_by, `steps[${index}].blocked_by`, MAX_LIST_ITEMS, 500, false, errors);
|
|
66
|
+
if (step.safe_retry !== undefined && typeof step.safe_retry !== 'string') {
|
|
67
|
+
errors.push(`steps[${index}].safe_retry_invalid`);
|
|
68
|
+
}
|
|
69
|
+
const suppliedSafeRetry = normalizedText(step.safe_retry, 800);
|
|
70
|
+
if (normalizedLength(step.safe_retry) > 800)
|
|
71
|
+
errors.push(`steps[${index}].safe_retry_too_long`);
|
|
72
|
+
if (blockedBy.length && !suppliedSafeRetry)
|
|
73
|
+
errors.push(`steps[${index}].safe_retry_missing`);
|
|
74
|
+
const safeRetry = suppliedSafeRetry || 'not required; confirm the recorded evidence is still current before execution';
|
|
75
|
+
return {
|
|
76
|
+
order: index + 1,
|
|
77
|
+
kind,
|
|
78
|
+
action: requiredText(step.action, `steps[${index}].action`, 800, errors),
|
|
79
|
+
why: requiredText(step.why, `steps[${index}].why`, 800, errors),
|
|
80
|
+
owner_route: ownerRoute,
|
|
81
|
+
owner_scope: textList(step.owner_scope, `steps[${index}].owner_scope`, MAX_LIST_ITEMS, 300, true, errors),
|
|
82
|
+
blocked_by: blockedBy,
|
|
83
|
+
safe_retry: safeRetry,
|
|
84
|
+
side_effects: textList(step.side_effects, `steps[${index}].side_effects`, MAX_LIST_ITEMS, 500, true, errors)
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
const actions = steps.map((step) => step.action).filter(Boolean);
|
|
88
|
+
if (new Set(actions).size !== actions.length)
|
|
89
|
+
errors.push('duplicate_step_action');
|
|
90
|
+
const rawEvidenceLevel = normalizedText(source.evidence_level, 8);
|
|
91
|
+
const evidenceLevel = EVIDENCE_LEVELS.has(rawEvidenceLevel)
|
|
92
|
+
? rawEvidenceLevel
|
|
93
|
+
: 'L0';
|
|
94
|
+
if (!EVIDENCE_LEVELS.has(rawEvidenceLevel))
|
|
95
|
+
errors.push('evidence_level_invalid');
|
|
96
|
+
const evidenceStamp = requiredText(source.evidence_stamp, 'evidence_stamp', 512, errors);
|
|
97
|
+
const rawTruthStatus = normalizedText(source.truth_status, 48);
|
|
98
|
+
const requestedTruth = TRUTH_STATUSES.has(rawTruthStatus)
|
|
99
|
+
? rawTruthStatus
|
|
100
|
+
: 'assumed';
|
|
101
|
+
if (!TRUTH_STATUSES.has(rawTruthStatus))
|
|
102
|
+
errors.push('truth_status_invalid');
|
|
103
|
+
const hasBlockers = steps.some((step) => step.blocked_by.length > 0);
|
|
104
|
+
if (hasBlockers && requestedTruth !== 'blocked')
|
|
105
|
+
errors.push('truth_status_contradicts_blockers');
|
|
106
|
+
if (!hasBlockers && requestedTruth === 'blocked')
|
|
107
|
+
errors.push('blocked_truth_without_blocker');
|
|
108
|
+
if (requestedTruth === 'verified' && EVIDENCE_RANK[evidenceLevel] < EVIDENCE_RANK.L2) {
|
|
109
|
+
errors.push('verified_truth_requires_l2_evidence');
|
|
110
|
+
}
|
|
111
|
+
if (requestedTruth === 'proven' && EVIDENCE_RANK[evidenceLevel] < EVIDENCE_RANK.L4) {
|
|
112
|
+
errors.push('proven_truth_requires_l4_evidence');
|
|
113
|
+
}
|
|
114
|
+
const contractErrors = boundedContractErrors(errors);
|
|
115
|
+
const contractValid = contractErrors.length === 0;
|
|
116
|
+
const truthStatus = contractValid
|
|
117
|
+
? hasBlockers ? 'blocked' : requestedTruth
|
|
118
|
+
: 'blocked';
|
|
119
|
+
const canonical = {
|
|
120
|
+
schema: 'yam.next-step.v1',
|
|
121
|
+
scan: {
|
|
122
|
+
current_situation: currentSituation,
|
|
123
|
+
forward_outlook: forwardOutlook,
|
|
124
|
+
critical_opinion: criticalOpinion,
|
|
125
|
+
improvement_recommendations: improvementRecommendations
|
|
126
|
+
},
|
|
127
|
+
steps,
|
|
128
|
+
evidence: { level: evidenceLevel, stamp: evidenceStamp },
|
|
129
|
+
contract_valid: contractValid,
|
|
130
|
+
contract_errors: contractErrors,
|
|
131
|
+
truth_status: truthStatus
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
...canonical,
|
|
135
|
+
digest: digest(canonical)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function verifyNextStep(value) {
|
|
139
|
+
const errors = [];
|
|
140
|
+
const record = isRecord(value) ? value : {};
|
|
141
|
+
exactKeys(record, TOP_LEVEL_KEYS, 'receipt', errors);
|
|
142
|
+
if (record.schema !== 'yam.next-step.v1')
|
|
143
|
+
errors.push('schema_invalid');
|
|
144
|
+
const scan = isRecord(record.scan) ? record.scan : {};
|
|
145
|
+
if (!isRecord(record.scan))
|
|
146
|
+
errors.push('scan_invalid');
|
|
147
|
+
exactKeys(scan, SCAN_KEYS, 'scan', errors);
|
|
148
|
+
validateText(scan.current_situation, 'current_situation', 1200, errors);
|
|
149
|
+
validateText(scan.forward_outlook, 'forward_outlook', 1200, errors);
|
|
150
|
+
validateText(scan.critical_opinion, 'critical_opinion', 1200, errors);
|
|
151
|
+
validateTextList(scan.improvement_recommendations, 'improvement_recommendations', MAX_LIST_ITEMS, 600, true, errors);
|
|
152
|
+
const steps = Array.isArray(record.steps) ? record.steps : [];
|
|
153
|
+
if (!Array.isArray(record.steps) || !steps.length)
|
|
154
|
+
errors.push('steps_missing');
|
|
155
|
+
if (steps.length > MAX_STEPS)
|
|
156
|
+
errors.push('step_limit_exceeded');
|
|
157
|
+
let plannedSeen = false;
|
|
158
|
+
let hasBlockers = false;
|
|
159
|
+
const actions = [];
|
|
160
|
+
for (const [index, value] of steps.entries()) {
|
|
161
|
+
const step = isRecord(value) ? value : {};
|
|
162
|
+
if (!isRecord(value))
|
|
163
|
+
errors.push(`steps[${index}]_invalid`);
|
|
164
|
+
exactKeys(step, STEP_KEYS, `steps[${index}]`, errors);
|
|
165
|
+
if (step.order !== index + 1)
|
|
166
|
+
errors.push(`steps[${index}].order_invalid`);
|
|
167
|
+
if (!STEP_KINDS.has(step.kind))
|
|
168
|
+
errors.push(`steps[${index}].kind_invalid`);
|
|
169
|
+
if (step.kind === 'planned')
|
|
170
|
+
plannedSeen = true;
|
|
171
|
+
if (step.kind === 'fix_first' && plannedSeen)
|
|
172
|
+
errors.push(`steps[${index}].fix_first_out_of_order`);
|
|
173
|
+
validateText(step.action, `steps[${index}].action`, 800, errors);
|
|
174
|
+
validateText(step.why, `steps[${index}].why`, 800, errors);
|
|
175
|
+
if (typeof step.action === 'string' && step.action)
|
|
176
|
+
actions.push(step.action);
|
|
177
|
+
if (!OWNER_ROUTES.has(step.owner_route))
|
|
178
|
+
errors.push(`steps[${index}].owner_route_invalid`);
|
|
179
|
+
validateTextList(step.owner_scope, `steps[${index}].owner_scope`, MAX_LIST_ITEMS, 300, true, errors);
|
|
180
|
+
validateTextList(step.blocked_by, `steps[${index}].blocked_by`, MAX_LIST_ITEMS, 500, false, errors);
|
|
181
|
+
validateText(step.safe_retry, `steps[${index}].safe_retry`, 800, errors);
|
|
182
|
+
validateTextList(step.side_effects, `steps[${index}].side_effects`, MAX_LIST_ITEMS, 500, true, errors);
|
|
183
|
+
const blockedBy = Array.isArray(step.blocked_by) ? step.blocked_by : [];
|
|
184
|
+
if (blockedBy.length)
|
|
185
|
+
hasBlockers = true;
|
|
186
|
+
}
|
|
187
|
+
if (new Set(actions).size !== actions.length)
|
|
188
|
+
errors.push('duplicate_step_action');
|
|
189
|
+
const evidence = isRecord(record.evidence) ? record.evidence : {};
|
|
190
|
+
if (!isRecord(record.evidence))
|
|
191
|
+
errors.push('evidence_invalid');
|
|
192
|
+
exactKeys(evidence, EVIDENCE_KEYS, 'evidence', errors);
|
|
193
|
+
if (!EVIDENCE_LEVELS.has(evidence.level))
|
|
194
|
+
errors.push('evidence_level_invalid');
|
|
195
|
+
validateText(evidence.stamp, 'evidence_stamp', 512, errors);
|
|
196
|
+
if (!TRUTH_STATUSES.has(record.truth_status))
|
|
197
|
+
errors.push('truth_status_invalid');
|
|
198
|
+
if (hasBlockers && record.truth_status !== 'blocked')
|
|
199
|
+
errors.push('truth_status_contradicts_blockers');
|
|
200
|
+
if (!hasBlockers && record.truth_status === 'blocked' && record.contract_valid === true)
|
|
201
|
+
errors.push('blocked_truth_without_blocker');
|
|
202
|
+
if (record.truth_status === 'verified' && EVIDENCE_RANK[evidence.level] < EVIDENCE_RANK.L2) {
|
|
203
|
+
errors.push('verified_truth_requires_l2_evidence');
|
|
204
|
+
}
|
|
205
|
+
if (record.truth_status === 'proven' && EVIDENCE_RANK[evidence.level] < EVIDENCE_RANK.L4) {
|
|
206
|
+
errors.push('proven_truth_requires_l4_evidence');
|
|
207
|
+
}
|
|
208
|
+
const contractErrors = Array.isArray(record.contract_errors) ? record.contract_errors : [];
|
|
209
|
+
if (!Array.isArray(record.contract_errors) || contractErrors.some((item) => typeof item !== 'string' || !item)) {
|
|
210
|
+
errors.push('contract_errors_invalid');
|
|
211
|
+
}
|
|
212
|
+
if (contractErrors.length > MAX_CONTRACT_ERRORS)
|
|
213
|
+
errors.push('contract_error_limit_exceeded');
|
|
214
|
+
if (typeof record.contract_valid !== 'boolean')
|
|
215
|
+
errors.push('contract_valid_missing');
|
|
216
|
+
if (record.contract_valid === true && contractErrors.length)
|
|
217
|
+
errors.push('contract_valid_with_errors');
|
|
218
|
+
if (record.contract_valid === false)
|
|
219
|
+
errors.push('receipt_contract_invalid');
|
|
220
|
+
const canonical = {
|
|
221
|
+
schema: record.schema,
|
|
222
|
+
scan: record.scan,
|
|
223
|
+
steps: record.steps,
|
|
224
|
+
evidence: record.evidence,
|
|
225
|
+
contract_valid: record.contract_valid,
|
|
226
|
+
contract_errors: record.contract_errors,
|
|
227
|
+
truth_status: record.truth_status
|
|
228
|
+
};
|
|
229
|
+
const expectedDigest = digest(canonical);
|
|
230
|
+
if (typeof record.digest !== 'string' || record.digest !== expectedDigest)
|
|
231
|
+
errors.push('digest_invalid');
|
|
232
|
+
const uniqueErrors = [...new Set(errors)];
|
|
233
|
+
return {
|
|
234
|
+
schema: 'yam.next-step-verification.v1',
|
|
235
|
+
valid: uniqueErrors.length === 0,
|
|
236
|
+
errors: uniqueErrors,
|
|
237
|
+
receipt_truth_status: TRUTH_STATUSES.has(record.truth_status)
|
|
238
|
+
? record.truth_status
|
|
239
|
+
: 'blocked',
|
|
240
|
+
truth_status: uniqueErrors.length ? 'blocked' : 'verified',
|
|
241
|
+
next_action: uniqueErrors.length
|
|
242
|
+
? `reject the Next step receipt and repair: ${uniqueErrors[0]}`
|
|
243
|
+
: 'retain the digest with the final report and execute steps in order'
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function requiredText(value, label, max, errors) {
|
|
247
|
+
if (typeof value !== 'string') {
|
|
248
|
+
errors.push(`${label}_invalid`);
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
const text = normalizedText(value, max);
|
|
252
|
+
if (!text)
|
|
253
|
+
errors.push(`${label}_missing`);
|
|
254
|
+
if (normalizedLength(value) > max)
|
|
255
|
+
errors.push(`${label}_too_long`);
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
function textList(value, label, maxItems, maxChars, required, errors) {
|
|
259
|
+
const items = Array.isArray(value) ? value : [];
|
|
260
|
+
if (!Array.isArray(value) && value !== undefined)
|
|
261
|
+
errors.push(`${label}_invalid`);
|
|
262
|
+
if (items.length > maxItems)
|
|
263
|
+
errors.push(`${label}_limit_exceeded`);
|
|
264
|
+
const normalized = items.slice(0, maxItems).map((item, index) => requiredText(item, `${label}[${index}]`, maxChars, errors)).filter(Boolean);
|
|
265
|
+
const unique = [...new Set(normalized)];
|
|
266
|
+
if (unique.length !== normalized.length)
|
|
267
|
+
errors.push(`${label}_duplicate`);
|
|
268
|
+
if (required && !unique.length)
|
|
269
|
+
errors.push(`${label}_missing`);
|
|
270
|
+
return unique;
|
|
271
|
+
}
|
|
272
|
+
function validateText(value, label, max, errors) {
|
|
273
|
+
if (typeof value !== 'string' || !value || value !== normalizedText(value, max))
|
|
274
|
+
errors.push(`${label}_invalid`);
|
|
275
|
+
if (typeof value === 'string' && value.length > max)
|
|
276
|
+
errors.push(`${label}_too_long`);
|
|
277
|
+
}
|
|
278
|
+
function validateTextList(value, label, maxItems, maxChars, required, errors) {
|
|
279
|
+
if (!Array.isArray(value)) {
|
|
280
|
+
errors.push(`${label}_invalid`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (value.length > maxItems)
|
|
284
|
+
errors.push(`${label}_limit_exceeded`);
|
|
285
|
+
if (required && !value.length)
|
|
286
|
+
errors.push(`${label}_missing`);
|
|
287
|
+
for (const [index, item] of value.entries())
|
|
288
|
+
validateText(item, `${label}[${index}]`, maxChars, errors);
|
|
289
|
+
if (new Set(value).size !== value.length)
|
|
290
|
+
errors.push(`${label}_duplicate`);
|
|
291
|
+
}
|
|
292
|
+
function exactKeys(record, expected, label, errors) {
|
|
293
|
+
const expectedSet = new Set(expected);
|
|
294
|
+
for (const key of Object.keys(record)) {
|
|
295
|
+
if (!expectedSet.has(key))
|
|
296
|
+
errors.push(`${label}_unexpected_key:${key}`);
|
|
297
|
+
}
|
|
298
|
+
for (const key of expected) {
|
|
299
|
+
if (!(key in record))
|
|
300
|
+
errors.push(`${label}_missing_key:${key}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function unexpectedKeys(record, allowed, label, errors) {
|
|
304
|
+
const allowedSet = new Set(allowed);
|
|
305
|
+
for (const key of Object.keys(record)) {
|
|
306
|
+
if (!allowedSet.has(key))
|
|
307
|
+
errors.push(`${label}_unexpected_key:${key}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function boundedContractErrors(errors) {
|
|
311
|
+
const unique = [...new Set(errors)];
|
|
312
|
+
if (unique.length <= MAX_CONTRACT_ERRORS)
|
|
313
|
+
return unique;
|
|
314
|
+
return [...unique.slice(0, MAX_CONTRACT_ERRORS - 1), 'contract_error_limit_exceeded'];
|
|
315
|
+
}
|
|
316
|
+
function normalizedLength(value) {
|
|
317
|
+
return typeof value === 'string'
|
|
318
|
+
? value.replace(/[\r\n\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().length
|
|
319
|
+
: 0;
|
|
320
|
+
}
|
|
321
|
+
function normalizedText(value, max) {
|
|
322
|
+
return typeof value === 'string'
|
|
323
|
+
? value.replace(/[\r\n\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
324
|
+
: '';
|
|
325
|
+
}
|
|
326
|
+
function isRecord(value) {
|
|
327
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
328
|
+
}
|
|
329
|
+
function digest(value) {
|
|
330
|
+
return `sha256:${createHash('sha256').update(stableJson(value)).digest('hex')}`;
|
|
331
|
+
}
|
|
332
|
+
function stableJson(value) {
|
|
333
|
+
if (Array.isArray(value))
|
|
334
|
+
return `[${value.map((item) => stableJson(item)).join(',')}]`;
|
|
335
|
+
if (value && typeof value === 'object') {
|
|
336
|
+
const record = value;
|
|
337
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`;
|
|
338
|
+
}
|
|
339
|
+
return JSON.stringify(value);
|
|
340
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type ReleaseAuthMode = 'trusted_publisher_oidc' | 'manual_token';
|
|
2
|
+
export type RequestedReleaseAuthMode = 'auto' | 'oidc' | 'token';
|
|
3
|
+
export interface TrustedPublisherWorkflowCheck {
|
|
4
|
+
id: 'workflow_dispatch' | 'id_token_write' | 'cloud_hosted_runner' | 'npm_publish' | 'provenance' | 'official_registry';
|
|
5
|
+
passed: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface TrustedPublisherWorkflowInspection {
|
|
8
|
+
path: string;
|
|
9
|
+
exists: boolean;
|
|
10
|
+
configured: boolean;
|
|
11
|
+
checks: TrustedPublisherWorkflowCheck[];
|
|
12
|
+
missing_checks: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface LocalTokenProbe {
|
|
15
|
+
command: 'npm whoami';
|
|
16
|
+
ok: boolean;
|
|
17
|
+
note: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ReleaseAuthBlocker {
|
|
20
|
+
kind: 'oidc_workflow_incomplete' | 'trusted_publisher_account_unverified' | 'auth_not_verified';
|
|
21
|
+
severity: 'error';
|
|
22
|
+
reason: string;
|
|
23
|
+
safe_next_action: string;
|
|
24
|
+
command: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function inspectTrustedPublisherWorkflowText(text: string, workflowPath?: string, exists?: boolean): TrustedPublisherWorkflowInspection;
|
|
27
|
+
export declare function buildReleaseAuthReadiness(input: {
|
|
28
|
+
workflow: TrustedPublisherWorkflowInspection;
|
|
29
|
+
local_token: LocalTokenProbe;
|
|
30
|
+
requested_mode?: RequestedReleaseAuthMode;
|
|
31
|
+
}): {
|
|
32
|
+
schema: "yam.release-auth-readiness.v1";
|
|
33
|
+
requested_mode: "token" | "auto";
|
|
34
|
+
mode: "manual_token";
|
|
35
|
+
status: "authenticated" | "not_authenticated";
|
|
36
|
+
local_token: {
|
|
37
|
+
command: "npm whoami";
|
|
38
|
+
required_for_selected_mode: boolean;
|
|
39
|
+
status: "authenticated" | "not_authenticated";
|
|
40
|
+
account: string;
|
|
41
|
+
note: string;
|
|
42
|
+
truth_status: "verified";
|
|
43
|
+
};
|
|
44
|
+
trusted_publisher: {
|
|
45
|
+
provider: "github_actions";
|
|
46
|
+
workflow_path: string;
|
|
47
|
+
workflow_status: "not_configured";
|
|
48
|
+
workflow_checks: TrustedPublisherWorkflowCheck[];
|
|
49
|
+
missing_checks: string[];
|
|
50
|
+
account_status: "not_applicable";
|
|
51
|
+
note: string;
|
|
52
|
+
};
|
|
53
|
+
blockers: ReleaseAuthBlocker[];
|
|
54
|
+
next_action: string;
|
|
55
|
+
truth_status: "verified" | "blocked";
|
|
56
|
+
} | {
|
|
57
|
+
schema: "yam.release-auth-readiness.v1";
|
|
58
|
+
requested_mode: "auto" | "oidc";
|
|
59
|
+
mode: "trusted_publisher_oidc";
|
|
60
|
+
status: "workflow_configured_account_unverified" | "workflow_incomplete";
|
|
61
|
+
local_token: {
|
|
62
|
+
required_for_selected_mode: boolean;
|
|
63
|
+
note: string;
|
|
64
|
+
command: "npm whoami";
|
|
65
|
+
status: "authenticated" | "not_authenticated";
|
|
66
|
+
account: string;
|
|
67
|
+
truth_status: "verified";
|
|
68
|
+
};
|
|
69
|
+
trusted_publisher: {
|
|
70
|
+
provider: "github_actions";
|
|
71
|
+
workflow_path: string;
|
|
72
|
+
workflow_status: "incomplete" | "static_config_verified";
|
|
73
|
+
workflow_checks: TrustedPublisherWorkflowCheck[];
|
|
74
|
+
missing_checks: string[];
|
|
75
|
+
account_status: "not_checked" | "not_verified";
|
|
76
|
+
note: string;
|
|
77
|
+
};
|
|
78
|
+
blockers: ReleaseAuthBlocker[];
|
|
79
|
+
next_action: string;
|
|
80
|
+
truth_status: "blocked";
|
|
81
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export function inspectTrustedPublisherWorkflowText(text, workflowPath = '.github/workflows/release.yml', exists = Boolean(text)) {
|
|
2
|
+
const source = String(text || '');
|
|
3
|
+
const activeSource = source
|
|
4
|
+
.split(/\r?\n/)
|
|
5
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
6
|
+
.join('\n');
|
|
7
|
+
const checks = [
|
|
8
|
+
{ id: 'workflow_dispatch', passed: /(?:^|\n)\s*workflow_dispatch\s*:/m.test(activeSource) },
|
|
9
|
+
{ id: 'id_token_write', passed: /(?:^|\n)\s*id-token\s*:\s*write\s*(?:#.*)?$/m.test(activeSource) },
|
|
10
|
+
{ id: 'cloud_hosted_runner', passed: /(?:^|\n)\s*runs-on\s*:\s*(?:ubuntu|macos|windows)-latest\s*(?:#.*)?$/m.test(activeSource) },
|
|
11
|
+
{ id: 'npm_publish', passed: /(?:^|\n)\s*(?:-\s*)?(?:run:\s*(?:>-?\s*)?)?npm\s+publish\b/m.test(activeSource) },
|
|
12
|
+
{ id: 'provenance', passed: /(?:^|\s)--provenance(?:\s|$)/m.test(activeSource) },
|
|
13
|
+
{ id: 'official_registry', passed: /--registry\s+https:\/\/registry\.npmjs\.org\/?(?:\s|$)/m.test(activeSource) }
|
|
14
|
+
];
|
|
15
|
+
const missingChecks = checks.filter((check) => !check.passed).map((check) => check.id);
|
|
16
|
+
return {
|
|
17
|
+
path: workflowPath,
|
|
18
|
+
exists,
|
|
19
|
+
configured: exists && missingChecks.length === 0,
|
|
20
|
+
checks,
|
|
21
|
+
missing_checks: missingChecks
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function buildReleaseAuthReadiness(input) {
|
|
25
|
+
const workflow = input.workflow;
|
|
26
|
+
const requestedMode = input.requested_mode || 'auto';
|
|
27
|
+
const oidcSelected = requestedMode === 'oidc' || (requestedMode === 'auto' && workflow.exists);
|
|
28
|
+
const localToken = {
|
|
29
|
+
command: 'npm whoami',
|
|
30
|
+
required_for_selected_mode: !oidcSelected,
|
|
31
|
+
status: input.local_token.ok ? 'authenticated' : 'not_authenticated',
|
|
32
|
+
account: input.local_token.ok ? 'observed_redacted' : '',
|
|
33
|
+
note: input.local_token.ok
|
|
34
|
+
? 'npm account observed for the optional manual-token path; username intentionally redacted'
|
|
35
|
+
: input.local_token.note,
|
|
36
|
+
truth_status: 'verified'
|
|
37
|
+
};
|
|
38
|
+
if (!oidcSelected) {
|
|
39
|
+
const blockers = input.local_token.ok ? [] : [{
|
|
40
|
+
kind: 'auth_not_verified',
|
|
41
|
+
severity: 'error',
|
|
42
|
+
reason: input.local_token.note || 'npm whoami did not confirm an authenticated publisher for the selected manual-token path',
|
|
43
|
+
safe_next_action: 'refresh npm login/token, then rerun `npm whoami` before manual-token publishing',
|
|
44
|
+
command: 'npm whoami'
|
|
45
|
+
}];
|
|
46
|
+
return {
|
|
47
|
+
schema: 'yam.release-auth-readiness.v1',
|
|
48
|
+
requested_mode: requestedMode,
|
|
49
|
+
mode: 'manual_token',
|
|
50
|
+
status: input.local_token.ok ? 'authenticated' : 'not_authenticated',
|
|
51
|
+
local_token: localToken,
|
|
52
|
+
trusted_publisher: {
|
|
53
|
+
provider: 'github_actions',
|
|
54
|
+
workflow_path: workflow.path,
|
|
55
|
+
workflow_status: 'not_configured',
|
|
56
|
+
workflow_checks: workflow.checks,
|
|
57
|
+
missing_checks: workflow.missing_checks,
|
|
58
|
+
account_status: 'not_applicable',
|
|
59
|
+
note: 'No trusted-publisher release workflow was detected; manual-token authentication remains selected.'
|
|
60
|
+
},
|
|
61
|
+
blockers,
|
|
62
|
+
next_action: blockers[0]?.safe_next_action || 'manual-token authentication is verified; publishing still requires explicit user intent',
|
|
63
|
+
truth_status: blockers.length ? 'blocked' : 'verified'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const blockers = workflow.configured ? [{
|
|
67
|
+
kind: 'trusted_publisher_account_unverified',
|
|
68
|
+
severity: 'error',
|
|
69
|
+
reason: 'The local report cannot verify the npmjs.com account-side Trusted Publisher binding; npm whoami does not reflect OIDC authentication status.',
|
|
70
|
+
safe_next_action: `confirm the package Trusted Publisher repository and workflow filename match ${workflow.path}, then use the hosted workflow result as authentication evidence`,
|
|
71
|
+
command: 'npmjs.com package settings'
|
|
72
|
+
}] : [{
|
|
73
|
+
kind: 'oidc_workflow_incomplete',
|
|
74
|
+
severity: 'error',
|
|
75
|
+
reason: `The trusted-publisher workflow is missing required controls: ${workflow.missing_checks.join(', ')}`,
|
|
76
|
+
safe_next_action: `repair ${workflow.path}, then rerun workflow pins, actionlint, and release auth smoke before publishing`,
|
|
77
|
+
command: 'npm run release:auth-smoke'
|
|
78
|
+
}];
|
|
79
|
+
return {
|
|
80
|
+
schema: 'yam.release-auth-readiness.v1',
|
|
81
|
+
requested_mode: requestedMode,
|
|
82
|
+
mode: 'trusted_publisher_oidc',
|
|
83
|
+
status: workflow.configured ? 'workflow_configured_account_unverified' : 'workflow_incomplete',
|
|
84
|
+
local_token: {
|
|
85
|
+
...localToken,
|
|
86
|
+
required_for_selected_mode: false,
|
|
87
|
+
note: input.local_token.ok
|
|
88
|
+
? `${localToken.note}; npm whoami does not prove OIDC readiness`
|
|
89
|
+
: `${localToken.note || 'no local npm token was observed'}; this does not block the selected OIDC path`
|
|
90
|
+
},
|
|
91
|
+
trusted_publisher: {
|
|
92
|
+
provider: 'github_actions',
|
|
93
|
+
workflow_path: workflow.path,
|
|
94
|
+
workflow_status: workflow.configured ? 'static_config_verified' : 'incomplete',
|
|
95
|
+
workflow_checks: workflow.checks,
|
|
96
|
+
missing_checks: workflow.missing_checks,
|
|
97
|
+
account_status: workflow.configured ? 'not_verified' : 'not_checked',
|
|
98
|
+
note: workflow.configured
|
|
99
|
+
? 'Local static checks cover the workflow only; npmjs.com account configuration and the publish-time OIDC exchange remain unverified.'
|
|
100
|
+
: 'The workflow must satisfy every required control before account-side Trusted Publisher evidence is relevant.'
|
|
101
|
+
},
|
|
102
|
+
blockers,
|
|
103
|
+
next_action: blockers[0].safe_next_action,
|
|
104
|
+
truth_status: 'blocked'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ReleaseRegistryCheck = {
|
|
2
|
+
id?: unknown;
|
|
3
|
+
status?: unknown;
|
|
4
|
+
note?: unknown;
|
|
5
|
+
};
|
|
6
|
+
export type ReleaseRegistryStatus = {
|
|
7
|
+
checked: boolean;
|
|
8
|
+
latest_version: string;
|
|
9
|
+
not_published: boolean;
|
|
10
|
+
note: string;
|
|
11
|
+
parse_failed: boolean;
|
|
12
|
+
queried_package: string;
|
|
13
|
+
queried_version: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function releaseRegistryStatusFromChecks(checks?: ReleaseRegistryCheck[], expected?: {
|
|
16
|
+
package_name?: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
}): ReleaseRegistryStatus;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function releaseRegistryStatusFromChecks(checks = [], expected = {}) {
|
|
2
|
+
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
3
|
+
const note = String(registryCheck?.note || '');
|
|
4
|
+
const base = {
|
|
5
|
+
checked: false,
|
|
6
|
+
latest_version: '',
|
|
7
|
+
not_published: false,
|
|
8
|
+
note,
|
|
9
|
+
parse_failed: false,
|
|
10
|
+
queried_package: '',
|
|
11
|
+
queried_version: ''
|
|
12
|
+
};
|
|
13
|
+
if (registryCheck?.status !== 'passed')
|
|
14
|
+
return base;
|
|
15
|
+
const match = note.match(/registry:check:\s*ok\s*\((.+)@([^@\s()]+)\s+is not published\)/i);
|
|
16
|
+
if (!match)
|
|
17
|
+
return { ...base, parse_failed: true };
|
|
18
|
+
const queriedPackage = String(match[1] || '').trim();
|
|
19
|
+
const queriedVersion = String(match[2] || '').trim();
|
|
20
|
+
const expectedPackage = String(expected.package_name || '').trim();
|
|
21
|
+
const expectedVersion = String(expected.version || '').trim();
|
|
22
|
+
if ((expectedPackage && queriedPackage !== expectedPackage) || (expectedVersion && queriedVersion !== expectedVersion)) {
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
parse_failed: true,
|
|
26
|
+
queried_package: queriedPackage,
|
|
27
|
+
queried_version: queriedVersion
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
checked: true,
|
|
33
|
+
not_published: true,
|
|
34
|
+
queried_package: queriedPackage,
|
|
35
|
+
queried_version: queriedVersion
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
declare const FAILURE_KINDS: readonly ["not_measured", "no_results", "blocked_waf", "rate_limited", "auth_required", "paywall", "network_failed", "empty_content"];
|
|
2
|
+
type FailureKind = typeof FAILURE_KINDS[number];
|
|
3
|
+
export interface ScoutReceiptSpec {
|
|
4
|
+
subject: {
|
|
5
|
+
canonical_name: string;
|
|
6
|
+
aliases: string[];
|
|
7
|
+
canonical_url: string;
|
|
8
|
+
};
|
|
9
|
+
clocks: {
|
|
10
|
+
registry_latest: string;
|
|
11
|
+
release_tag: string;
|
|
12
|
+
main_version: string;
|
|
13
|
+
latest_commit: string;
|
|
14
|
+
stability: 'stable' | 'release_candidate' | 'unreleased' | 'mixed' | 'unknown';
|
|
15
|
+
};
|
|
16
|
+
sources: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
canonical_url: string;
|
|
19
|
+
access_path: string;
|
|
20
|
+
source_class: 'official' | 'implementation' | 'third_party' | 'community' | 'contrarian' | 'local';
|
|
21
|
+
retrieved_at: string;
|
|
22
|
+
version: string;
|
|
23
|
+
revision: string;
|
|
24
|
+
content_digest: string;
|
|
25
|
+
authority: 'high' | 'medium' | 'low';
|
|
26
|
+
freshness: 'high' | 'medium' | 'low';
|
|
27
|
+
directness: 'high' | 'medium' | 'low';
|
|
28
|
+
}>;
|
|
29
|
+
claims: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
text: string;
|
|
32
|
+
source_ids: string[];
|
|
33
|
+
confidence: 'high' | 'medium' | 'low';
|
|
34
|
+
uncertainty: string;
|
|
35
|
+
decision_impact: string;
|
|
36
|
+
}>;
|
|
37
|
+
acquisition_failures: Array<{
|
|
38
|
+
source_id: string;
|
|
39
|
+
kind: FailureKind;
|
|
40
|
+
note: string;
|
|
41
|
+
}>;
|
|
42
|
+
opposition: string;
|
|
43
|
+
recommendation: string;
|
|
44
|
+
stop_reason: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ScoutReceipt extends ScoutReceiptSpec {
|
|
47
|
+
schema: 'yam.scout-receipt.v1';
|
|
48
|
+
created_at: string;
|
|
49
|
+
evidence_boundary: {
|
|
50
|
+
acquisition_by_yam: false;
|
|
51
|
+
source_interpretation: 'operator_supplied';
|
|
52
|
+
external_content_trusted_as_instruction: false;
|
|
53
|
+
};
|
|
54
|
+
truth_status: 'partial';
|
|
55
|
+
digest: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function createScoutReceipt(input: {
|
|
58
|
+
root: string;
|
|
59
|
+
receipt_path: string;
|
|
60
|
+
spec: ScoutReceiptSpec;
|
|
61
|
+
now?: () => Date;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
schema: "yam.scout-receipt-write.v1";
|
|
64
|
+
receipt_path: string;
|
|
65
|
+
receipt: ScoutReceipt;
|
|
66
|
+
truth_status: "partial";
|
|
67
|
+
next_action: string;
|
|
68
|
+
}>;
|
|
69
|
+
export declare function verifyScoutReceipt(value: unknown): {
|
|
70
|
+
schema: "yam.scout-receipt-verification.v1";
|
|
71
|
+
valid: boolean;
|
|
72
|
+
errors: string[];
|
|
73
|
+
receipt_truth_status: any;
|
|
74
|
+
truth_status: "verified" | "blocked";
|
|
75
|
+
next_action: string;
|
|
76
|
+
};
|
|
77
|
+
export declare function verifyScoutReceiptFile(input: {
|
|
78
|
+
root: string;
|
|
79
|
+
receipt_path: string;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
receipt_path: string;
|
|
82
|
+
schema: "yam.scout-receipt-verification.v1";
|
|
83
|
+
valid: boolean;
|
|
84
|
+
errors: string[];
|
|
85
|
+
receipt_truth_status: any;
|
|
86
|
+
truth_status: "verified" | "blocked";
|
|
87
|
+
next_action: string;
|
|
88
|
+
}>;
|
|
89
|
+
export {};
|