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.
- package/CHANGELOG.md +25 -0
- package/COMMANDS.md +27 -1
- package/README.md +19 -1
- package/ROADMAP.md +38 -6
- package/dist/bin/yam.js +389 -6
- package/dist/lib/benchmark-promotion.d.ts +53 -0
- package/dist/lib/benchmark-promotion.js +204 -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 +286 -30
- package/dist/lib/gate-result.d.ts +44 -0
- package/dist/lib/gate-result.js +219 -0
- package/dist/lib/trust-kernel.js +17 -1
- package/package.json +4 -2
- package/references/ueye.md +14 -0
- package/skills/ueye/SKILL.md +11 -0
- package/src/bin/yam.ts +394 -6
- package/src/lib/benchmark-promotion.ts +249 -0
- package/src/lib/design-production.ts +1441 -0
- package/src/lib/external-updates.ts +331 -28
- package/src/lib/gate-result.ts +223 -0
- package/src/lib/trust-kernel.ts +20 -1
- package/yam.manifest.json +2 -2
|
@@ -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
|
|
|
7
8
|
export const EXTERNAL_UPDATE_COMPONENTS = ['yam', 'scrapling', 'insane-search'] as const;
|
|
8
9
|
export type ExternalUpdateComponent = typeof EXTERNAL_UPDATE_COMPONENTS[number];
|
|
@@ -83,6 +84,21 @@ export interface ExternalComponentReceipt {
|
|
|
83
84
|
persistence?: 'written' | 'failed';
|
|
84
85
|
receipt_path?: string;
|
|
85
86
|
receipt_error?: string;
|
|
87
|
+
install_identity?: YamInstallIdentity;
|
|
88
|
+
rollback_identity?: YamInstallIdentity;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface YamInstallIdentity {
|
|
92
|
+
expected_version: string;
|
|
93
|
+
observed_version: string;
|
|
94
|
+
first_on_path: string;
|
|
95
|
+
canonical_executable: string;
|
|
96
|
+
npm_global_root: string;
|
|
97
|
+
canonical_npm_global_root: string;
|
|
98
|
+
package_root: string;
|
|
99
|
+
package_manifest: string;
|
|
100
|
+
package_entrypoint: string;
|
|
101
|
+
canonical_entrypoint: string;
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
interface ResolvedDependencies {
|
|
@@ -100,6 +116,18 @@ interface ReceiptWriteResult {
|
|
|
100
116
|
error: string;
|
|
101
117
|
}
|
|
102
118
|
|
|
119
|
+
interface YamIdentityValidation {
|
|
120
|
+
passed: boolean;
|
|
121
|
+
error: string;
|
|
122
|
+
check: ExternalComponentReceipt['checks'][number];
|
|
123
|
+
identity?: YamInstallIdentity;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface YamRollbackResult {
|
|
127
|
+
ok: boolean;
|
|
128
|
+
identity?: YamInstallIdentity;
|
|
129
|
+
}
|
|
130
|
+
|
|
103
131
|
const YAM_REGISTRY_URL = 'https://registry.npmjs.org/yam-flow/latest';
|
|
104
132
|
const SCRAPLING_PYPI_URL = 'https://pypi.org/pypi/scrapling/json';
|
|
105
133
|
const INSANE_MARKETPLACE = 'gptaku-codex';
|
|
@@ -156,6 +184,44 @@ export async function applyExternalUpdates(
|
|
|
156
184
|
const receipts: ExternalComponentReceipt[] = [];
|
|
157
185
|
try {
|
|
158
186
|
lockHandle = await acquireApplyLock(deps);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const lockError = boundedNote(errorMessage(error));
|
|
189
|
+
const nextAction = `confirm no updater is running, inspect the existing lock, then remove it manually only if stale: ${deps.paths.lockFile}`;
|
|
190
|
+
const gateResult = buildStrictGateResult({
|
|
191
|
+
gate_id: 'external-update-apply',
|
|
192
|
+
boundary: 'update',
|
|
193
|
+
checks: [{
|
|
194
|
+
id: 'update-lock-acquisition',
|
|
195
|
+
status: 'failed',
|
|
196
|
+
note: lockError,
|
|
197
|
+
required: true
|
|
198
|
+
}],
|
|
199
|
+
blockers: [`update_lock_not_acquired: ${lockError}`],
|
|
200
|
+
evidence: [`requested-components:${components.join(',')}`, `lock-path:${deps.paths.lockFile}`],
|
|
201
|
+
next_action: nextAction
|
|
202
|
+
});
|
|
203
|
+
const gateContract = verifyStrictGateResult(gateResult);
|
|
204
|
+
return {
|
|
205
|
+
schema: 'yam.external-update-apply.v1',
|
|
206
|
+
generated_at: deps.now().toISOString(),
|
|
207
|
+
mutation_authorized: true,
|
|
208
|
+
requested_components: components,
|
|
209
|
+
applied_components: [],
|
|
210
|
+
receipts: [],
|
|
211
|
+
stopped_early: true,
|
|
212
|
+
lock: {
|
|
213
|
+
path: deps.paths.lockFile,
|
|
214
|
+
acquired: false,
|
|
215
|
+
released: false
|
|
216
|
+
},
|
|
217
|
+
gate_result: gateResult,
|
|
218
|
+
gate_contract: gateContract,
|
|
219
|
+
success: false,
|
|
220
|
+
truth_status: 'blocked' as const,
|
|
221
|
+
next_action: nextAction
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
159
225
|
for (const component of components) {
|
|
160
226
|
const receipt = component === 'yam'
|
|
161
227
|
? await applyYam(currentYamVersion, deps)
|
|
@@ -191,6 +257,49 @@ export async function applyExternalUpdates(
|
|
|
191
257
|
|| receipt.persistence === 'failed'
|
|
192
258
|
));
|
|
193
259
|
const incomplete = receipts.length !== components.length;
|
|
260
|
+
const operationSuccess = !failed && !incomplete && lockReleased;
|
|
261
|
+
const gateBlockers = receipts.flatMap((receipt) => receipt.checks
|
|
262
|
+
.filter((check) => check.status === 'failed')
|
|
263
|
+
.map((check) => `${receipt.component}.${check.id}: ${check.note}`));
|
|
264
|
+
for (const receipt of receipts) {
|
|
265
|
+
if (receipt.persistence === 'failed') gateBlockers.push(`${receipt.component}.receipt_persistence: ${receipt.receipt_error || 'receipt was not persisted'}`);
|
|
266
|
+
if (receipt.outcome === 'manual_plugin_update_required') gateBlockers.push(`${receipt.component}.manual_update_required: ${receipt.error || 'safe in-place plugin update unavailable'}`);
|
|
267
|
+
}
|
|
268
|
+
if (incomplete) gateBlockers.push(`component_inventory_incomplete: applied ${receipts.length} of ${components.length}`);
|
|
269
|
+
if (!lockReleased) gateBlockers.push(`update_lock_not_released: ${deps.paths.lockFile}`);
|
|
270
|
+
const gateResult = buildStrictGateResult({
|
|
271
|
+
gate_id: 'external-update-apply',
|
|
272
|
+
boundary: 'update',
|
|
273
|
+
checks: [
|
|
274
|
+
{
|
|
275
|
+
id: 'component-inventory',
|
|
276
|
+
status: incomplete ? 'failed' : 'passed',
|
|
277
|
+
note: incomplete ? `applied ${receipts.length} of ${components.length} requested components` : 'every requested component produced one outcome receipt',
|
|
278
|
+
required: true
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 'component-outcomes',
|
|
282
|
+
status: failed ? 'failed' : 'passed',
|
|
283
|
+
note: failed ? firstFailureNextAction(receipts) : 'all component outcomes passed',
|
|
284
|
+
required: true
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'update-lock-release',
|
|
288
|
+
status: lockReleased ? 'passed' : 'failed',
|
|
289
|
+
note: lockReleased ? 'external update lock was removed' : `inspect the stale lock: ${deps.paths.lockFile}`,
|
|
290
|
+
required: true
|
|
291
|
+
}
|
|
292
|
+
],
|
|
293
|
+
blockers: gateBlockers,
|
|
294
|
+
evidence: receipts.map((receipt) => `${receipt.component}:${receipt.outcome}:${receipt.persistence || 'not-persisted'}:${receipt.receipt_path || 'no-path'}`),
|
|
295
|
+
next_action: failed
|
|
296
|
+
? firstFailureNextAction(receipts)
|
|
297
|
+
: !lockReleased
|
|
298
|
+
? `confirm no updater is running, inspect the stale lock, then remove it manually: ${deps.paths.lockFile}`
|
|
299
|
+
: 'review the component receipts and restart Codex only if yam skills or a Codex plugin changed'
|
|
300
|
+
});
|
|
301
|
+
const gateContract = verifyStrictGateResult(gateResult);
|
|
302
|
+
const success = operationSuccess && gateResult.status === 'passed' && gateContract.valid;
|
|
194
303
|
return {
|
|
195
304
|
schema: 'yam.external-update-apply.v1',
|
|
196
305
|
generated_at: deps.now().toISOString(),
|
|
@@ -201,10 +310,13 @@ export async function applyExternalUpdates(
|
|
|
201
310
|
stopped_early: incomplete,
|
|
202
311
|
lock: {
|
|
203
312
|
path: deps.paths.lockFile,
|
|
313
|
+
acquired: true,
|
|
204
314
|
released: lockReleased
|
|
205
315
|
},
|
|
206
|
-
|
|
207
|
-
|
|
316
|
+
gate_result: gateResult,
|
|
317
|
+
gate_contract: gateContract,
|
|
318
|
+
success,
|
|
319
|
+
truth_status: success ? 'verified' : 'blocked',
|
|
208
320
|
next_action: failed
|
|
209
321
|
? firstFailureNextAction(receipts)
|
|
210
322
|
: !lockReleased
|
|
@@ -465,32 +577,49 @@ async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
|
|
|
465
577
|
}
|
|
466
578
|
sideEffects.push(`globally installed ${exactPackage}`);
|
|
467
579
|
|
|
580
|
+
let installIdentity: YamInstallIdentity | undefined;
|
|
581
|
+
let rollbackIdentity: YamInstallIdentity | undefined;
|
|
582
|
+
const identityValidation = await validateEffectiveYamIdentity(
|
|
583
|
+
check.latest_version,
|
|
584
|
+
deps,
|
|
585
|
+
'yam_effective_identity'
|
|
586
|
+
);
|
|
587
|
+
checks.push(identityValidation.check);
|
|
588
|
+
if (identityValidation.passed) installIdentity = identityValidation.identity;
|
|
589
|
+
|
|
468
590
|
const postCommands: Array<[string, string[]]> = [
|
|
469
591
|
['yam_install_skills', ['install']],
|
|
470
592
|
['yam_version', ['version']],
|
|
471
593
|
['yam_status', ['status']]
|
|
472
594
|
];
|
|
473
|
-
let postFailure =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
595
|
+
let postFailure = identityValidation.passed
|
|
596
|
+
? ''
|
|
597
|
+
: `yam_effective_identity failed: ${identityValidation.error}`;
|
|
598
|
+
if (!postFailure && installIdentity) {
|
|
599
|
+
for (const [id, args] of postCommands) {
|
|
600
|
+
const result = await deps.run(installIdentity.first_on_path, args, { env: deps.env, timeout: 180000 });
|
|
601
|
+
checks.push(commandCheck(id, result));
|
|
602
|
+
if (!result.ok || (id === 'yam_version' && extractVersion(result.stdout) !== check.latest_version)) {
|
|
603
|
+
postFailure = `${id} failed: ${commandNote(result)}`;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
480
606
|
}
|
|
481
607
|
}
|
|
482
|
-
if (!postFailure) {
|
|
483
|
-
const doctor = await deps.run(
|
|
608
|
+
if (!postFailure && installIdentity) {
|
|
609
|
+
const doctor = await deps.run(installIdentity.first_on_path, ['doctor', '--json'], { env: deps.env, timeout: 180000 });
|
|
484
610
|
const validation = doctorValidation('yam_finalize_doctor', doctor);
|
|
485
611
|
checks.push(validation.check);
|
|
486
612
|
if (!validation.passed) postFailure = `yam_finalize_doctor failed: ${validation.error}`;
|
|
487
613
|
}
|
|
488
614
|
if (postFailure) {
|
|
489
615
|
const rollback = await rollbackYam(check.installed_version, deps, checks);
|
|
616
|
+
rollbackIdentity = rollback.identity;
|
|
490
617
|
return persistReceipt(baseReceipt(check, 'failed', checks, {
|
|
491
618
|
...yamRollback(check.installed_version),
|
|
492
|
-
automatic: rollback
|
|
619
|
+
automatic: rollback.ok
|
|
493
620
|
}, {
|
|
621
|
+
...(installIdentity ? { install_identity: installIdentity } : {}),
|
|
622
|
+
...(rollbackIdentity ? { rollback_identity: rollbackIdentity } : {}),
|
|
494
623
|
side_effects: sideEffects,
|
|
495
624
|
error: postFailure
|
|
496
625
|
}), deps);
|
|
@@ -498,16 +627,18 @@ async function applyYam(currentYamVersion: string, deps: ResolvedDependencies) {
|
|
|
498
627
|
|
|
499
628
|
const receipt = baseReceipt(check, 'updated', checks, yamRollback(check.installed_version), {
|
|
500
629
|
installed_version: check.latest_version,
|
|
630
|
+
...(installIdentity ? { install_identity: installIdentity } : {}),
|
|
501
631
|
side_effects: sideEffects,
|
|
502
632
|
truth_status: 'verified'
|
|
503
633
|
});
|
|
504
634
|
const persisted = await persistReceipt(receipt, deps);
|
|
505
635
|
if (persisted.persistence === 'failed') {
|
|
506
636
|
const rollback = await rollbackYam(check.installed_version, deps, persisted.checks);
|
|
507
|
-
persisted.rollback_hint.automatic = rollback;
|
|
637
|
+
persisted.rollback_hint.automatic = rollback.ok;
|
|
638
|
+
if (rollback.identity) persisted.rollback_identity = rollback.identity;
|
|
508
639
|
persisted.outcome = 'failed';
|
|
509
640
|
persisted.truth_status = 'blocked';
|
|
510
|
-
persisted.error = `update completed but receipt persistence failed; automatic rollback ${rollback ? 'passed' : 'failed'}`;
|
|
641
|
+
persisted.error = `update completed but receipt persistence failed; automatic rollback ${rollback.ok ? 'passed' : 'failed'}`;
|
|
511
642
|
}
|
|
512
643
|
return persisted;
|
|
513
644
|
}
|
|
@@ -516,30 +647,155 @@ async function rollbackYam(
|
|
|
516
647
|
previousVersion: string,
|
|
517
648
|
deps: ResolvedDependencies,
|
|
518
649
|
checks: ExternalComponentReceipt['checks']
|
|
519
|
-
) {
|
|
520
|
-
if (!previousVersion) return false;
|
|
650
|
+
): Promise<YamRollbackResult> {
|
|
651
|
+
if (!previousVersion) return { ok: false };
|
|
521
652
|
const result = await deps.run('npm', ['install', '-g', `yam-flow@${previousVersion}`], {
|
|
522
653
|
env: deps.env,
|
|
523
654
|
timeout: 600000
|
|
524
655
|
});
|
|
525
656
|
checks.push(commandCheck('automatic_yam_rollback', result));
|
|
526
|
-
if (!result.ok) return false;
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
657
|
+
if (!result.ok) return { ok: false };
|
|
658
|
+
|
|
659
|
+
const identityValidation = await validateEffectiveYamIdentity(
|
|
660
|
+
previousVersion,
|
|
661
|
+
deps,
|
|
662
|
+
'automatic_yam_rollback_identity'
|
|
663
|
+
);
|
|
664
|
+
checks.push(identityValidation.check);
|
|
665
|
+
if (!identityValidation.passed || !identityValidation.identity) return { ok: false };
|
|
666
|
+
|
|
667
|
+
const executable = identityValidation.identity.first_on_path;
|
|
668
|
+
const install = await deps.run(executable, ['install'], { env: deps.env, timeout: 180000 });
|
|
669
|
+
const version = await deps.run(executable, ['version'], { env: deps.env, timeout: 30000 });
|
|
670
|
+
const status = await deps.run(executable, ['status'], { env: deps.env, timeout: 120000 });
|
|
671
|
+
const doctor = await deps.run(executable, ['doctor', '--json'], { env: deps.env, timeout: 180000 });
|
|
531
672
|
const doctorResult = doctorValidation('automatic_yam_rollback_doctor', doctor);
|
|
532
673
|
checks.push(commandCheck('automatic_yam_rollback_skills', install));
|
|
533
674
|
checks.push(commandCheck('automatic_yam_rollback_version', version));
|
|
534
675
|
checks.push(commandCheck('automatic_yam_rollback_status', status));
|
|
535
676
|
checks.push(doctorResult.check);
|
|
536
|
-
return
|
|
677
|
+
return {
|
|
678
|
+
ok: (
|
|
537
679
|
install.ok
|
|
538
680
|
&& version.ok
|
|
539
681
|
&& extractVersion(version.stdout) === previousVersion
|
|
540
682
|
&& status.ok
|
|
541
683
|
&& doctorResult.passed
|
|
542
|
-
|
|
684
|
+
),
|
|
685
|
+
identity: identityValidation.identity
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function validateEffectiveYamIdentity(
|
|
690
|
+
expectedVersion: string,
|
|
691
|
+
deps: ResolvedDependencies,
|
|
692
|
+
id: string
|
|
693
|
+
): Promise<YamIdentityValidation> {
|
|
694
|
+
try {
|
|
695
|
+
const npmRootResult = await deps.run('npm', ['root', '-g'], { env: deps.env, timeout: 30000 });
|
|
696
|
+
if (!npmRootResult.ok) {
|
|
697
|
+
throw new Error(`npm global root lookup failed: ${commandNote(npmRootResult)}`);
|
|
698
|
+
}
|
|
699
|
+
const npmGlobalRoot = firstOutputLine(npmRootResult.stdout);
|
|
700
|
+
if (!npmGlobalRoot || !path.isAbsolute(npmGlobalRoot)) {
|
|
701
|
+
throw new Error('npm global root lookup did not return one absolute path');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const packageRoot = path.join(path.resolve(npmGlobalRoot), 'yam-flow');
|
|
705
|
+
const packageManifest = path.join(packageRoot, 'package.json');
|
|
706
|
+
let manifest: JsonRecord;
|
|
707
|
+
try {
|
|
708
|
+
const parsed = JSON.parse(await fsp.readFile(packageManifest, 'utf8'));
|
|
709
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
|
|
710
|
+
manifest = parsed;
|
|
711
|
+
} catch {
|
|
712
|
+
throw new Error(`yam-flow package manifest is missing or malformed: ${packageManifest}`);
|
|
713
|
+
}
|
|
714
|
+
if (manifest.name !== 'yam-flow') {
|
|
715
|
+
throw new Error(`npm global package manifest has unexpected name at ${packageManifest}`);
|
|
716
|
+
}
|
|
717
|
+
const manifestVersion = requireStableVersion(manifest.version, 'yam-flow package manifest version');
|
|
718
|
+
if (manifestVersion !== expectedVersion) {
|
|
719
|
+
throw new Error(`yam-flow package manifest version ${manifestVersion} does not match expected ${expectedVersion}`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const binValue = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.yam;
|
|
723
|
+
if (typeof binValue !== 'string' || !binValue.trim()) {
|
|
724
|
+
throw new Error(`yam-flow package manifest has no usable yam bin entry: ${packageManifest}`);
|
|
725
|
+
}
|
|
726
|
+
const packageEntrypoint = path.resolve(packageRoot, binValue);
|
|
727
|
+
assertChildPath(packageRoot, packageEntrypoint, 'yam-flow package entrypoint');
|
|
728
|
+
|
|
729
|
+
const whichResult = await deps.run('which', ['yam'], { env: deps.env, timeout: 10000 });
|
|
730
|
+
if (!whichResult.ok) {
|
|
731
|
+
throw new Error(`first yam on PATH could not be resolved: ${commandNote(whichResult)}`);
|
|
732
|
+
}
|
|
733
|
+
const firstOnPath = firstOutputLine(whichResult.stdout);
|
|
734
|
+
if (!firstOnPath || !path.isAbsolute(firstOnPath)) {
|
|
735
|
+
throw new Error('first yam on PATH was not reported as one absolute path');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let canonicalNpmGlobalRoot: string;
|
|
739
|
+
let canonicalPackageRoot: string;
|
|
740
|
+
let canonicalEntrypoint: string;
|
|
741
|
+
let canonicalExecutable: string;
|
|
742
|
+
try {
|
|
743
|
+
[canonicalNpmGlobalRoot, canonicalPackageRoot, canonicalEntrypoint, canonicalExecutable] = await Promise.all([
|
|
744
|
+
fsp.realpath(npmGlobalRoot),
|
|
745
|
+
fsp.realpath(packageRoot),
|
|
746
|
+
fsp.realpath(packageEntrypoint),
|
|
747
|
+
fsp.realpath(firstOnPath)
|
|
748
|
+
]);
|
|
749
|
+
} catch {
|
|
750
|
+
throw new Error('yam install identity contains an unreadable package path, entrypoint, or PATH executable');
|
|
751
|
+
}
|
|
752
|
+
assertChildPath(canonicalNpmGlobalRoot, canonicalPackageRoot, 'canonical yam-flow package root');
|
|
753
|
+
assertChildPath(canonicalPackageRoot, canonicalEntrypoint, 'canonical yam-flow package entrypoint');
|
|
754
|
+
if (canonicalExecutable !== canonicalEntrypoint) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`first yam on PATH is shadowed: ${firstOnPath} resolves to ${canonicalExecutable}, expected ${canonicalEntrypoint}; reorder PATH or remove the stale shim before retrying`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const versionResult = await deps.run(firstOnPath, ['version'], { env: deps.env, timeout: 30000 });
|
|
761
|
+
if (!versionResult.ok) {
|
|
762
|
+
throw new Error(`effective yam version check failed: ${commandNote(versionResult)}`);
|
|
763
|
+
}
|
|
764
|
+
const observedVersion = requireStableVersion(extractVersion(versionResult.stdout), 'effective yam version');
|
|
765
|
+
if (observedVersion !== expectedVersion) {
|
|
766
|
+
throw new Error(`effective yam version ${observedVersion} does not match expected ${expectedVersion}`);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const identity: YamInstallIdentity = {
|
|
770
|
+
expected_version: expectedVersion,
|
|
771
|
+
observed_version: observedVersion,
|
|
772
|
+
first_on_path: firstOnPath,
|
|
773
|
+
canonical_executable: canonicalExecutable,
|
|
774
|
+
npm_global_root: path.resolve(npmGlobalRoot),
|
|
775
|
+
canonical_npm_global_root: canonicalNpmGlobalRoot,
|
|
776
|
+
package_root: packageRoot,
|
|
777
|
+
package_manifest: packageManifest,
|
|
778
|
+
package_entrypoint: packageEntrypoint,
|
|
779
|
+
canonical_entrypoint: canonicalEntrypoint
|
|
780
|
+
};
|
|
781
|
+
return {
|
|
782
|
+
passed: true,
|
|
783
|
+
error: '',
|
|
784
|
+
check: {
|
|
785
|
+
id,
|
|
786
|
+
status: 'passed',
|
|
787
|
+
note: boundedNote(`verified yam install identity: ${JSON.stringify(identity)}`)
|
|
788
|
+
},
|
|
789
|
+
identity
|
|
790
|
+
};
|
|
791
|
+
} catch (error) {
|
|
792
|
+
const note = boundedNote(errorMessage(error));
|
|
793
|
+
return {
|
|
794
|
+
passed: false,
|
|
795
|
+
error: note,
|
|
796
|
+
check: { id, status: 'failed', note }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
543
799
|
}
|
|
544
800
|
|
|
545
801
|
async function applyScrapling(deps: ResolvedDependencies) {
|
|
@@ -1058,7 +1314,7 @@ function doctorValidation(id: string, result: ExternalCommandResult) {
|
|
|
1058
1314
|
};
|
|
1059
1315
|
}
|
|
1060
1316
|
if (report.ok !== true) {
|
|
1061
|
-
const note =
|
|
1317
|
+
const note = doctorFailureDiagnostic(report);
|
|
1062
1318
|
return {
|
|
1063
1319
|
passed: false,
|
|
1064
1320
|
error: note,
|
|
@@ -1074,6 +1330,37 @@ function doctorValidation(id: string, result: ExternalCommandResult) {
|
|
|
1074
1330
|
};
|
|
1075
1331
|
}
|
|
1076
1332
|
|
|
1333
|
+
function doctorFailureDiagnostic(report: JsonRecord) {
|
|
1334
|
+
const detail = arrayValue(report.nextActionDetails).find((item) => (
|
|
1335
|
+
item
|
|
1336
|
+
&& typeof item === 'object'
|
|
1337
|
+
&& !Array.isArray(item)
|
|
1338
|
+
&& [item.reason, item.next_action, item.nextAction, item.command].some((value) => cleanDiagnosticValue(value))
|
|
1339
|
+
));
|
|
1340
|
+
const issue = cleanDiagnosticValue(detail?.reason) || firstDiagnosticValue(report.issues);
|
|
1341
|
+
const nextAction = cleanDiagnosticValue(detail?.next_action || detail?.nextAction)
|
|
1342
|
+
|| firstDiagnosticValue(report.nextActions);
|
|
1343
|
+
const command = cleanDiagnosticValue(detail?.command);
|
|
1344
|
+
const fragments = [
|
|
1345
|
+
issue ? `issue: ${issue}` : '',
|
|
1346
|
+
nextAction ? `next: ${nextAction}` : '',
|
|
1347
|
+
command ? `command: ${command}` : ''
|
|
1348
|
+
].filter(Boolean);
|
|
1349
|
+
return boundedNote(
|
|
1350
|
+
fragments.length
|
|
1351
|
+
? `yam.doctor.v1 reported ok=false; ${fragments.join('; ')}`
|
|
1352
|
+
: 'yam.doctor.v1 reported ok=false'
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function firstDiagnosticValue(value: unknown) {
|
|
1357
|
+
return arrayValue(value).map(cleanDiagnosticValue).find(Boolean) || '';
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function cleanDiagnosticValue(value: unknown) {
|
|
1361
|
+
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1077
1364
|
function boundedNote(text: string) {
|
|
1078
1365
|
return redactSensitiveText(String(text || '')).slice(0, 800);
|
|
1079
1366
|
}
|
|
@@ -1125,6 +1412,10 @@ function extractVersion(text: string) {
|
|
|
1125
1412
|
return String(text || '').match(/\b\d+(?:\.\d+){1,3}\b/)?.[0] || '';
|
|
1126
1413
|
}
|
|
1127
1414
|
|
|
1415
|
+
function firstOutputLine(text: string) {
|
|
1416
|
+
return String(text || '').split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1128
1419
|
function compareVersions(left: string, right: string) {
|
|
1129
1420
|
const a = left.split('.').map(Number);
|
|
1130
1421
|
const b = right.split('.').map(Number);
|
|
@@ -1201,6 +1492,7 @@ async function unlinkIfExists(file: string) {
|
|
|
1201
1492
|
function redactSensitiveText(text: string) {
|
|
1202
1493
|
return String(text || '')
|
|
1203
1494
|
.replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
|
|
1495
|
+
.replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY))\s*[:=]\s*\S+/gi, '$1=[redacted]')
|
|
1204
1496
|
.replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
|
|
1205
1497
|
.replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
|
|
1206
1498
|
.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]');
|
|
@@ -1213,8 +1505,19 @@ function firstFailureNextAction(receipts: ExternalComponentReceipt[]) {
|
|
|
1213
1505
|
|| item.persistence === 'failed'
|
|
1214
1506
|
));
|
|
1215
1507
|
if (!receipt) return 'inspect the incomplete component inventory before retrying';
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1508
|
+
const deepestFailedCheck = [...receipt.checks].reverse().find((check) => check.status === 'failed');
|
|
1509
|
+
const deepestCause = deepestFailedCheck
|
|
1510
|
+
? `${deepestFailedCheck.id} failed: ${deepestFailedCheck.note}`
|
|
1511
|
+
: receipt.persistence === 'failed' && receipt.receipt_error
|
|
1512
|
+
? `receipt persistence failed: ${receipt.receipt_error}`
|
|
1513
|
+
: receipt.error || `${receipt.component} update failed`;
|
|
1514
|
+
const persistenceContext = deepestFailedCheck && receipt.persistence === 'failed' && receipt.receipt_error
|
|
1515
|
+
? `; secondary receipt persistence failure: ${receipt.receipt_error}`
|
|
1516
|
+
: '';
|
|
1517
|
+
const cause = boundedNote(`${deepestCause}${persistenceContext}`).slice(0, 420);
|
|
1518
|
+
const recoveryGuidance = receipt.outcome === 'manual_plugin_update_required'
|
|
1519
|
+
? 'Update Insane Search through a reviewed official Codex plugin workflow; do not remove-first or edit the plugin cache directly.'
|
|
1520
|
+
: receipt.rollback_hint.guidance || `resolve the ${receipt.component} failure before retrying`;
|
|
1521
|
+
const recovery = boundedNote(recoveryGuidance).slice(0, 340);
|
|
1522
|
+
return boundedNote(`resolve ${receipt.component} failure: ${cause}. ${recovery}`);
|
|
1220
1523
|
}
|