yam-flow 2.5.1 → 2.7.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.
@@ -70,6 +70,20 @@ export interface ExternalComponentReceipt {
70
70
  persistence?: 'written' | 'failed';
71
71
  receipt_path?: string;
72
72
  receipt_error?: string;
73
+ install_identity?: YamInstallIdentity;
74
+ rollback_identity?: YamInstallIdentity;
75
+ }
76
+ export interface YamInstallIdentity {
77
+ expected_version: string;
78
+ observed_version: string;
79
+ first_on_path: string;
80
+ canonical_executable: string;
81
+ npm_global_root: string;
82
+ canonical_npm_global_root: string;
83
+ package_root: string;
84
+ package_manifest: string;
85
+ package_entrypoint: string;
86
+ canonical_entrypoint: string;
73
87
  }
74
88
  export declare function checkExternalUpdates(currentYamVersion: string, dependencies?: ExternalUpdateDependencies): Promise<{
75
89
  schema: string;
@@ -94,8 +108,17 @@ export declare function applyExternalUpdates(currentYamVersion: string, selectio
94
108
  stopped_early: boolean;
95
109
  lock: {
96
110
  path: string;
111
+ acquired: boolean;
97
112
  released: boolean;
98
113
  };
114
+ gate_result: import("./gate-result.js").StrictGateResult;
115
+ gate_contract: {
116
+ schema: "yam.gate-result-verification.v1";
117
+ valid: boolean;
118
+ errors: string[];
119
+ truth_status: "verified" | "blocked";
120
+ next_action: string;
121
+ };
99
122
  success: boolean;
100
123
  truth_status: string;
101
124
  next_action: string;
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import fsp from 'node:fs/promises';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { buildStrictGateResult, verifyStrictGateResult } from './gate-result.js';
6
7
  export const EXTERNAL_UPDATE_COMPONENTS = ['yam', 'scrapling', 'insane-search'];
7
8
  const YAM_REGISTRY_URL = 'https://registry.npmjs.org/yam-flow/latest';
8
9
  const SCRAPLING_PYPI_URL = 'https://pypi.org/pypi/scrapling/json';
@@ -50,6 +51,45 @@ export async function applyExternalUpdates(currentYamVersion, selection, depende
50
51
  const receipts = [];
51
52
  try {
52
53
  lockHandle = await acquireApplyLock(deps);
54
+ }
55
+ catch (error) {
56
+ const lockError = boundedNote(errorMessage(error));
57
+ const nextAction = `confirm no updater is running, inspect the existing lock, then remove it manually only if stale: ${deps.paths.lockFile}`;
58
+ const gateResult = buildStrictGateResult({
59
+ gate_id: 'external-update-apply',
60
+ boundary: 'update',
61
+ checks: [{
62
+ id: 'update-lock-acquisition',
63
+ status: 'failed',
64
+ note: lockError,
65
+ required: true
66
+ }],
67
+ blockers: [`update_lock_not_acquired: ${lockError}`],
68
+ evidence: [`requested-components:${components.join(',')}`, `lock-path:${deps.paths.lockFile}`],
69
+ next_action: nextAction
70
+ });
71
+ const gateContract = verifyStrictGateResult(gateResult);
72
+ return {
73
+ schema: 'yam.external-update-apply.v1',
74
+ generated_at: deps.now().toISOString(),
75
+ mutation_authorized: true,
76
+ requested_components: components,
77
+ applied_components: [],
78
+ receipts: [],
79
+ stopped_early: true,
80
+ lock: {
81
+ path: deps.paths.lockFile,
82
+ acquired: false,
83
+ released: false
84
+ },
85
+ gate_result: gateResult,
86
+ gate_contract: gateContract,
87
+ success: false,
88
+ truth_status: 'blocked',
89
+ next_action: nextAction
90
+ };
91
+ }
92
+ try {
53
93
  for (const component of components) {
54
94
  const receipt = component === 'yam'
55
95
  ? await applyYam(currentYamVersion, deps)
@@ -84,6 +124,53 @@ export async function applyExternalUpdates(currentYamVersion, selection, depende
84
124
  || receipt.outcome === 'manual_plugin_update_required'
85
125
  || receipt.persistence === 'failed'));
86
126
  const incomplete = receipts.length !== components.length;
127
+ const operationSuccess = !failed && !incomplete && lockReleased;
128
+ const gateBlockers = receipts.flatMap((receipt) => receipt.checks
129
+ .filter((check) => check.status === 'failed')
130
+ .map((check) => `${receipt.component}.${check.id}: ${check.note}`));
131
+ for (const receipt of receipts) {
132
+ if (receipt.persistence === 'failed')
133
+ gateBlockers.push(`${receipt.component}.receipt_persistence: ${receipt.receipt_error || 'receipt was not persisted'}`);
134
+ if (receipt.outcome === 'manual_plugin_update_required')
135
+ gateBlockers.push(`${receipt.component}.manual_update_required: ${receipt.error || 'safe in-place plugin update unavailable'}`);
136
+ }
137
+ if (incomplete)
138
+ gateBlockers.push(`component_inventory_incomplete: applied ${receipts.length} of ${components.length}`);
139
+ if (!lockReleased)
140
+ gateBlockers.push(`update_lock_not_released: ${deps.paths.lockFile}`);
141
+ const gateResult = buildStrictGateResult({
142
+ gate_id: 'external-update-apply',
143
+ boundary: 'update',
144
+ checks: [
145
+ {
146
+ id: 'component-inventory',
147
+ status: incomplete ? 'failed' : 'passed',
148
+ note: incomplete ? `applied ${receipts.length} of ${components.length} requested components` : 'every requested component produced one outcome receipt',
149
+ required: true
150
+ },
151
+ {
152
+ id: 'component-outcomes',
153
+ status: failed ? 'failed' : 'passed',
154
+ note: failed ? firstFailureNextAction(receipts) : 'all component outcomes passed',
155
+ required: true
156
+ },
157
+ {
158
+ id: 'update-lock-release',
159
+ status: lockReleased ? 'passed' : 'failed',
160
+ note: lockReleased ? 'external update lock was removed' : `inspect the stale lock: ${deps.paths.lockFile}`,
161
+ required: true
162
+ }
163
+ ],
164
+ blockers: gateBlockers,
165
+ evidence: receipts.map((receipt) => `${receipt.component}:${receipt.outcome}:${receipt.persistence || 'not-persisted'}:${receipt.receipt_path || 'no-path'}`),
166
+ next_action: failed
167
+ ? firstFailureNextAction(receipts)
168
+ : !lockReleased
169
+ ? `confirm no updater is running, inspect the stale lock, then remove it manually: ${deps.paths.lockFile}`
170
+ : 'review the component receipts and restart Codex only if yam skills or a Codex plugin changed'
171
+ });
172
+ const gateContract = verifyStrictGateResult(gateResult);
173
+ const success = operationSuccess && gateResult.status === 'passed' && gateContract.valid;
87
174
  return {
88
175
  schema: 'yam.external-update-apply.v1',
89
176
  generated_at: deps.now().toISOString(),
@@ -94,10 +181,13 @@ export async function applyExternalUpdates(currentYamVersion, selection, depende
94
181
  stopped_early: incomplete,
95
182
  lock: {
96
183
  path: deps.paths.lockFile,
184
+ acquired: true,
97
185
  released: lockReleased
98
186
  },
99
- success: !failed && !incomplete && lockReleased,
100
- truth_status: failed || incomplete || !lockReleased ? 'blocked' : 'verified',
187
+ gate_result: gateResult,
188
+ gate_contract: gateContract,
189
+ success,
190
+ truth_status: success ? 'verified' : 'blocked',
101
191
  next_action: failed
102
192
  ? firstFailureNextAction(receipts)
103
193
  : !lockReleased
@@ -340,22 +430,32 @@ async function applyYam(currentYamVersion, deps) {
340
430
  }), deps);
341
431
  }
342
432
  sideEffects.push(`globally installed ${exactPackage}`);
433
+ let installIdentity;
434
+ let rollbackIdentity;
435
+ const identityValidation = await validateEffectiveYamIdentity(check.latest_version, deps, 'yam_effective_identity');
436
+ checks.push(identityValidation.check);
437
+ if (identityValidation.passed)
438
+ installIdentity = identityValidation.identity;
343
439
  const postCommands = [
344
440
  ['yam_install_skills', ['install']],
345
441
  ['yam_version', ['version']],
346
442
  ['yam_status', ['status']]
347
443
  ];
348
- let postFailure = '';
349
- for (const [id, args] of postCommands) {
350
- const result = await deps.run('yam', args, { env: deps.env, timeout: 180000 });
351
- checks.push(commandCheck(id, result));
352
- if (!result.ok || (id === 'yam_version' && extractVersion(result.stdout) !== check.latest_version)) {
353
- postFailure = `${id} failed: ${commandNote(result)}`;
354
- break;
444
+ let postFailure = identityValidation.passed
445
+ ? ''
446
+ : `yam_effective_identity failed: ${identityValidation.error}`;
447
+ if (!postFailure && installIdentity) {
448
+ for (const [id, args] of postCommands) {
449
+ const result = await deps.run(installIdentity.first_on_path, args, { env: deps.env, timeout: 180000 });
450
+ checks.push(commandCheck(id, result));
451
+ if (!result.ok || (id === 'yam_version' && extractVersion(result.stdout) !== check.latest_version)) {
452
+ postFailure = `${id} failed: ${commandNote(result)}`;
453
+ break;
454
+ }
355
455
  }
356
456
  }
357
- if (!postFailure) {
358
- const doctor = await deps.run('yam', ['doctor', '--json'], { env: deps.env, timeout: 180000 });
457
+ if (!postFailure && installIdentity) {
458
+ const doctor = await deps.run(installIdentity.first_on_path, ['doctor', '--json'], { env: deps.env, timeout: 180000 });
359
459
  const validation = doctorValidation('yam_finalize_doctor', doctor);
360
460
  checks.push(validation.check);
361
461
  if (!validation.passed)
@@ -363,53 +463,170 @@ async function applyYam(currentYamVersion, deps) {
363
463
  }
364
464
  if (postFailure) {
365
465
  const rollback = await rollbackYam(check.installed_version, deps, checks);
466
+ rollbackIdentity = rollback.identity;
366
467
  return persistReceipt(baseReceipt(check, 'failed', checks, {
367
468
  ...yamRollback(check.installed_version),
368
- automatic: rollback
469
+ automatic: rollback.ok
369
470
  }, {
471
+ ...(installIdentity ? { install_identity: installIdentity } : {}),
472
+ ...(rollbackIdentity ? { rollback_identity: rollbackIdentity } : {}),
370
473
  side_effects: sideEffects,
371
474
  error: postFailure
372
475
  }), deps);
373
476
  }
374
477
  const receipt = baseReceipt(check, 'updated', checks, yamRollback(check.installed_version), {
375
478
  installed_version: check.latest_version,
479
+ ...(installIdentity ? { install_identity: installIdentity } : {}),
376
480
  side_effects: sideEffects,
377
481
  truth_status: 'verified'
378
482
  });
379
483
  const persisted = await persistReceipt(receipt, deps);
380
484
  if (persisted.persistence === 'failed') {
381
485
  const rollback = await rollbackYam(check.installed_version, deps, persisted.checks);
382
- persisted.rollback_hint.automatic = rollback;
486
+ persisted.rollback_hint.automatic = rollback.ok;
487
+ if (rollback.identity)
488
+ persisted.rollback_identity = rollback.identity;
383
489
  persisted.outcome = 'failed';
384
490
  persisted.truth_status = 'blocked';
385
- persisted.error = `update completed but receipt persistence failed; automatic rollback ${rollback ? 'passed' : 'failed'}`;
491
+ persisted.error = `update completed but receipt persistence failed; automatic rollback ${rollback.ok ? 'passed' : 'failed'}`;
386
492
  }
387
493
  return persisted;
388
494
  }
389
495
  async function rollbackYam(previousVersion, deps, checks) {
390
496
  if (!previousVersion)
391
- return false;
497
+ return { ok: false };
392
498
  const result = await deps.run('npm', ['install', '-g', `yam-flow@${previousVersion}`], {
393
499
  env: deps.env,
394
500
  timeout: 600000
395
501
  });
396
502
  checks.push(commandCheck('automatic_yam_rollback', result));
397
503
  if (!result.ok)
398
- return false;
399
- const install = await deps.run('yam', ['install'], { env: deps.env, timeout: 180000 });
400
- const version = await deps.run('yam', ['version'], { env: deps.env, timeout: 30000 });
401
- const status = await deps.run('yam', ['status'], { env: deps.env, timeout: 120000 });
402
- const doctor = await deps.run('yam', ['doctor', '--json'], { env: deps.env, timeout: 180000 });
504
+ return { ok: false };
505
+ const identityValidation = await validateEffectiveYamIdentity(previousVersion, deps, 'automatic_yam_rollback_identity');
506
+ checks.push(identityValidation.check);
507
+ if (!identityValidation.passed || !identityValidation.identity)
508
+ return { ok: false };
509
+ const executable = identityValidation.identity.first_on_path;
510
+ const install = await deps.run(executable, ['install'], { env: deps.env, timeout: 180000 });
511
+ const version = await deps.run(executable, ['version'], { env: deps.env, timeout: 30000 });
512
+ const status = await deps.run(executable, ['status'], { env: deps.env, timeout: 120000 });
513
+ const doctor = await deps.run(executable, ['doctor', '--json'], { env: deps.env, timeout: 180000 });
403
514
  const doctorResult = doctorValidation('automatic_yam_rollback_doctor', doctor);
404
515
  checks.push(commandCheck('automatic_yam_rollback_skills', install));
405
516
  checks.push(commandCheck('automatic_yam_rollback_version', version));
406
517
  checks.push(commandCheck('automatic_yam_rollback_status', status));
407
518
  checks.push(doctorResult.check);
408
- return (install.ok
409
- && version.ok
410
- && extractVersion(version.stdout) === previousVersion
411
- && status.ok
412
- && doctorResult.passed);
519
+ return {
520
+ ok: (install.ok
521
+ && version.ok
522
+ && extractVersion(version.stdout) === previousVersion
523
+ && status.ok
524
+ && doctorResult.passed),
525
+ identity: identityValidation.identity
526
+ };
527
+ }
528
+ async function validateEffectiveYamIdentity(expectedVersion, deps, id) {
529
+ try {
530
+ const npmRootResult = await deps.run('npm', ['root', '-g'], { env: deps.env, timeout: 30000 });
531
+ if (!npmRootResult.ok) {
532
+ throw new Error(`npm global root lookup failed: ${commandNote(npmRootResult)}`);
533
+ }
534
+ const npmGlobalRoot = firstOutputLine(npmRootResult.stdout);
535
+ if (!npmGlobalRoot || !path.isAbsolute(npmGlobalRoot)) {
536
+ throw new Error('npm global root lookup did not return one absolute path');
537
+ }
538
+ const packageRoot = path.join(path.resolve(npmGlobalRoot), 'yam-flow');
539
+ const packageManifest = path.join(packageRoot, 'package.json');
540
+ let manifest;
541
+ try {
542
+ const parsed = JSON.parse(await fsp.readFile(packageManifest, 'utf8'));
543
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
544
+ throw new Error('not an object');
545
+ manifest = parsed;
546
+ }
547
+ catch {
548
+ throw new Error(`yam-flow package manifest is missing or malformed: ${packageManifest}`);
549
+ }
550
+ if (manifest.name !== 'yam-flow') {
551
+ throw new Error(`npm global package manifest has unexpected name at ${packageManifest}`);
552
+ }
553
+ const manifestVersion = requireStableVersion(manifest.version, 'yam-flow package manifest version');
554
+ if (manifestVersion !== expectedVersion) {
555
+ throw new Error(`yam-flow package manifest version ${manifestVersion} does not match expected ${expectedVersion}`);
556
+ }
557
+ const binValue = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.yam;
558
+ if (typeof binValue !== 'string' || !binValue.trim()) {
559
+ throw new Error(`yam-flow package manifest has no usable yam bin entry: ${packageManifest}`);
560
+ }
561
+ const packageEntrypoint = path.resolve(packageRoot, binValue);
562
+ assertChildPath(packageRoot, packageEntrypoint, 'yam-flow package entrypoint');
563
+ const whichResult = await deps.run('which', ['yam'], { env: deps.env, timeout: 10000 });
564
+ if (!whichResult.ok) {
565
+ throw new Error(`first yam on PATH could not be resolved: ${commandNote(whichResult)}`);
566
+ }
567
+ const firstOnPath = firstOutputLine(whichResult.stdout);
568
+ if (!firstOnPath || !path.isAbsolute(firstOnPath)) {
569
+ throw new Error('first yam on PATH was not reported as one absolute path');
570
+ }
571
+ let canonicalNpmGlobalRoot;
572
+ let canonicalPackageRoot;
573
+ let canonicalEntrypoint;
574
+ let canonicalExecutable;
575
+ try {
576
+ [canonicalNpmGlobalRoot, canonicalPackageRoot, canonicalEntrypoint, canonicalExecutable] = await Promise.all([
577
+ fsp.realpath(npmGlobalRoot),
578
+ fsp.realpath(packageRoot),
579
+ fsp.realpath(packageEntrypoint),
580
+ fsp.realpath(firstOnPath)
581
+ ]);
582
+ }
583
+ catch {
584
+ throw new Error('yam install identity contains an unreadable package path, entrypoint, or PATH executable');
585
+ }
586
+ assertChildPath(canonicalNpmGlobalRoot, canonicalPackageRoot, 'canonical yam-flow package root');
587
+ assertChildPath(canonicalPackageRoot, canonicalEntrypoint, 'canonical yam-flow package entrypoint');
588
+ if (canonicalExecutable !== canonicalEntrypoint) {
589
+ throw new Error(`first yam on PATH is shadowed: ${firstOnPath} resolves to ${canonicalExecutable}, expected ${canonicalEntrypoint}; reorder PATH or remove the stale shim before retrying`);
590
+ }
591
+ const versionResult = await deps.run(firstOnPath, ['version'], { env: deps.env, timeout: 30000 });
592
+ if (!versionResult.ok) {
593
+ throw new Error(`effective yam version check failed: ${commandNote(versionResult)}`);
594
+ }
595
+ const observedVersion = requireStableVersion(extractVersion(versionResult.stdout), 'effective yam version');
596
+ if (observedVersion !== expectedVersion) {
597
+ throw new Error(`effective yam version ${observedVersion} does not match expected ${expectedVersion}`);
598
+ }
599
+ const identity = {
600
+ expected_version: expectedVersion,
601
+ observed_version: observedVersion,
602
+ first_on_path: firstOnPath,
603
+ canonical_executable: canonicalExecutable,
604
+ npm_global_root: path.resolve(npmGlobalRoot),
605
+ canonical_npm_global_root: canonicalNpmGlobalRoot,
606
+ package_root: packageRoot,
607
+ package_manifest: packageManifest,
608
+ package_entrypoint: packageEntrypoint,
609
+ canonical_entrypoint: canonicalEntrypoint
610
+ };
611
+ return {
612
+ passed: true,
613
+ error: '',
614
+ check: {
615
+ id,
616
+ status: 'passed',
617
+ note: boundedNote(`verified yam install identity: ${JSON.stringify(identity)}`)
618
+ },
619
+ identity
620
+ };
621
+ }
622
+ catch (error) {
623
+ const note = boundedNote(errorMessage(error));
624
+ return {
625
+ passed: false,
626
+ error: note,
627
+ check: { id, status: 'failed', note }
628
+ };
629
+ }
413
630
  }
414
631
  async function applyScrapling(deps) {
415
632
  const check = await checkScrapling(deps);
@@ -906,7 +1123,7 @@ function doctorValidation(id, result) {
906
1123
  };
907
1124
  }
908
1125
  if (report.ok !== true) {
909
- const note = 'yam.doctor.v1 reported ok=false';
1126
+ const note = doctorFailureDiagnostic(report);
910
1127
  return {
911
1128
  passed: false,
912
1129
  error: note,
@@ -920,6 +1137,30 @@ function doctorValidation(id, result) {
920
1137
  check: { id, status: 'passed', note }
921
1138
  };
922
1139
  }
1140
+ function doctorFailureDiagnostic(report) {
1141
+ const detail = arrayValue(report.nextActionDetails).find((item) => (item
1142
+ && typeof item === 'object'
1143
+ && !Array.isArray(item)
1144
+ && [item.reason, item.next_action, item.nextAction, item.command].some((value) => cleanDiagnosticValue(value))));
1145
+ const issue = cleanDiagnosticValue(detail?.reason) || firstDiagnosticValue(report.issues);
1146
+ const nextAction = cleanDiagnosticValue(detail?.next_action || detail?.nextAction)
1147
+ || firstDiagnosticValue(report.nextActions);
1148
+ const command = cleanDiagnosticValue(detail?.command);
1149
+ const fragments = [
1150
+ issue ? `issue: ${issue}` : '',
1151
+ nextAction ? `next: ${nextAction}` : '',
1152
+ command ? `command: ${command}` : ''
1153
+ ].filter(Boolean);
1154
+ return boundedNote(fragments.length
1155
+ ? `yam.doctor.v1 reported ok=false; ${fragments.join('; ')}`
1156
+ : 'yam.doctor.v1 reported ok=false');
1157
+ }
1158
+ function firstDiagnosticValue(value) {
1159
+ return arrayValue(value).map(cleanDiagnosticValue).find(Boolean) || '';
1160
+ }
1161
+ function cleanDiagnosticValue(value) {
1162
+ return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
1163
+ }
923
1164
  function boundedNote(text) {
924
1165
  return redactSensitiveText(String(text || '')).slice(0, 800);
925
1166
  }
@@ -966,6 +1207,9 @@ function requireStableVersion(value, label) {
966
1207
  function extractVersion(text) {
967
1208
  return String(text || '').match(/\b\d+(?:\.\d+){1,3}\b/)?.[0] || '';
968
1209
  }
1210
+ function firstOutputLine(text) {
1211
+ return String(text || '').split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
1212
+ }
969
1213
  function compareVersions(left, right) {
970
1214
  const a = left.split('.').map(Number);
971
1215
  const b = right.split('.').map(Number);
@@ -1037,6 +1281,7 @@ async function unlinkIfExists(file) {
1037
1281
  function redactSensitiveText(text) {
1038
1282
  return String(text || '')
1039
1283
  .replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
1284
+ .replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY))\s*[:=]\s*\S+/gi, '$1=[redacted]')
1040
1285
  .replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
1041
1286
  .replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
1042
1287
  .replace(/\b(npm_[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[redacted-token]');
@@ -1047,8 +1292,19 @@ function firstFailureNextAction(receipts) {
1047
1292
  || item.persistence === 'failed'));
1048
1293
  if (!receipt)
1049
1294
  return 'inspect the incomplete component inventory before retrying';
1050
- if (receipt.outcome === 'manual_plugin_update_required') {
1051
- return 'update Insane Search through a reviewed official Codex plugin workflow; do not remove-first or edit the plugin cache directly';
1052
- }
1053
- return receipt.rollback_hint.guidance || `resolve the ${receipt.component} failure before retrying`;
1295
+ const deepestFailedCheck = [...receipt.checks].reverse().find((check) => check.status === 'failed');
1296
+ const deepestCause = deepestFailedCheck
1297
+ ? `${deepestFailedCheck.id} failed: ${deepestFailedCheck.note}`
1298
+ : receipt.persistence === 'failed' && receipt.receipt_error
1299
+ ? `receipt persistence failed: ${receipt.receipt_error}`
1300
+ : receipt.error || `${receipt.component} update failed`;
1301
+ const persistenceContext = deepestFailedCheck && receipt.persistence === 'failed' && receipt.receipt_error
1302
+ ? `; secondary receipt persistence failure: ${receipt.receipt_error}`
1303
+ : '';
1304
+ const cause = boundedNote(`${deepestCause}${persistenceContext}`).slice(0, 420);
1305
+ const recoveryGuidance = receipt.outcome === 'manual_plugin_update_required'
1306
+ ? 'Update Insane Search through a reviewed official Codex plugin workflow; do not remove-first or edit the plugin cache directly.'
1307
+ : receipt.rollback_hint.guidance || `resolve the ${receipt.component} failure before retrying`;
1308
+ const recovery = boundedNote(recoveryGuidance).slice(0, 340);
1309
+ return boundedNote(`resolve ${receipt.component} failure: ${cause}. ${recovery}`);
1054
1310
  }
@@ -0,0 +1,44 @@
1
+ export type GateBoundary = 'release' | 'update' | 'mission' | 'generic';
2
+ export type GateCheckStatus = 'passed' | 'failed' | 'skipped';
3
+ export interface GateCheckInput {
4
+ id?: string;
5
+ status?: GateCheckStatus;
6
+ note?: string;
7
+ required?: boolean;
8
+ }
9
+ export interface StrictGateInput {
10
+ gate_id?: string;
11
+ boundary?: GateBoundary;
12
+ checks?: GateCheckInput[];
13
+ blockers?: string[];
14
+ evidence?: string[];
15
+ next_action?: string;
16
+ }
17
+ export interface StrictGateResult {
18
+ schema: 'yam.gate-result.v1';
19
+ generated_at: string;
20
+ gate_id: string;
21
+ boundary: GateBoundary;
22
+ status: 'passed' | 'failed';
23
+ checks: Array<{
24
+ id: string;
25
+ status: GateCheckStatus;
26
+ note: string;
27
+ required: boolean;
28
+ }>;
29
+ blockers: string[];
30
+ evidence: string[];
31
+ next_action: string;
32
+ contract_valid: boolean;
33
+ contract_errors: string[];
34
+ digest: string;
35
+ truth_status: 'verified' | 'blocked';
36
+ }
37
+ export declare function buildStrictGateResult(input?: StrictGateInput): StrictGateResult;
38
+ export declare function verifyStrictGateResult(value: unknown): {
39
+ schema: "yam.gate-result-verification.v1";
40
+ valid: boolean;
41
+ errors: string[];
42
+ truth_status: "verified" | "blocked";
43
+ next_action: string;
44
+ };