@davidvornholt/standards 0.18.0 → 0.20.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.
Files changed (38) hide show
  1. package/README.md +5 -4
  2. package/package.json +12 -1
  3. package/src/cli.ts +283 -73
  4. package/src/creds-add-cloudflare-commit.ts +2 -5
  5. package/src/creds-add-github.ts +82 -30
  6. package/src/creds-add-policy.ts +106 -26
  7. package/src/creds-add-preflight.ts +68 -0
  8. package/src/creds-add.ts +29 -20
  9. package/src/creds-args.ts +5 -0
  10. package/src/creds-cloudflare-zone.ts +40 -0
  11. package/src/creds-commands.ts +21 -9
  12. package/src/creds-github-app-api.ts +132 -0
  13. package/src/creds-github-apps.ts +107 -0
  14. package/src/creds-login-github-manifest.ts +38 -15
  15. package/src/creds-login-github.ts +78 -24
  16. package/src/creds-plan-renew.ts +2 -5
  17. package/src/creds-sops-value.ts +89 -0
  18. package/src/creds-sops.ts +0 -53
  19. package/src/creds-store-github.ts +121 -0
  20. package/src/creds-store-lock-holder.ts +98 -0
  21. package/src/creds-store-lock-inspection.ts +125 -0
  22. package/src/creds-store-lock-lease.ts +32 -0
  23. package/src/creds-store-lock.ts +27 -51
  24. package/src/creds-store.ts +14 -43
  25. package/src/github-command-shared.ts +8 -1
  26. package/src/github-live-drift.ts +3 -3
  27. package/src/managed-files.ts +116 -14
  28. package/src/managed-path-ownership.ts +85 -0
  29. package/src/poller-closing-reference.ts +179 -0
  30. package/src/poller-fix-outcome.ts +9 -0
  31. package/src/poller-fix-publication.ts +5 -12
  32. package/src/poller-fix-run.ts +2 -3
  33. package/src/poller-fix-validation.ts +7 -8
  34. package/src/poller-outcome.ts +4 -1
  35. package/src/poller-protected-paths.ts +1 -25
  36. package/src/poller-protocol.ts +0 -42
  37. package/src/poller-review-publication.ts +2 -8
  38. package/src/poller-review-validation.ts +5 -12
package/src/cli.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  import { execFileSync } from 'node:child_process';
14
14
  import { createHash } from 'node:crypto';
15
15
  import { existsSync, mkdtempSync, rmSync } from 'node:fs';
16
- import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
16
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
17
17
  import { tmpdir } from 'node:os';
18
18
  import { dirname, isAbsolute, join, posix, resolve } from 'node:path';
19
19
  import process from 'node:process';
@@ -32,8 +32,16 @@ import { loadGithubSettings } from './github-settings';
32
32
  import { isNonEmptyString, isRecord } from './github-settings-parse';
33
33
  import {
34
34
  findManagedFilesContainingBiomeDirectiveToken,
35
+ inspectManagedPath,
35
36
  listManagedFiles,
37
+ type ManagedEntry,
38
+ managedEntryDigestInput,
39
+ writeManagedEntry,
36
40
  } from './managed-files';
41
+ import {
42
+ interposedAncestor,
43
+ unlockedPathsUnder,
44
+ } from './managed-path-ownership';
37
45
  import { runPollerCommand } from './poller-commands';
38
46
  import { collectStructureProblems } from './structure-check';
39
47
  import type { StructureProfile } from './structure-profile';
@@ -44,6 +52,10 @@ const DEFAULT_UPSTREAM = 'github:davidvornholt/standards';
44
52
  // Characters of a sha256 hex digest shown in drift reports; enough to identify.
45
53
  const HASH_PREVIEW_LENGTH = 12;
46
54
 
55
+ // Unmanaged paths named as examples in a collision refusal, which also reports
56
+ // the full count.
57
+ const COLLISION_SAMPLE = 3;
58
+
47
59
  const GITHUB_PREFIX = 'github:';
48
60
  const SKIP_GITHUB_CHECK_ENV = 'STANDARDS_SKIP_GITHUB_CHECK';
49
61
 
@@ -112,6 +124,38 @@ const assertSafeRelativePath = (path: string, label: string): void => {
112
124
  }
113
125
  };
114
126
 
127
+ // `C:/Windows` is absolute on Windows and relative to POSIX `isAbsolute`, so a
128
+ // drive-qualified target written with forward slashes would escape a consumer
129
+ // synced on Linux. Managed targets are POSIX-relative on every platform.
130
+ const DRIVE_QUALIFIED = /^[A-Za-z]:/u;
131
+
132
+ // A managed symlink is mirrored verbatim into every consumer, so its target
133
+ // must resolve inside the consumer repository rather than at the host
134
+ // filesystem around it. Returns the problem instead of throwing, so a command
135
+ // can report every offending link before it writes anything.
136
+ const symlinkTargetProblem = (rel: string, target: string): string | null => {
137
+ const resolved = posix.normalize(posix.join(posix.dirname(rel), target));
138
+ if (
139
+ target.length === 0 ||
140
+ posix.isAbsolute(target) ||
141
+ DRIVE_QUALIFIED.test(target) ||
142
+ target.includes('\\') ||
143
+ resolved.split('/').includes('..')
144
+ ) {
145
+ return `canonical symlink ${rel} must point inside the repository: ${target}`;
146
+ }
147
+ return null;
148
+ };
149
+
150
+ const symlinkTargetProblems = (
151
+ entries: ReadonlyMap<string, ManagedEntry>,
152
+ ): ReadonlyArray<string> =>
153
+ [...entries]
154
+ .map(([rel, entry]) =>
155
+ entry.kind === 'symlink' ? symlinkTargetProblem(rel, entry.target) : null,
156
+ )
157
+ .filter((problem): problem is string => problem !== null);
158
+
115
159
  const parseManifest = (raw: unknown): Manifest => {
116
160
  if (typeof raw !== 'object' || raw === null) {
117
161
  throw new Error('sync-standards.json must be a JSON object');
@@ -249,22 +293,147 @@ type MirrorResult = {
249
293
  readonly updated: ReadonlyArray<string>;
250
294
  readonly deleted: ReadonlyArray<string>;
251
295
  readonly tampered: ReadonlyArray<string>;
296
+ // Locked paths the prune pass deliberately left on disk, each with the reason
297
+ // the operator needs in order to finish the job by hand.
298
+ readonly retained: ReadonlyArray<string>;
299
+ // Locked paths that sit below a managed symlink once this run has finished.
300
+ // Nothing is left on disk for the operator to act on: the lock entry is
301
+ // dropped, and the name now resolves through the link into content this run
302
+ // must not delete. Reported so the drop is not silent, never as an instruction.
303
+ readonly stale: ReadonlyArray<string>;
252
304
  };
253
305
 
254
306
  type MirrorOptions = {
255
- readonly manifest: Manifest;
256
- readonly srcDir: string;
307
+ readonly upstream: ReadonlyMap<string, ManagedEntry>;
257
308
  readonly consumer: string;
258
309
  readonly previous: Record<string, string>;
259
310
  readonly dryRun: boolean;
260
311
  };
261
312
 
313
+ // Two things init/sync must settle before their first write: an escaping
314
+ // canonical link target, and a managed destination that is a directory holding
315
+ // paths the lock does not record. The lock is the only record of what this
316
+ // engine put in a consumer, so whether the destination itself is locked decides
317
+ // nothing — a consumer can replace a locked link with a directory of its own
318
+ // skills, and that directory is still consumer work. Both problems are decided
319
+ // from data already in hand, and both are reported together so one run surfaces
320
+ // every offending managed path.
321
+ const mirrorPreconditionProblems = async (
322
+ consumer: string,
323
+ upstream: ReadonlyMap<string, ManagedEntry>,
324
+ previous: Record<string, string>,
325
+ ): Promise<ReadonlyArray<string>> => {
326
+ const locked = new Set(Object.keys(previous));
327
+ const collisions = await Promise.all(
328
+ [...upstream.keys()].map(async (rel) => {
329
+ const current = await inspectManagedPath(join(consumer, rel));
330
+ if (current?.kind !== 'directory') {
331
+ return null;
332
+ }
333
+ const unlocked = await unlockedPathsUnder(consumer, rel, locked);
334
+ return unlocked.length === 0
335
+ ? null
336
+ : `${rel} is a directory holding ${unlocked.length} path(s) this repository does not manage (${unlocked.slice(0, COLLISION_SAMPLE).join(', ')}); move or delete them, then re-run`;
337
+ }),
338
+ );
339
+ return [
340
+ ...symlinkTargetProblems(upstream),
341
+ ...collisions.filter((problem): problem is string => problem !== null),
342
+ ];
343
+ };
344
+
345
+ const assertNoProblems = (
346
+ problems: ReadonlyArray<string>,
347
+ headline: string,
348
+ ): void => {
349
+ if (problems.length > 0) {
350
+ throw new Error(
351
+ [`${headline}:`, ...problems.map((problem) => ` - ${problem}`)].join(
352
+ '\n',
353
+ ),
354
+ );
355
+ }
356
+ };
357
+
358
+ type PruneOutcome =
359
+ | { readonly kind: 'absent' }
360
+ | { readonly kind: 'delete'; readonly rel: string }
361
+ | { readonly kind: 'retain'; readonly note: string }
362
+ | { readonly kind: 'stale'; readonly note: string };
363
+
364
+ // Two things the prune pass must never do: resolve through a symlink it (or an
365
+ // older CLI's lock) put in the parent chain, and recursively delete a
366
+ // destination the consumer turned into a directory. Every answer is decided
367
+ // against the shape the mirror leaves behind, not the shape on disk right now,
368
+ // so `--dry-run` predicts what the matching real run does.
369
+ const pruneOutcome = async (
370
+ consumer: string,
371
+ rel: string,
372
+ planned: ReadonlyMap<string, ManagedEntry>,
373
+ ): Promise<PruneOutcome> => {
374
+ const ancestor = await interposedAncestor(consumer, rel, planned);
375
+ if (ancestor?.kind === 'symlink') {
376
+ return {
377
+ kind: 'stale',
378
+ note: `${rel} (below the symlink ${ancestor.rel}; nothing was deleted through it)`,
379
+ };
380
+ }
381
+ // Below a managed file the name resolves to nothing once the mirror has run,
382
+ // whether or not a directory still stands there while a dry run looks.
383
+ const current =
384
+ ancestor === null ? await inspectManagedPath(join(consumer, rel)) : null;
385
+ if (current === null) {
386
+ return { kind: 'absent' };
387
+ }
388
+ if (current.kind === 'directory') {
389
+ return {
390
+ kind: 'retain',
391
+ note: `${rel}: removed upstream but now a directory this repository does not manage; delete it yourself if you no longer want it`,
392
+ };
393
+ }
394
+ return { kind: 'delete', rel };
395
+ };
396
+
397
+ // Remove the locked paths that vanished upstream. Retained paths are reported
398
+ // and left alone; the lock drops every removable key either way, so the next
399
+ // run is clean.
400
+ const prune = async (
401
+ consumer: string,
402
+ removable: ReadonlyArray<string>,
403
+ planned: ReadonlyMap<string, ManagedEntry>,
404
+ dryRun: boolean,
405
+ ): Promise<{
406
+ readonly deleted: ReadonlyArray<string>;
407
+ readonly retained: ReadonlyArray<string>;
408
+ readonly stale: ReadonlyArray<string>;
409
+ }> => {
410
+ const outcomes = await Promise.all(
411
+ removable.map((rel) => pruneOutcome(consumer, rel, planned)),
412
+ );
413
+ const deleted = outcomes.flatMap((outcome) =>
414
+ outcome.kind === 'delete' ? [outcome.rel] : [],
415
+ );
416
+ const retained = outcomes.flatMap((outcome) =>
417
+ outcome.kind === 'retain' ? [outcome.note] : [],
418
+ );
419
+ const stale = outcomes.flatMap((outcome) =>
420
+ outcome.kind === 'stale' ? [outcome.note] : [],
421
+ );
422
+ if (!dryRun) {
423
+ // `pruneOutcome` decides what may be deleted; a directory takes the retain
424
+ // branch there and never reaches this call.
425
+ await Promise.all(
426
+ deleted.map((rel) => rm(join(consumer, rel), { force: true })),
427
+ );
428
+ }
429
+ return { deleted, retained, stale };
430
+ };
431
+
262
432
  // Mirror managed files into the consumer, deleting any previously-locked file
263
433
  // that no longer exists upstream (three-way reconcile against the lock). When
264
434
  // `dryRun` is set nothing is written or deleted; the returned plan is reported.
265
435
  const mirror = async ({
266
- manifest,
267
- srcDir,
436
+ upstream,
268
437
  consumer,
269
438
  previous,
270
439
  dryRun,
@@ -272,19 +441,23 @@ const mirror = async ({
272
441
  for (const rel of Object.keys(previous)) {
273
442
  assertSafeRelativePath(rel, 'sync-standards.lock file');
274
443
  }
275
- const upstream = await listManagedFiles(srcDir, manifest.paths);
444
+ assertNoProblems(
445
+ await mirrorPreconditionProblems(consumer, upstream, previous),
446
+ 'cannot mirror canonical files',
447
+ );
276
448
  const next: Record<string, string> = {};
277
449
  const created: Array<string> = [];
278
450
  const updated: Array<string> = [];
279
451
  const tampered: Array<string> = [];
280
452
  await Promise.all(
281
- [...upstream].map(async ([rel, abs]) => {
453
+ [...upstream].map(async ([rel, entry]) => {
282
454
  const dest = join(consumer, rel);
283
- const buf = await readFile(abs);
284
- const hash = sha256(buf);
285
- const currentHash = existsSync(dest)
286
- ? sha256(await readFile(dest))
287
- : null;
455
+ const hash = sha256(await managedEntryDigestInput(entry));
456
+ const current = await inspectManagedPath(dest);
457
+ const currentHash =
458
+ current === null
459
+ ? null
460
+ : sha256(await managedEntryDigestInput(current));
288
461
  const prev = previous[rel];
289
462
  if (prev !== undefined && currentHash !== null && currentHash !== prev) {
290
463
  tampered.push(rel);
@@ -296,70 +469,95 @@ const mirror = async ({
296
469
  }
297
470
  if (!dryRun) {
298
471
  await mkdir(dirname(dest), { recursive: true });
299
- await writeFile(dest, buf);
472
+ // The precondition above proved every path inside this directory is one
473
+ // the lock already records, so replacing it restores the managed shape
474
+ // rather than destroying consumer work.
475
+ if (current?.kind === 'directory') {
476
+ await rm(dest, { recursive: true, force: true });
477
+ }
478
+ await writeManagedEntry(dest, entry);
300
479
  }
301
480
  next[rel] = hash;
302
481
  }),
303
482
  );
304
- const deleted = Object.keys(previous).filter(
305
- (rel) => !(rel in next) && existsSync(join(consumer, rel)),
483
+ // `existsSync` would miss a broken link, so the prune pass lstats instead.
484
+ const { deleted, retained, stale } = await prune(
485
+ consumer,
486
+ Object.keys(previous).filter((rel) => !(rel in next)),
487
+ upstream,
488
+ dryRun,
306
489
  );
307
- if (!dryRun) {
308
- await Promise.all(deleted.map((rel) => rm(join(consumer, rel))));
309
- }
310
- return { files: next, created, updated, deleted, tampered };
490
+ return { files: next, created, updated, deleted, tampered, retained, stale };
491
+ };
492
+
493
+ // What a mirror reports, in the tense the run calls for: a dry run also
494
+ // announces the writes ahead of it, while a real sync stays quiet about them and
495
+ // speaks only of removals, lock entries it drops, and clobbered local edits. One
496
+ // list in both tenses so a dry run cannot describe an outcome differently from
497
+ // the real run it predicts.
498
+ const mirrorLines = (
499
+ result: MirrorResult,
500
+ dryRun: boolean,
501
+ ): ReadonlyArray<string> => {
502
+ const tense = (would: string, did: string): string => (dryRun ? would : did);
503
+ const writes = dryRun
504
+ ? [
505
+ ...result.created.map((rel) => `would create ${rel}`),
506
+ ...result.updated.map((rel) => `would update ${rel}`),
507
+ ]
508
+ : [];
509
+ const tampered =
510
+ result.tampered.length === 0
511
+ ? []
512
+ : [
513
+ `${tense('would overwrite', 'overwrote')} ${result.tampered.length} locally-modified canonical path(s): ${result.tampered.join(', ')}`,
514
+ ];
515
+ return [
516
+ ...writes,
517
+ ...result.deleted.map(
518
+ (rel) => `${tense('would delete', 'deleted')} ${rel} (removed upstream)`,
519
+ ),
520
+ ...result.retained.map(
521
+ (note) => `${tense('would leave in place', 'left in place')} ${note}`,
522
+ ),
523
+ ...result.stale.map(
524
+ (note) =>
525
+ `${tense('would drop', 'dropped')} the stale lock entry ${note}`,
526
+ ),
527
+ ...tampered,
528
+ ];
311
529
  };
312
530
 
313
- // Print what a mirror did (or, for a dry run, would do). Real syncs stay quiet
314
- // about unchanged files and only announce deletions and clobbered local edits.
315
531
  const reportMirror = (result: MirrorResult, dryRun: boolean): void => {
316
- if (dryRun) {
317
- for (const rel of result.created) {
318
- console.log(` would create ${rel}`);
319
- }
320
- for (const rel of result.updated) {
321
- console.log(` would update ${rel}`);
322
- }
323
- for (const rel of result.deleted) {
324
- console.log(` would delete ${rel} (removed upstream)`);
325
- }
326
- if (result.tampered.length > 0) {
327
- console.log(
328
- ` would overwrite ${result.tampered.length} locally-modified canonical file(s): ${result.tampered.join(', ')}`,
329
- );
330
- }
331
- return;
332
- }
333
- for (const rel of result.deleted) {
334
- console.log(` deleted ${rel} (removed upstream)`);
335
- }
336
- if (result.tampered.length > 0) {
337
- console.log(
338
- ` overwrote ${result.tampered.length} locally-modified canonical file(s): ${result.tampered.join(', ')}`,
339
- );
532
+ for (const line of mirrorLines(result, dryRun)) {
533
+ console.log(` ${line}`);
340
534
  }
341
535
  };
342
536
 
537
+ // A retained or stale key leaves the lock, so the run rewrites `sync-standards.lock`
538
+ // even when it touches nothing on disk; neither may be summed into "no changes".
343
539
  const reportDryRunSummary = (
344
540
  result: MirrorResult,
345
541
  generated: boolean,
346
542
  ): void => {
543
+ const unlocked = result.retained.length + result.stale.length;
347
544
  const changes =
348
545
  result.created.length +
349
546
  result.updated.length +
350
547
  result.deleted.length +
548
+ unlocked +
351
549
  Number(generated);
352
550
  console.log(
353
551
  changes === 0
354
552
  ? 'dry run: already in sync; no changes'
355
- : `dry run: ${result.created.length} to create, ${result.updated.length} to update, ${result.deleted.length} to delete, ${Number(generated)} to generate`,
553
+ : `dry run: ${result.created.length} to create, ${result.updated.length} to update, ${result.deleted.length} to delete, ${Number(generated)} to generate, ${unlocked} to drop from the lock without deleting`,
356
554
  );
357
555
  };
358
556
 
359
557
  const seedTargets = (
360
558
  srcDir: string,
361
559
  seedDir: string,
362
- ): Promise<ReadonlyMap<string, string>> => {
560
+ ): Promise<ReadonlyMap<string, ManagedEntry>> => {
363
561
  const root = join(srcDir, seedDir);
364
562
  return listManagedFiles(root, ['.']);
365
563
  };
@@ -371,27 +569,39 @@ const runInit = async (
371
569
  ): Promise<void> => {
372
570
  const seeds = await seedTargets(src.dir, manifest.seedDir);
373
571
  assertDisjoint(manifest.paths, [...seeds.keys()]);
572
+ const upstream = await listManagedFiles(src.dir, manifest.paths);
573
+ // Seeds are written before the mirror, so both halves of the payload must
574
+ // clear their preconditions here for `init` to refuse before writing anything.
575
+ assertNoProblems(
576
+ [
577
+ ...symlinkTargetProblems(seeds),
578
+ ...(await mirrorPreconditionProblems(consumer, upstream, {})),
579
+ ],
580
+ 'cannot initialize from this source',
581
+ );
374
582
  const prospectiveDependabot = await prepareProspectiveDependabot(
375
- manifest,
376
- src.dir,
583
+ upstream,
377
584
  consumer,
378
585
  seeds.get(DEPENDABOT_LOCAL_FILE) ?? null,
379
586
  );
380
587
  await Promise.all(
381
- [...seeds].map(async ([rel, abs]) => {
588
+ [...seeds].map(async ([rel, entry]) => {
382
589
  const dest = join(consumer, rel);
383
- if (existsSync(dest)) {
590
+ // A seed destination is kept whenever anything at all occupies it.
591
+ // `existsSync` reports a broken link as absent, so it would call this
592
+ // destination free and let the seed replace a link the consumer put
593
+ // there; `lstat` sees the link and keeps it.
594
+ if ((await inspectManagedPath(dest)) !== null) {
384
595
  console.log(` kept ${rel} (already present)`);
385
596
  return;
386
597
  }
387
598
  await mkdir(dirname(dest), { recursive: true });
388
- await cp(abs, dest);
599
+ await writeManagedEntry(dest, entry);
389
600
  console.log(` seeded ${rel}`);
390
601
  }),
391
602
  );
392
603
  const result = await mirror({
393
- manifest,
394
- srcDir: src.dir,
604
+ upstream,
395
605
  consumer,
396
606
  previous: {},
397
607
  dryRun: false,
@@ -404,7 +614,7 @@ const runInit = async (
404
614
  files: result.files,
405
615
  });
406
616
  console.log(
407
- `init complete: ${Object.keys(result.files).length} managed file(s) at ${src.sha}`,
617
+ `init complete: ${Object.keys(result.files).length} managed path(s) at ${src.sha}`,
408
618
  );
409
619
  };
410
620
 
@@ -416,16 +626,15 @@ const runSync = async (
416
626
  ): Promise<void> => {
417
627
  const seeds = await seedTargets(src.dir, manifest.seedDir);
418
628
  assertDisjoint(manifest.paths, [...seeds.keys()]);
629
+ const upstream = await listManagedFiles(src.dir, manifest.paths);
419
630
  const prospectiveDependabot = await prepareProspectiveDependabot(
420
- manifest,
421
- src.dir,
631
+ upstream,
422
632
  consumer,
423
633
  null,
424
634
  );
425
635
  const lock = await readLock(consumer);
426
636
  const result = await mirror({
427
- manifest,
428
- srcDir: src.dir,
637
+ upstream,
429
638
  consumer,
430
639
  previous: lock?.files ?? {},
431
640
  dryRun,
@@ -446,7 +655,7 @@ const runSync = async (
446
655
  files: result.files,
447
656
  });
448
657
  console.log(
449
- `sync complete: ${Object.keys(result.files).length} managed file(s) at ${src.sha}`,
658
+ `sync complete: ${Object.keys(result.files).length} managed path(s) at ${src.sha}`,
450
659
  );
451
660
  };
452
661
 
@@ -468,10 +677,11 @@ const runCheck = async (consumer: string): Promise<boolean> => {
468
677
  const results = await Promise.all(
469
678
  Object.entries(lock.files).map(async ([rel, hash]) => {
470
679
  const dest = join(consumer, rel);
471
- if (!existsSync(dest)) {
680
+ const entry = await inspectManagedPath(dest);
681
+ if (entry === null) {
472
682
  return ` missing: ${rel}`;
473
683
  }
474
- const current = sha256(await readFile(dest));
684
+ const current = sha256(await managedEntryDigestInput(entry));
475
685
  if (current !== hash) {
476
686
  return ` modified: ${rel} (expected ${hash.slice(0, HASH_PREVIEW_LENGTH)}, found ${current.slice(0, HASH_PREVIEW_LENGTH)})`;
477
687
  }
@@ -483,7 +693,7 @@ const runCheck = async (consumer: string): Promise<boolean> => {
483
693
  await findManagedFilesContainingBiomeDirectiveToken(lockedFiles);
484
694
  if (problems.length > 0) {
485
695
  console.error(
486
- `standards: ${problems.length} canonical file(s) drifted from the last synced state:`,
696
+ `standards: ${problems.length} canonical path(s) drifted from the last synced state:`,
487
697
  );
488
698
  console.error(problems.join('\n'));
489
699
  console.error(
@@ -501,7 +711,7 @@ const runCheck = async (consumer: string): Promise<boolean> => {
501
711
  }
502
712
  if (problems.length === 0) {
503
713
  console.log(
504
- `standards: ${Object.keys(lock.files).length} canonical file(s) match the last synced state`,
714
+ `standards: ${Object.keys(lock.files).length} canonical path(s) match the last synced state`,
505
715
  );
506
716
  }
507
717
  return problems.length === 0 && directiveFiles.length === 0;
@@ -591,14 +801,12 @@ type ProspectiveDependabot = {
591
801
  // file. For init, an existing overlay wins; otherwise the overlay seed is what
592
802
  // the command is about to install.
593
803
  const prepareProspectiveDependabot = async (
594
- manifest: Manifest,
595
- srcDir: string,
804
+ incoming: ReadonlyMap<string, ManagedEntry>,
596
805
  consumer: string,
597
- localSeed: string | null,
806
+ localSeed: ManagedEntry | null,
598
807
  ): Promise<ProspectiveDependabot> => {
599
- const incoming = await listManagedFiles(srcDir, manifest.paths);
600
- const basePath = incoming.get(DEPENDABOT_BASE_FILE);
601
- if (basePath === undefined) {
808
+ const base = incoming.get(DEPENDABOT_BASE_FILE);
809
+ if (base === undefined || base.kind !== 'file') {
602
810
  throw new Error(
603
811
  `source content must manage ${DEPENDABOT_BASE_FILE}; @davidvornholt/standards 0.10.1 requires a 0.10.1-compatible content ref`,
604
812
  );
@@ -608,10 +816,12 @@ const prepareProspectiveDependabot = async (
608
816
  );
609
817
  const local =
610
818
  existingLocal ??
611
- (localSeed === null ? null : await readFile(localSeed, 'utf8'));
819
+ (localSeed === null || localSeed.kind !== 'file'
820
+ ? null
821
+ : await readFile(localSeed.absolutePath, 'utf8'));
612
822
  const current = await readTextIfPresent(join(consumer, DEPENDABOT_FILE));
613
823
  const sources = {
614
- base: await readFile(basePath, 'utf8'),
824
+ base: await readFile(base.absolutePath, 'utf8'),
615
825
  local,
616
826
  current,
617
827
  };
@@ -1006,7 +1216,7 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
1006
1216
  };
1007
1217
 
1008
1218
  // The canonical workflow sets this only for its unprivileged quality job,
1009
- // where a separate isolated job runs the same live check with the settings-read
1219
+ // where a separate isolated job runs the same live check with its own workflow
1010
1220
  // token. Absent that exact workflow seam, local and explicit checks remain
1011
1221
  // fail-closed.
1012
1222
  const runGithubCheckGate = (consumer: string): Promise<boolean> => {
@@ -1,9 +1,6 @@
1
1
  import { deleteAccountToken } from './creds-cloudflare';
2
- import {
3
- type SopsValueChange,
4
- type SopsWriteResult,
5
- verifySopsStoredValue,
6
- } from './creds-sops';
2
+ import type { SopsValueChange, SopsWriteResult } from './creds-sops';
3
+ import { verifySopsStoredValue } from './creds-sops-value';
7
4
 
8
5
  export const commitCreatedCloudflareToken = async (input: {
9
6
  readonly consumer: string;