@mmnto/cli 1.107.1 → 1.109.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 (46) hide show
  1. package/dist/commands/config-drift.test.js +51 -0
  2. package/dist/commands/config-drift.test.js.map +1 -1
  3. package/dist/commands/docs.d.ts +1 -1
  4. package/dist/commands/docs.d.ts.map +1 -1
  5. package/dist/commands/docs.js +1 -1
  6. package/dist/commands/doctor-parity.d.ts.map +1 -1
  7. package/dist/commands/doctor-parity.js +25 -0
  8. package/dist/commands/doctor-parity.js.map +1 -1
  9. package/dist/commands/doctor-parity.test.js +86 -0
  10. package/dist/commands/doctor-parity.test.js.map +1 -1
  11. package/dist/commands/init-templates.d.ts +4 -4
  12. package/dist/commands/init-templates.d.ts.map +1 -1
  13. package/dist/commands/init-templates.js +85 -2
  14. package/dist/commands/init-templates.js.map +1 -1
  15. package/dist/commands/init.test.js +94 -0
  16. package/dist/commands/init.test.js.map +1 -1
  17. package/dist/commands/install-hooks.d.ts.map +1 -1
  18. package/dist/commands/install-hooks.js +13 -0
  19. package/dist/commands/install-hooks.js.map +1 -1
  20. package/dist/commands/install-hooks.test.js +69 -0
  21. package/dist/commands/install-hooks.test.js.map +1 -1
  22. package/dist/commands/shield-banner.test.d.ts +17 -0
  23. package/dist/commands/shield-banner.test.d.ts.map +1 -0
  24. package/dist/commands/shield-banner.test.js +118 -0
  25. package/dist/commands/shield-banner.test.js.map +1 -0
  26. package/dist/commands/shield.d.ts.map +1 -1
  27. package/dist/commands/shield.js +11 -0
  28. package/dist/commands/shield.js.map +1 -1
  29. package/dist/commands/spec-templates.d.ts +1 -1
  30. package/dist/commands/spec-templates.d.ts.map +1 -1
  31. package/dist/commands/spec-templates.js +1 -1
  32. package/dist/commands/verify-lockfile-sync.d.ts +11 -0
  33. package/dist/commands/verify-lockfile-sync.d.ts.map +1 -1
  34. package/dist/commands/verify-lockfile-sync.js +457 -2
  35. package/dist/commands/verify-lockfile-sync.js.map +1 -1
  36. package/dist/commands/verify-lockfile-sync.test.js +1112 -0
  37. package/dist/commands/verify-lockfile-sync.test.js.map +1 -1
  38. package/dist/git.d.ts +7 -0
  39. package/dist/git.d.ts.map +1 -1
  40. package/dist/git.js +75 -4
  41. package/dist/git.js.map +1 -1
  42. package/dist/git.test.js +125 -1
  43. package/dist/git.test.js.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/index.js.map +1 -1
  46. package/package.json +2 -2
@@ -52,12 +52,332 @@ index aaaaaa..bbbbbb 100644
52
52
  "commander": "^11.0.0"
53
53
  }
54
54
  `;
55
+ // The workspace at HEAD for the pin-add branch: importers `.`, `apps/web`, and
56
+ // `packages/cli`. Key forms match real pnpm emission — only scoped names are
57
+ // quoted; unscoped `packages:`/`snapshots:` keys are bare.
58
+ const HEAD_LOCKFILE_WORKSPACE = `lockfileVersion: '9.0'
59
+
60
+ settings:
61
+ autoInstallPeers: true
62
+
63
+ importers:
64
+
65
+ .:
66
+ dependencies:
67
+ '@mmnto/cli':
68
+ specifier: ^1.43.2
69
+ version: 1.43.2
70
+
71
+ apps/web:
72
+ dependencies:
73
+ foo:
74
+ specifier: ^1.2.3
75
+ version: 1.2.3
76
+
77
+ packages/cli:
78
+ dependencies:
79
+ commander:
80
+ specifier: ^11.0.0
81
+ version: 11.0.0
82
+
83
+ packages:
84
+
85
+ '@mmnto/cli@1.43.2':
86
+ resolution: {integrity: sha512-aaaa}
87
+ commander@11.0.0:
88
+ resolution: {integrity: sha512-bbbb}
89
+ foo@1.2.3:
90
+ resolution: {integrity: sha512-cccc}
91
+
92
+ snapshots:
93
+
94
+ '@mmnto/cli@1.43.2': {}
95
+ commander@11.0.0: {}
96
+ foo@1.2.3: {}
97
+ `;
98
+ // ─── Removed-pin fixtures (mmnto-ai/totem-strategy#630) ───
99
+ //
100
+ // Provenance: live-fire 2026-08-01, mmnto-ai/totem-strategy#630 comment
101
+ // 5152301962 — a failed optional-dependency fetch (dead registry token) removed
102
+ // every `@mmnto/strategy-doctrine` entry from pnpm-lock.yaml while package.json
103
+ // kept the pin, and `pnpm install` exited 0. The key shapes below are modeled on
104
+ // the real pnpm-lock.yaml entries: importer key (:48), packages key (:865),
105
+ // snapshots key (:3697).
106
+ const DOCTRINE_PIN_REMOVAL_DIFF = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
107
+ index aaaaaa..bbbbbb 100644
108
+ --- a/pnpm-lock.yaml
109
+ +++ b/pnpm-lock.yaml
110
+ @@ -44,9 +44,6 @@ importers:
111
+ optionalDependencies:
112
+ '@mmnto/cli':
113
+ specifier: workspace:*
114
+ version: link:packages/cli
115
+ - '@mmnto/strategy-doctrine':
116
+ - specifier: 0.1.28
117
+ - version: 0.1.28
118
+ @@ -862,4 +859,1 @@ packages:
119
+
120
+ - '@mmnto/strategy-doctrine@0.1.28':
121
+ - resolution: {integrity: sha512-sxiE7gO3XK/kPx59f9ZJockuCdxcu9PN0Mqy2oCcTnFm1wnGu2uXC47u/tDOEHkWrLmG07i//ggfhcqqVS7baw==}
122
+ -
123
+ @@ -3694,4 +3688,1 @@ snapshots:
124
+
125
+ - '@mmnto/strategy-doctrine@0.1.28':
126
+ - optional: true
127
+ -
128
+ `;
129
+ // HEAD lockfile after the drop: the package resolves NOWHERE, which is what
130
+ // makes the removal decisive rather than a dedupe.
131
+ const HEAD_LOCKFILE_WITHOUT_DOCTRINE = `lockfileVersion: '9.0'
132
+
133
+ importers:
134
+
135
+ .:
136
+ optionalDependencies:
137
+ '@mmnto/cli':
138
+ specifier: workspace:*
139
+ version: link:packages/cli
140
+
141
+ packages:
142
+
143
+ '@mmnto/cli@1.60.0':
144
+ resolution: {integrity: sha512-aaaa}
145
+
146
+ snapshots:
147
+
148
+ '@mmnto/cli@1.60.0': {}
149
+ `;
150
+ // Same drop, but the workspace also has a `packages/app` importer.
151
+ const HEAD_LOCKFILE_WITH_APP_IMPORTER = `lockfileVersion: '9.0'
152
+
153
+ importers:
154
+
155
+ .:
156
+ dependencies:
157
+ '@mmnto/cli':
158
+ specifier: workspace:*
159
+ version: link:packages/cli
160
+
161
+ packages/app:
162
+ dependencies:
163
+ '@mmnto/cli':
164
+ specifier: workspace:*
165
+ version: link:packages/cli
166
+
167
+ packages:
168
+
169
+ '@mmnto/cli@1.60.0':
170
+ resolution: {integrity: sha512-aaaa}
171
+
172
+ snapshots:
173
+
174
+ '@mmnto/cli@1.60.0': {}
175
+ `;
176
+ // A pure version bump: the old key is removed and a new key for the SAME name is
177
+ // added in the same diff. The HEAD lockfile still resolves the name.
178
+ const DOCTRINE_VERSION_BUMP_DIFF = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
179
+ index aaaaaa..bbbbbb 100644
180
+ --- a/pnpm-lock.yaml
181
+ +++ b/pnpm-lock.yaml
182
+ @@ -44,7 +44,7 @@ importers:
183
+ '@mmnto/strategy-doctrine':
184
+ - specifier: 0.1.28
185
+ - version: 0.1.28
186
+ + specifier: 0.1.29
187
+ + version: 0.1.29
188
+ @@ -862,4 +862,4 @@ packages:
189
+ - '@mmnto/strategy-doctrine@0.1.28':
190
+ - resolution: {integrity: sha512-aaaa}
191
+ + '@mmnto/strategy-doctrine@0.1.29':
192
+ + resolution: {integrity: sha512-bbbb}
193
+ @@ -3694,4 +3694,4 @@ snapshots:
194
+ - '@mmnto/strategy-doctrine@0.1.28':
195
+ - optional: true
196
+ + '@mmnto/strategy-doctrine@0.1.29':
197
+ + optional: true
198
+ `;
199
+ const HEAD_LOCKFILE_WITH_DOCTRINE_29 = `lockfileVersion: '9.0'
200
+
201
+ importers:
202
+
203
+ .:
204
+ optionalDependencies:
205
+ '@mmnto/strategy-doctrine':
206
+ specifier: 0.1.29
207
+ version: 0.1.29
208
+
209
+ packages:
210
+
211
+ '@mmnto/strategy-doctrine@0.1.29':
212
+ resolution: {integrity: sha512-bbbb}
213
+
214
+ snapshots:
215
+
216
+ '@mmnto/strategy-doctrine@0.1.29': {}
217
+ `;
218
+ // A dedupe: one version's keys go away while the package still resolves at HEAD
219
+ // under another version. Unscoped keys are BARE — real pnpm quotes only names
220
+ // that need it.
221
+ const DEDUPE_DIFF = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
222
+ index aaaaaa..bbbbbb 100644
223
+ --- a/pnpm-lock.yaml
224
+ +++ b/pnpm-lock.yaml
225
+ @@ -1200,4 +1200,1 @@ packages:
226
+ - ajv@6.15.0:
227
+ - resolution: {integrity: sha512-aaaa}
228
+ -
229
+ @@ -4100,4 +4097,1 @@ snapshots:
230
+ - ajv@6.15.0: {}
231
+ -
232
+ `;
233
+ const HEAD_LOCKFILE_WITH_AJV_8 = `lockfileVersion: '9.0'
234
+
235
+ importers:
236
+
237
+ .:
238
+ dependencies:
239
+ ajv:
240
+ specifier: ^8.18.0
241
+ version: 8.18.0
242
+
243
+ packages:
244
+
245
+ ajv@8.18.0:
246
+ resolution: {integrity: sha512-bbbb}
247
+
248
+ snapshots:
249
+
250
+ ajv@8.18.0: {}
251
+ `;
252
+ // ── Metadata-laundering fixtures (falsification round 2) ──
253
+ // `zod` is dropped outright, but survives at HEAD ONLY as a peer-metadata child
254
+ // of another package. Those children are valueless key lines at a DEEPER indent
255
+ // than a `packages:` entry — the shape that made a shape-only harvest report
256
+ // "still resolves" and pass the #630 class through.
257
+ const ZOD_PIN_REMOVAL_DIFF = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
258
+ --- a/pnpm-lock.yaml
259
+ +++ b/pnpm-lock.yaml
260
+ @@ -44,9 +44,6 @@ importers:
261
+ dependencies:
262
+ - zod:
263
+ - specifier: ^3.25.76
264
+ - version: 3.25.76
265
+ @@ -862,4 +859,1 @@ packages:
266
+ - zod@3.25.76:
267
+ - resolution: {integrity: sha512-aaaa}
268
+ -
269
+ @@ -3694,4 +3688,1 @@ snapshots:
270
+ - zod@3.25.76: {}
271
+ -
272
+ `;
273
+ // Same drop, plus an ADDED peer-metadata child naming the dropped package — the
274
+ // second laundering vector (an added-name subtraction excluded the candidate).
275
+ const ZOD_PIN_REMOVAL_WITH_ADDED_PEER_META_DIFF = `${ZOD_PIN_REMOVAL_DIFF}@@ -900,3 +900,5 @@ packages:
276
+ ajv@8.18.0:
277
+ resolution: {integrity: sha512-bbbb}
278
+ + peerDependenciesMeta:
279
+ + zod:
280
+ + optional: true
281
+ `;
282
+ const HEAD_LOCKFILE_ZOD_ONLY_IN_PEER_META = `lockfileVersion: '9.0'
283
+
284
+ importers:
285
+
286
+ .:
287
+ dependencies:
288
+ ajv:
289
+ specifier: ^8.18.0
290
+ version: 8.18.0
291
+
292
+ packages:
293
+
294
+ ajv@8.18.0:
295
+ resolution: {integrity: sha512-bbbb}
296
+ peerDependencies:
297
+ zod: ^3.25 || ^4.0
298
+ peerDependenciesMeta:
299
+ zod:
300
+ optional: true
301
+
302
+ snapshots:
303
+
304
+ ajv@8.18.0: {}
305
+ `;
306
+ // ── Transitive-drop fixture (falsification round 2) ──
307
+ // A transitive dep leaves the lockfile as a consequence of bumping its parent.
308
+ // A NON-importer manifest (a separately-deployed service) still declares it.
309
+ const HONO_DROP_DIFF = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
310
+ --- a/pnpm-lock.yaml
311
+ +++ b/pnpm-lock.yaml
312
+ @@ -862,4 +859,1 @@ packages:
313
+ - hono@4.12.3:
314
+ - resolution: {integrity: sha512-aaaa}
315
+ -
316
+ @@ -3694,4 +3688,1 @@ snapshots:
317
+ - hono@4.12.3: {}
318
+ -
319
+ `;
320
+ const HEAD_LOCKFILE_WITHOUT_HONO = `lockfileVersion: '9.0'
321
+
322
+ importers:
323
+
324
+ .:
325
+ dependencies:
326
+ '@modelcontextprotocol/sdk':
327
+ specifier: ^1.28.0
328
+ version: 1.28.0
329
+
330
+ packages:
331
+
332
+ '@modelcontextprotocol/sdk@1.28.0':
333
+ resolution: {integrity: sha512-bbbb}
334
+
335
+ snapshots:
336
+
337
+ '@modelcontextprotocol/sdk@1.28.0': {}
338
+ `;
339
+ /** package.json contents at HEAD, keyed by tracked path. */
340
+ const HEAD_PACKAGE_JSONS = {
341
+ 'package.json': `{
342
+ "name": "totem",
343
+ "optionalDependencies": {
344
+ "@mmnto/strategy-doctrine": "0.1.28"
345
+ },
346
+ "dependencies": {
347
+ "ajv": "^8.18.0"
348
+ }
349
+ }`,
350
+ };
351
+ /** The workspace definition most fixtures assume at HEAD. */
352
+ const DEFAULT_WORKSPACE_YAML = `packages:
353
+ - 'packages/*'
354
+ `;
355
+ /**
356
+ * Emulates `git show HEAD:<path>`: the lockfile blob, the workspace definition,
357
+ * or a tracked manifest. A path absent from `manifests` throws, as git does.
358
+ */
359
+ function showAtHead(args, headLockfile, manifests = {}, workspaceYaml = DEFAULT_WORKSPACE_YAML) {
360
+ const ref = args[1] ?? '';
361
+ if (ref === 'HEAD:pnpm-lock.yaml')
362
+ return headLockfile;
363
+ if (ref === 'HEAD:pnpm-workspace.yaml') {
364
+ // `null` models a repo with no workspace file (git exits non-zero).
365
+ if (workspaceYaml === null)
366
+ throw new Error(`fatal: ${ref} does not exist in HEAD`);
367
+ return workspaceYaml;
368
+ }
369
+ const content = manifests[ref.slice('HEAD:'.length)];
370
+ if (content === undefined)
371
+ throw new Error(`fatal: ${ref} does not exist in HEAD`);
372
+ return content;
373
+ }
55
374
  describe('verifyLockfileSyncCommand', () => {
56
375
  beforeEach(() => {
57
376
  vi.resetModules();
58
377
  });
59
378
  afterEach(() => {
60
379
  vi.unstubAllEnvs();
380
+ vi.restoreAllMocks();
61
381
  });
62
382
  it('passes when not inside a git repo', async () => {
63
383
  vi.doMock('@mmnto/totem', async () => {
@@ -100,6 +420,8 @@ describe('verifyLockfileSyncCommand', () => {
100
420
  }
101
421
  if (args[0] === 'diff')
102
422
  return CARET_BUMP_DIFF;
423
+ if (args[0] === 'show')
424
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
103
425
  throw new Error('unexpected');
104
426
  },
105
427
  };
@@ -123,6 +445,8 @@ describe('verifyLockfileSyncCommand', () => {
123
445
  }
124
446
  if (args[0] === 'diff')
125
447
  return NESTED_PKG_BUMP_DIFF;
448
+ if (args[0] === 'show')
449
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
126
450
  throw new Error('unexpected');
127
451
  },
128
452
  };
@@ -130,9 +454,195 @@ describe('verifyLockfileSyncCommand', () => {
130
454
  const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
131
455
  const result = await verifyLockfileSyncCommand();
132
456
  expect(result.valid).toBe(false);
457
+ expect(result.failureClass).toBe('missing-lockfile');
133
458
  expect(result.reason).toMatch(/Tracked lockfile detected/);
134
459
  expect(result.reason).toMatch(/pnpm-lock\.yaml/);
135
460
  });
461
+ it('passes when the pin is added to a NON-importer manifest (test fixture, not a workspace package)', async () => {
462
+ vi.doMock('@mmnto/totem', async () => {
463
+ const actual = await vi.importActual('@mmnto/totem');
464
+ return {
465
+ ...actual,
466
+ resolveGitRoot: () => '/repo',
467
+ getDefaultBranch: () => 'main',
468
+ safeExec: (_cmd, args) => {
469
+ if (args[0] === 'ls-files')
470
+ return 'pnpm-lock.yaml';
471
+ if (args[0] === 'diff' && args.includes('--name-only')) {
472
+ return 'tests/fixtures/sample/package.json';
473
+ }
474
+ if (args[0] === 'show')
475
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
476
+ // Reaching the unified-diff pull would mean the non-importer manifest
477
+ // was still in scope.
478
+ throw new Error('unexpected');
479
+ },
480
+ };
481
+ });
482
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
483
+ const result = await verifyLockfileSyncCommand();
484
+ expect(result.valid).toBe(true);
485
+ });
486
+ it('still fails when a WORKSPACE importer manifest adds a pin without the lockfile', async () => {
487
+ vi.doMock('@mmnto/totem', async () => {
488
+ const actual = await vi.importActual('@mmnto/totem');
489
+ return {
490
+ ...actual,
491
+ resolveGitRoot: () => '/repo',
492
+ getDefaultBranch: () => 'main',
493
+ safeExec: (_cmd, args) => {
494
+ if (args[0] === 'ls-files')
495
+ return 'pnpm-lock.yaml';
496
+ if (args[0] === 'diff' && args.includes('--name-only')) {
497
+ // One importer manifest, one fixture manifest: only the importer
498
+ // may reach the pin scan.
499
+ return 'tests/fixtures/sample/package.json\napps/web/package.json';
500
+ }
501
+ if (args[0] === 'diff') {
502
+ expect(args).toContain('apps/web/package.json');
503
+ expect(args).not.toContain('tests/fixtures/sample/package.json');
504
+ return NESTED_PKG_BUMP_DIFF;
505
+ }
506
+ if (args[0] === 'show')
507
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
508
+ throw new Error('unexpected');
509
+ },
510
+ };
511
+ });
512
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
513
+ const result = await verifyLockfileSyncCommand();
514
+ expect(result.valid).toBe(false);
515
+ expect(result.reason).toMatch(/Tracked lockfile detected/);
516
+ });
517
+ it('fails when a NEW workspace-glob manifest adds a pin without the lockfile', async () => {
518
+ // `packages/new` is not yet an importer at HEAD (that is exactly the shape
519
+ // of a branch adding a package) but its directory matches the workspace
520
+ // `packages:` glob, so the lockfile companion is still required.
521
+ vi.doMock('@mmnto/totem', async () => {
522
+ const actual = await vi.importActual('@mmnto/totem');
523
+ return {
524
+ ...actual,
525
+ resolveGitRoot: () => '/repo',
526
+ getDefaultBranch: () => 'main',
527
+ safeExec: (_cmd, args) => {
528
+ if (args[0] === 'ls-files')
529
+ return 'pnpm-lock.yaml';
530
+ if (args[0] === 'diff' && args.includes('--name-only')) {
531
+ return 'packages/new/package.json';
532
+ }
533
+ if (args[0] === 'diff') {
534
+ expect(args).toContain('packages/new/package.json');
535
+ return NESTED_PKG_BUMP_DIFF;
536
+ }
537
+ if (args[0] === 'show')
538
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
539
+ throw new Error('unexpected');
540
+ },
541
+ };
542
+ });
543
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
544
+ const result = await verifyLockfileSyncCommand();
545
+ expect(result.valid).toBe(false);
546
+ expect(result.failureClass).toBe('missing-lockfile');
547
+ });
548
+ it('honors `!` negations in the workspace globs (an excluded directory is not admitted)', async () => {
549
+ const workspaceWithNegation = `packages:
550
+ - 'packages/*'
551
+ - '!packages/legacy'
552
+ `;
553
+ vi.doMock('@mmnto/totem', async () => {
554
+ const actual = await vi.importActual('@mmnto/totem');
555
+ return {
556
+ ...actual,
557
+ resolveGitRoot: () => '/repo',
558
+ getDefaultBranch: () => 'main',
559
+ safeExec: (_cmd, args) => {
560
+ if (args[0] === 'ls-files')
561
+ return 'pnpm-lock.yaml';
562
+ if (args[0] === 'diff' && args.includes('--name-only')) {
563
+ return 'packages/legacy/package.json';
564
+ }
565
+ if (args[0] === 'show') {
566
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE, {}, workspaceWithNegation);
567
+ }
568
+ // Reaching the unified-diff pull would mean the excluded directory
569
+ // was admitted.
570
+ throw new Error('unexpected');
571
+ },
572
+ };
573
+ });
574
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
575
+ const result = await verifyLockfileSyncCommand();
576
+ expect(result.valid).toBe(true);
577
+ });
578
+ it('admits the ROOT manifest even when the lockfile lists no root importer', async () => {
579
+ const noRootImporter = `lockfileVersion: '9.0'
580
+
581
+ importers:
582
+
583
+ packages/cli:
584
+ dependencies:
585
+ commander:
586
+ specifier: ^11.0.0
587
+ version: 11.0.0
588
+
589
+ packages:
590
+
591
+ commander@11.0.0:
592
+ resolution: {integrity: sha512-bbbb}
593
+ `;
594
+ vi.doMock('@mmnto/totem', async () => {
595
+ const actual = await vi.importActual('@mmnto/totem');
596
+ return {
597
+ ...actual,
598
+ resolveGitRoot: () => '/repo',
599
+ getDefaultBranch: () => 'main',
600
+ safeExec: (_cmd, args) => {
601
+ if (args[0] === 'ls-files')
602
+ return 'pnpm-lock.yaml';
603
+ if (args[0] === 'diff' && args.includes('--name-only'))
604
+ return 'package.json';
605
+ if (args[0] === 'diff')
606
+ return CARET_BUMP_DIFF;
607
+ if (args[0] === 'show')
608
+ return showAtHead(args, noRootImporter);
609
+ throw new Error('unexpected');
610
+ },
611
+ };
612
+ });
613
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
614
+ const result = await verifyLockfileSyncCommand();
615
+ expect(result.valid).toBe(false);
616
+ });
617
+ it('declares a skip of the glob admission when pnpm-workspace.yaml is unreadable at HEAD', async () => {
618
+ const stderr = [];
619
+ vi.spyOn(console, 'error').mockImplementation((...args) => {
620
+ stderr.push(args.map(String).join(' '));
621
+ });
622
+ vi.doMock('@mmnto/totem', async () => {
623
+ const actual = await vi.importActual('@mmnto/totem');
624
+ return {
625
+ ...actual,
626
+ resolveGitRoot: () => '/repo',
627
+ getDefaultBranch: () => 'main',
628
+ safeExec: (_cmd, args) => {
629
+ if (args[0] === 'ls-files')
630
+ return 'pnpm-lock.yaml';
631
+ if (args[0] === 'diff' && args.includes('--name-only')) {
632
+ return 'packages/new/package.json';
633
+ }
634
+ // No workspace file at HEAD → importer-set-only admission.
635
+ if (args[0] === 'show')
636
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE, {}, null);
637
+ throw new Error('unexpected');
638
+ },
639
+ };
640
+ });
641
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
642
+ const result = await verifyLockfileSyncCommand();
643
+ expect(result.valid).toBe(true);
644
+ expect(stderr.join('\n')).toMatch(/pnpm-workspace\.yaml/);
645
+ });
136
646
  it('passes when package.json diff contains only deletions', async () => {
137
647
  vi.doMock('@mmnto/totem', async () => {
138
648
  const actual = await vi.importActual('@mmnto/totem');
@@ -147,6 +657,8 @@ describe('verifyLockfileSyncCommand', () => {
147
657
  return 'package.json';
148
658
  if (args[0] === 'diff')
149
659
  return DELETIONS_ONLY_DIFF;
660
+ if (args[0] === 'show')
661
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
150
662
  throw new Error('unexpected');
151
663
  },
152
664
  };
@@ -170,6 +682,8 @@ describe('verifyLockfileSyncCommand', () => {
170
682
  }
171
683
  if (args[0] === 'diff')
172
684
  return VERSION_FIELD_ONLY_DIFF;
685
+ if (args[0] === 'show')
686
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
173
687
  throw new Error('unexpected');
174
688
  },
175
689
  };
@@ -193,6 +707,8 @@ describe('verifyLockfileSyncCommand', () => {
193
707
  }
194
708
  if (args[0] === 'diff')
195
709
  return WORKSPACE_REF_DIFF;
710
+ if (args[0] === 'show')
711
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
196
712
  throw new Error('unexpected');
197
713
  },
198
714
  };
@@ -236,6 +752,8 @@ describe('verifyLockfileSyncCommand', () => {
236
752
  return 'package.json';
237
753
  if (args[0] === 'diff')
238
754
  return CARET_BUMP_DIFF;
755
+ if (args[0] === 'show')
756
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
239
757
  throw new Error('unexpected');
240
758
  },
241
759
  };
@@ -266,6 +784,566 @@ describe('verifyLockfileSyncCommand', () => {
266
784
  const result = await verifyLockfileSyncCommand();
267
785
  expect(result.valid).toBe(true);
268
786
  });
787
+ // ─── Removed-pin gate (mmnto-ai/totem-strategy#630) ───
788
+ it('fails when the lockfile diff drops every entry for a package still pinned at HEAD (live-fire positive control)', async () => {
789
+ const showRefs = [];
790
+ vi.doMock('@mmnto/totem', async () => {
791
+ const actual = await vi.importActual('@mmnto/totem');
792
+ return {
793
+ ...actual,
794
+ resolveGitRoot: () => '/repo',
795
+ getDefaultBranch: () => 'main',
796
+ safeExec: (_cmd, args) => {
797
+ if (args[0] === 'ls-files')
798
+ return 'pnpm-lock.yaml';
799
+ if (args[0] === 'diff' && args.includes('--name-only'))
800
+ return 'pnpm-lock.yaml';
801
+ if (args[0] === 'diff')
802
+ return DOCTRINE_PIN_REMOVAL_DIFF;
803
+ if (args[0] === 'show') {
804
+ showRefs.push(args[1] ?? '');
805
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, HEAD_PACKAGE_JSONS);
806
+ }
807
+ throw new Error('unexpected');
808
+ },
809
+ };
810
+ });
811
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
812
+ const result = await verifyLockfileSyncCommand();
813
+ expect(result.valid).toBe(false);
814
+ expect(result.failureClass).toBe('removed-pin');
815
+ expect(result.reason).toMatch(/@mmnto\/strategy-doctrine/);
816
+ // The declaration probe reads the ROOT importer's manifest at HEAD — the
817
+ // lockfile's own answer to "which manifests do I resolve?".
818
+ expect(showRefs).toContain('HEAD:pnpm-lock.yaml');
819
+ expect(showRefs).toContain('HEAD:package.json');
820
+ });
821
+ it('counts an optionalDependencies declaration in a NESTED importer manifest as declared', async () => {
822
+ const nestedOnly = {
823
+ 'package.json': `{ "name": "totem" }`,
824
+ 'packages/app/package.json': `{
825
+ "name": "@acme/app",
826
+ "optionalDependencies": {
827
+ "@mmnto/strategy-doctrine": "0.1.28"
828
+ }
829
+ }`,
830
+ };
831
+ vi.doMock('@mmnto/totem', async () => {
832
+ const actual = await vi.importActual('@mmnto/totem');
833
+ return {
834
+ ...actual,
835
+ resolveGitRoot: () => '/repo',
836
+ getDefaultBranch: () => 'main',
837
+ safeExec: (_cmd, args) => {
838
+ if (args[0] === 'ls-files')
839
+ return 'pnpm-lock.yaml';
840
+ if (args[0] === 'diff' && args.includes('--name-only'))
841
+ return 'pnpm-lock.yaml';
842
+ if (args[0] === 'diff')
843
+ return DOCTRINE_PIN_REMOVAL_DIFF;
844
+ if (args[0] === 'show') {
845
+ return showAtHead(args, HEAD_LOCKFILE_WITH_APP_IMPORTER, nestedOnly);
846
+ }
847
+ throw new Error('unexpected');
848
+ },
849
+ };
850
+ });
851
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
852
+ const result = await verifyLockfileSyncCommand();
853
+ expect(result.valid).toBe(false);
854
+ expect(result.reason).toMatch(/@mmnto\/strategy-doctrine/);
855
+ });
856
+ it('passes when a NON-importer manifest declares a dropped transitive dep (services/ repro)', async () => {
857
+ const showRefs = [];
858
+ const manifests = {
859
+ 'package.json': `{
860
+ "name": "totem",
861
+ "dependencies": {
862
+ "@modelcontextprotocol/sdk": "^1.28.0"
863
+ }
864
+ }`,
865
+ // Not a workspace importer: this lockfile never resolved its pins.
866
+ 'services/compile-worker/package.json': `{
867
+ "name": "compile-worker",
868
+ "dependencies": {
869
+ "hono": "^4.12.3"
870
+ }
871
+ }`,
872
+ };
873
+ vi.doMock('@mmnto/totem', async () => {
874
+ const actual = await vi.importActual('@mmnto/totem');
875
+ return {
876
+ ...actual,
877
+ resolveGitRoot: () => '/repo',
878
+ getDefaultBranch: () => 'main',
879
+ safeExec: (_cmd, args) => {
880
+ if (args[0] === 'ls-files')
881
+ return 'pnpm-lock.yaml';
882
+ if (args[0] === 'diff' && args.includes('--name-only'))
883
+ return 'pnpm-lock.yaml';
884
+ if (args[0] === 'diff')
885
+ return HONO_DROP_DIFF;
886
+ if (args[0] === 'show') {
887
+ showRefs.push(args[1] ?? '');
888
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_HONO, manifests);
889
+ }
890
+ throw new Error('unexpected');
891
+ },
892
+ };
893
+ });
894
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
895
+ const result = await verifyLockfileSyncCommand();
896
+ expect(result.valid).toBe(true);
897
+ // The non-importer manifest is never even consulted.
898
+ expect(showRefs).not.toContain('HEAD:services/compile-worker/package.json');
899
+ });
900
+ it('fires when the dropped package survives at HEAD only as a peer-metadata child', async () => {
901
+ const manifests = {
902
+ 'package.json': `{
903
+ "name": "totem",
904
+ "dependencies": {
905
+ "zod": "^3.25.76",
906
+ "ajv": "^8.18.0"
907
+ }
908
+ }`,
909
+ };
910
+ vi.doMock('@mmnto/totem', async () => {
911
+ const actual = await vi.importActual('@mmnto/totem');
912
+ return {
913
+ ...actual,
914
+ resolveGitRoot: () => '/repo',
915
+ getDefaultBranch: () => 'main',
916
+ safeExec: (_cmd, args) => {
917
+ if (args[0] === 'ls-files')
918
+ return 'pnpm-lock.yaml';
919
+ if (args[0] === 'diff' && args.includes('--name-only'))
920
+ return 'pnpm-lock.yaml';
921
+ if (args[0] === 'diff')
922
+ return ZOD_PIN_REMOVAL_DIFF;
923
+ if (args[0] === 'show') {
924
+ return showAtHead(args, HEAD_LOCKFILE_ZOD_ONLY_IN_PEER_META, manifests);
925
+ }
926
+ throw new Error('unexpected');
927
+ },
928
+ };
929
+ });
930
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
931
+ const result = await verifyLockfileSyncCommand();
932
+ expect(result.valid).toBe(false);
933
+ expect(result.reason).toMatch(/zod/);
934
+ });
935
+ it('fires even when the diff ADDS a peer-metadata line naming the dropped package', async () => {
936
+ const manifests = {
937
+ 'package.json': `{
938
+ "name": "totem",
939
+ "dependencies": {
940
+ "zod": "^3.25.76",
941
+ "ajv": "^8.18.0"
942
+ }
943
+ }`,
944
+ };
945
+ vi.doMock('@mmnto/totem', async () => {
946
+ const actual = await vi.importActual('@mmnto/totem');
947
+ return {
948
+ ...actual,
949
+ resolveGitRoot: () => '/repo',
950
+ getDefaultBranch: () => 'main',
951
+ safeExec: (_cmd, args) => {
952
+ if (args[0] === 'ls-files')
953
+ return 'pnpm-lock.yaml';
954
+ if (args[0] === 'diff' && args.includes('--name-only'))
955
+ return 'pnpm-lock.yaml';
956
+ if (args[0] === 'diff')
957
+ return ZOD_PIN_REMOVAL_WITH_ADDED_PEER_META_DIFF;
958
+ if (args[0] === 'show') {
959
+ return showAtHead(args, HEAD_LOCKFILE_ZOD_ONLY_IN_PEER_META, manifests);
960
+ }
961
+ throw new Error('unexpected');
962
+ },
963
+ };
964
+ });
965
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
966
+ const result = await verifyLockfileSyncCommand();
967
+ expect(result.valid).toBe(false);
968
+ expect(result.reason).toMatch(/zod/);
969
+ });
970
+ it('fires on a MASS removal that hides one genuinely dropped, still-declared pin', async () => {
971
+ // The old candidate cap waived the gate on any large diff, so a real drop
972
+ // could ride in behind an ordinary big update. Per-candidate cost is now an
973
+ // O(1) lookup against one lockfile index, so volume buys nothing but false
974
+ // negatives.
975
+ const massRemoval = [
976
+ 'diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml',
977
+ '--- a/pnpm-lock.yaml',
978
+ '+++ b/pnpm-lock.yaml',
979
+ '@@ -1,2000 +1,1 @@ packages:',
980
+ ...Array.from({ length: 900 }, (_v, i) => `- pkg-${i}@1.0.0:`),
981
+ "- '@acme/dropped@2.0.0':",
982
+ '- resolution: {integrity: sha512-aaaa}',
983
+ ].join('\n');
984
+ const manifests = {
985
+ 'package.json': `{
986
+ "name": "totem",
987
+ "dependencies": {
988
+ "@acme/dropped": "^2.0.0"
989
+ }
990
+ }`,
991
+ };
992
+ vi.doMock('@mmnto/totem', async () => {
993
+ const actual = await vi.importActual('@mmnto/totem');
994
+ return {
995
+ ...actual,
996
+ resolveGitRoot: () => '/repo',
997
+ getDefaultBranch: () => 'main',
998
+ safeExec: (_cmd, args) => {
999
+ if (args[0] === 'ls-files')
1000
+ return 'pnpm-lock.yaml';
1001
+ if (args[0] === 'diff' && args.includes('--name-only'))
1002
+ return 'pnpm-lock.yaml';
1003
+ if (args[0] === 'diff')
1004
+ return massRemoval;
1005
+ if (args[0] === 'show') {
1006
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, manifests);
1007
+ }
1008
+ throw new Error('unexpected');
1009
+ },
1010
+ };
1011
+ });
1012
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1013
+ const result = await verifyLockfileSyncCommand();
1014
+ expect(result.valid).toBe(false);
1015
+ expect(result.failureClass).toBe('removed-pin');
1016
+ expect(result.reason).toMatch(/@acme\/dropped/);
1017
+ // The 900 undeclared removals are not named — only the declared one is.
1018
+ expect(result.reason).not.toMatch(/pkg-500/);
1019
+ });
1020
+ it('canonicalizes leading-slash keys emitted by old-vintage (v5/v6) lockfiles', async () => {
1021
+ const v6FormRemoval = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
1022
+ --- a/pnpm-lock.yaml
1023
+ +++ b/pnpm-lock.yaml
1024
+ @@ -1200,6 +1200,1 @@ packages:
1025
+ - /ajv@8.18.0:
1026
+ - resolution: {integrity: sha512-aaaa}
1027
+ - '/@scope/name@1.0.0':
1028
+ - resolution: {integrity: sha512-bbbb}
1029
+ `;
1030
+ const manifests = {
1031
+ 'package.json': `{
1032
+ "name": "totem",
1033
+ "dependencies": {
1034
+ "ajv": "^8.18.0",
1035
+ "@scope/name": "^1.0.0"
1036
+ }
1037
+ }`,
1038
+ };
1039
+ vi.doMock('@mmnto/totem', async () => {
1040
+ const actual = await vi.importActual('@mmnto/totem');
1041
+ return {
1042
+ ...actual,
1043
+ resolveGitRoot: () => '/repo',
1044
+ getDefaultBranch: () => 'main',
1045
+ safeExec: (_cmd, args) => {
1046
+ if (args[0] === 'ls-files')
1047
+ return 'pnpm-lock.yaml';
1048
+ if (args[0] === 'diff' && args.includes('--name-only'))
1049
+ return 'pnpm-lock.yaml';
1050
+ if (args[0] === 'diff')
1051
+ return v6FormRemoval;
1052
+ if (args[0] === 'show') {
1053
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, manifests);
1054
+ }
1055
+ throw new Error('unexpected');
1056
+ },
1057
+ };
1058
+ });
1059
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1060
+ const result = await verifyLockfileSyncCommand();
1061
+ expect(result.valid).toBe(false);
1062
+ // Both key forms canonicalize to the manifest's own spelling.
1063
+ expect(result.reason).toMatch(/(^|[^/])ajv/);
1064
+ expect(result.reason).toMatch(/@scope\/name/);
1065
+ expect(result.reason).not.toMatch(/\/ajv/);
1066
+ expect(result.reason).not.toMatch(/\/@scope/);
1067
+ });
1068
+ it('passes on a pure version bump (the name still resolves at HEAD)', async () => {
1069
+ vi.doMock('@mmnto/totem', async () => {
1070
+ const actual = await vi.importActual('@mmnto/totem');
1071
+ return {
1072
+ ...actual,
1073
+ resolveGitRoot: () => '/repo',
1074
+ getDefaultBranch: () => 'main',
1075
+ safeExec: (_cmd, args) => {
1076
+ if (args[0] === 'ls-files')
1077
+ return 'pnpm-lock.yaml';
1078
+ if (args[0] === 'diff' && args.includes('--name-only'))
1079
+ return 'pnpm-lock.yaml';
1080
+ if (args[0] === 'diff')
1081
+ return DOCTRINE_VERSION_BUMP_DIFF;
1082
+ if (args[0] === 'show') {
1083
+ return showAtHead(args, HEAD_LOCKFILE_WITH_DOCTRINE_29, HEAD_PACKAGE_JSONS);
1084
+ }
1085
+ throw new Error('unexpected');
1086
+ },
1087
+ };
1088
+ });
1089
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1090
+ const result = await verifyLockfileSyncCommand();
1091
+ expect(result.valid).toBe(true);
1092
+ });
1093
+ it('passes on a legitimate removal (the declaration is gone from the importer manifest at HEAD)', async () => {
1094
+ const withoutDeclaration = { 'package.json': `{ "name": "totem", "dependencies": {} }` };
1095
+ vi.doMock('@mmnto/totem', async () => {
1096
+ const actual = await vi.importActual('@mmnto/totem');
1097
+ return {
1098
+ ...actual,
1099
+ resolveGitRoot: () => '/repo',
1100
+ getDefaultBranch: () => 'main',
1101
+ safeExec: (_cmd, args) => {
1102
+ if (args[0] === 'ls-files')
1103
+ return 'pnpm-lock.yaml';
1104
+ if (args[0] === 'diff' && args.includes('--name-only'))
1105
+ return 'pnpm-lock.yaml';
1106
+ if (args[0] === 'diff')
1107
+ return DOCTRINE_PIN_REMOVAL_DIFF;
1108
+ if (args[0] === 'show') {
1109
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, withoutDeclaration);
1110
+ }
1111
+ throw new Error('unexpected');
1112
+ },
1113
+ };
1114
+ });
1115
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1116
+ const result = await verifyLockfileSyncCommand();
1117
+ expect(result.valid).toBe(true);
1118
+ });
1119
+ it('passes on a dedupe (the removed key is gone but the name still resolves at HEAD)', async () => {
1120
+ vi.doMock('@mmnto/totem', async () => {
1121
+ const actual = await vi.importActual('@mmnto/totem');
1122
+ return {
1123
+ ...actual,
1124
+ resolveGitRoot: () => '/repo',
1125
+ getDefaultBranch: () => 'main',
1126
+ safeExec: (_cmd, args) => {
1127
+ if (args[0] === 'ls-files')
1128
+ return 'pnpm-lock.yaml';
1129
+ if (args[0] === 'diff' && args.includes('--name-only'))
1130
+ return 'pnpm-lock.yaml';
1131
+ if (args[0] === 'diff')
1132
+ return DEDUPE_DIFF;
1133
+ // Still declared — only the HEAD-lockfile resolution saves this case.
1134
+ if (args[0] === 'show')
1135
+ return showAtHead(args, HEAD_LOCKFILE_WITH_AJV_8, HEAD_PACKAGE_JSONS);
1136
+ throw new Error('unexpected');
1137
+ },
1138
+ };
1139
+ });
1140
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1141
+ const result = await verifyLockfileSyncCommand();
1142
+ expect(result.valid).toBe(true);
1143
+ });
1144
+ it('passes when the removed name only collides with a MANIFEST key (`type` vs "type": "module")', async () => {
1145
+ // `type` is a real npm package AND a package.json manifest key: membership
1146
+ // must be decided inside the dependency blocks, never by a text match.
1147
+ const typeRemovalDiff = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
1148
+ --- a/pnpm-lock.yaml
1149
+ +++ b/pnpm-lock.yaml
1150
+ @@ -1200,4 +1200,1 @@ packages:
1151
+ - type@2.7.3:
1152
+ - resolution: {integrity: sha512-aaaa}
1153
+ -
1154
+ @@ -4100,4 +4097,1 @@ snapshots:
1155
+ - type@2.7.3: {}
1156
+ -
1157
+ `;
1158
+ const manifestsWithTypeField = {
1159
+ 'package.json': `{
1160
+ "name": "totem",
1161
+ "type": "module",
1162
+ "dependencies": {
1163
+ "ajv": "^8.18.0"
1164
+ }
1165
+ }`,
1166
+ };
1167
+ vi.doMock('@mmnto/totem', async () => {
1168
+ const actual = await vi.importActual('@mmnto/totem');
1169
+ return {
1170
+ ...actual,
1171
+ resolveGitRoot: () => '/repo',
1172
+ getDefaultBranch: () => 'main',
1173
+ safeExec: (_cmd, args) => {
1174
+ if (args[0] === 'ls-files')
1175
+ return 'pnpm-lock.yaml';
1176
+ if (args[0] === 'diff' && args.includes('--name-only'))
1177
+ return 'pnpm-lock.yaml';
1178
+ if (args[0] === 'diff')
1179
+ return typeRemovalDiff;
1180
+ if (args[0] === 'show') {
1181
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, manifestsWithTypeField);
1182
+ }
1183
+ throw new Error('unexpected');
1184
+ },
1185
+ };
1186
+ });
1187
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1188
+ const result = await verifyLockfileSyncCommand();
1189
+ expect(result.valid).toBe(true);
1190
+ });
1191
+ it('does not count an `overrides`-only pin as a dependency declaration', async () => {
1192
+ const overridesOnly = {
1193
+ 'package.json': `{
1194
+ "name": "totem",
1195
+ "pnpm": {
1196
+ "overrides": {
1197
+ "@mmnto/strategy-doctrine": "0.1.28"
1198
+ }
1199
+ }
1200
+ }`,
1201
+ };
1202
+ vi.doMock('@mmnto/totem', async () => {
1203
+ const actual = await vi.importActual('@mmnto/totem');
1204
+ return {
1205
+ ...actual,
1206
+ resolveGitRoot: () => '/repo',
1207
+ getDefaultBranch: () => 'main',
1208
+ safeExec: (_cmd, args) => {
1209
+ if (args[0] === 'ls-files')
1210
+ return 'pnpm-lock.yaml';
1211
+ if (args[0] === 'diff' && args.includes('--name-only'))
1212
+ return 'pnpm-lock.yaml';
1213
+ if (args[0] === 'diff')
1214
+ return DOCTRINE_PIN_REMOVAL_DIFF;
1215
+ if (args[0] === 'show') {
1216
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, overridesOnly);
1217
+ }
1218
+ throw new Error('unexpected');
1219
+ },
1220
+ };
1221
+ });
1222
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1223
+ const result = await verifyLockfileSyncCommand();
1224
+ expect(result.valid).toBe(true);
1225
+ });
1226
+ it('declares a skip for an importer manifest that cannot be parsed at HEAD', async () => {
1227
+ const stderr = [];
1228
+ vi.spyOn(console, 'error').mockImplementation((...args) => {
1229
+ stderr.push(args.map(String).join(' '));
1230
+ });
1231
+ const brokenManifest = {
1232
+ 'package.json': `{ "optionalDependencies": { "@mmnto/strategy-doctrine": "0.1.28" `,
1233
+ };
1234
+ vi.doMock('@mmnto/totem', async () => {
1235
+ const actual = await vi.importActual('@mmnto/totem');
1236
+ return {
1237
+ ...actual,
1238
+ resolveGitRoot: () => '/repo',
1239
+ getDefaultBranch: () => 'main',
1240
+ safeExec: (_cmd, args) => {
1241
+ if (args[0] === 'ls-files')
1242
+ return 'pnpm-lock.yaml';
1243
+ if (args[0] === 'diff' && args.includes('--name-only'))
1244
+ return 'pnpm-lock.yaml';
1245
+ if (args[0] === 'diff')
1246
+ return DOCTRINE_PIN_REMOVAL_DIFF;
1247
+ if (args[0] === 'show') {
1248
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, brokenManifest);
1249
+ }
1250
+ throw new Error('unexpected');
1251
+ },
1252
+ };
1253
+ });
1254
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1255
+ const result = await verifyLockfileSyncCommand();
1256
+ expect(result.valid).toBe(true);
1257
+ // The skip is DECLARED — never silent.
1258
+ expect(stderr.join('\n')).toMatch(/package\.json/);
1259
+ });
1260
+ it('passes when the diff removes a lockfile GRAMMAR key (last devDependency dropped)', async () => {
1261
+ const lastDevDepDiff = `diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
1262
+ --- a/pnpm-lock.yaml
1263
+ +++ b/pnpm-lock.yaml
1264
+ @@ -10,6 +10,1 @@ importers:
1265
+ .:
1266
+ - devDependencies:
1267
+ - vitest:
1268
+ - specifier: ^4.0.0
1269
+ - version: 4.0.0
1270
+ `;
1271
+ const headPkgKeepsEmptyBlock = {
1272
+ 'package.json': `{
1273
+ "name": "totem",
1274
+ "devDependencies": {}
1275
+ }`,
1276
+ };
1277
+ vi.doMock('@mmnto/totem', async () => {
1278
+ const actual = await vi.importActual('@mmnto/totem');
1279
+ return {
1280
+ ...actual,
1281
+ resolveGitRoot: () => '/repo',
1282
+ getDefaultBranch: () => 'main',
1283
+ safeExec: (_cmd, args) => {
1284
+ if (args[0] === 'ls-files')
1285
+ return 'pnpm-lock.yaml';
1286
+ if (args[0] === 'diff' && args.includes('--name-only'))
1287
+ return 'pnpm-lock.yaml';
1288
+ if (args[0] === 'diff')
1289
+ return lastDevDepDiff;
1290
+ if (args[0] === 'show') {
1291
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, headPkgKeepsEmptyBlock);
1292
+ }
1293
+ throw new Error('unexpected');
1294
+ },
1295
+ };
1296
+ });
1297
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1298
+ const result = await verifyLockfileSyncCommand();
1299
+ expect(result.valid).toBe(true);
1300
+ });
1301
+ it('falls through to pass when the lockfile diff read fails (declared skip)', async () => {
1302
+ vi.doMock('@mmnto/totem', async () => {
1303
+ const actual = await vi.importActual('@mmnto/totem');
1304
+ return {
1305
+ ...actual,
1306
+ resolveGitRoot: () => '/repo',
1307
+ getDefaultBranch: () => 'main',
1308
+ safeExec: (_cmd, args) => {
1309
+ if (args[0] === 'ls-files')
1310
+ return 'pnpm-lock.yaml';
1311
+ if (args[0] === 'diff' && args.includes('--name-only'))
1312
+ return 'pnpm-lock.yaml';
1313
+ if (args[0] === 'diff')
1314
+ throw new Error('fatal: bad object');
1315
+ throw new Error('unexpected');
1316
+ },
1317
+ };
1318
+ });
1319
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1320
+ const result = await verifyLockfileSyncCommand();
1321
+ expect(result.valid).toBe(true);
1322
+ });
1323
+ it('falls through to pass when the HEAD-lockfile probe fails (declared skip)', async () => {
1324
+ vi.doMock('@mmnto/totem', async () => {
1325
+ const actual = await vi.importActual('@mmnto/totem');
1326
+ return {
1327
+ ...actual,
1328
+ resolveGitRoot: () => '/repo',
1329
+ getDefaultBranch: () => 'main',
1330
+ safeExec: (_cmd, args) => {
1331
+ if (args[0] === 'ls-files')
1332
+ return 'pnpm-lock.yaml';
1333
+ if (args[0] === 'diff' && args.includes('--name-only'))
1334
+ return 'pnpm-lock.yaml';
1335
+ if (args[0] === 'diff')
1336
+ return DOCTRINE_PIN_REMOVAL_DIFF;
1337
+ if (args[0] === 'show')
1338
+ throw new Error('fatal: path does not exist in HEAD');
1339
+ throw new Error('unexpected');
1340
+ },
1341
+ };
1342
+ });
1343
+ const { verifyLockfileSyncCommand } = await import('./verify-lockfile-sync.js');
1344
+ const result = await verifyLockfileSyncCommand();
1345
+ expect(result.valid).toBe(true);
1346
+ });
269
1347
  it('falls through to pass when both origin/<base> and local <base> diff lookups fail', async () => {
270
1348
  vi.doMock('@mmnto/totem', async () => {
271
1349
  const actual = await vi.importActual('@mmnto/totem');
@@ -305,6 +1383,8 @@ describe('verifyLockfileSyncCliCommand', () => {
305
1383
  return 'package.json';
306
1384
  if (args[0] === 'diff')
307
1385
  return CARET_BUMP_DIFF;
1386
+ if (args[0] === 'show')
1387
+ return showAtHead(args, HEAD_LOCKFILE_WORKSPACE);
308
1388
  throw new Error('unexpected');
309
1389
  },
310
1390
  };
@@ -322,6 +1402,38 @@ describe('verifyLockfileSyncCliCommand', () => {
322
1402
  expect(err.recoveryHint).toMatch(/pnpm install/);
323
1403
  }
324
1404
  });
1405
+ it('carries the removed-pin recovery hint (distinct from the pin-add hint) on the #630 failure class', async () => {
1406
+ vi.doMock('@mmnto/totem', async () => {
1407
+ const actual = await vi.importActual('@mmnto/totem');
1408
+ return {
1409
+ ...actual,
1410
+ resolveGitRoot: () => '/repo',
1411
+ getDefaultBranch: () => 'main',
1412
+ safeExec: (_cmd, args) => {
1413
+ if (args[0] === 'ls-files')
1414
+ return 'pnpm-lock.yaml';
1415
+ if (args[0] === 'diff' && args.includes('--name-only'))
1416
+ return 'pnpm-lock.yaml';
1417
+ if (args[0] === 'diff')
1418
+ return DOCTRINE_PIN_REMOVAL_DIFF;
1419
+ if (args[0] === 'show') {
1420
+ return showAtHead(args, HEAD_LOCKFILE_WITHOUT_DOCTRINE, HEAD_PACKAGE_JSONS);
1421
+ }
1422
+ throw new Error('unexpected');
1423
+ },
1424
+ };
1425
+ });
1426
+ const { verifyLockfileSyncCliCommand } = await import('./verify-lockfile-sync.js');
1427
+ const thrown = await verifyLockfileSyncCliCommand().then(() => null, (err) => err);
1428
+ expect(thrown).toBeInstanceOf(Error);
1429
+ const hint = thrown.recoveryHint ?? '';
1430
+ expect(hint).toMatch(/npm whoami/);
1431
+ expect(hint).toMatch(/pnpm update/);
1432
+ expect(hint).toMatch(/--lockfile-only/);
1433
+ // The two failure classes must not share a remedy: this one must NOT send
1434
+ // the user to the plain `pnpm install` the pin-add class prescribes.
1435
+ expect(hint).not.toMatch(/Run `pnpm install` from the repo root/);
1436
+ });
325
1437
  it('resolves cleanly when the underlying check passes', async () => {
326
1438
  vi.doMock('@mmnto/totem', async () => {
327
1439
  const actual = await vi.importActual('@mmnto/totem');