declapract-typescript-ehmpathy 0.49.14 → 0.49.15

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 (53) hide show
  1. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/declapract.use.yml +4 -0
  2. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/package.json +17 -0
  3. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/left-pad@1.3.0.patch +6 -0
  4. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/pg-pool@3.14.0.patch +12 -0
  5. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/__snapshots__/pg-pool-patch.declapract.integration.test.ts.snap +147 -0
  6. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/best-practice/.declapract.readme.md +22 -0
  7. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/best-practice/package.json +5 -0
  8. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/best-practice/patches/pg-pool.patch +21 -0
  9. package/dist/.test/assets/cicd-service/declarations/practices/persist-with-rds/pg-pool-patch.declapract.integration.test.ts +884 -0
  10. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
  11. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
  12. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
  13. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
  14. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
  15. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
  16. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
  17. package/dist/.test/assets/expo-fullusecase/declarations/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
  18. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
  19. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
  20. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
  21. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
  22. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
  23. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
  24. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
  25. package/dist/.test/assets/expo-migration/declarations/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
  26. package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
  27. package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
  28. package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
  29. package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
  30. package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
  31. package/dist/.test/assets/pnpm/declarations/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
  32. package/dist/.test/assets/pnpm/declarations/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
  33. package/dist/.test/assets/pnpm/declarations/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
  34. package/dist/__snapshots__/typecheckBestPracticeTemplates.declapract.integration.test.ts.snap +80 -0
  35. package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/declapract.use.yml +4 -0
  36. package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/package.json +17 -0
  37. package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/left-pad@1.3.0.patch +6 -0
  38. package/dist/practices/persist-with-rds/.test/assets/repo-with-hand-patch/patches/pg-pool@3.14.0.patch +12 -0
  39. package/dist/practices/persist-with-rds/__snapshots__/pg-pool-patch.declapract.integration.test.ts.snap +147 -0
  40. package/dist/practices/persist-with-rds/best-practice/.declapract.readme.md +22 -0
  41. package/dist/practices/persist-with-rds/best-practice/package.json +5 -0
  42. package/dist/practices/persist-with-rds/best-practice/patches/pg-pool.patch +21 -0
  43. package/dist/practices/persist-with-rds/pg-pool-patch.declapract.integration.test.ts +884 -0
  44. package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/declapract.use.yml +4 -0
  45. package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/package.json +15 -0
  46. package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy.patch +6 -0
  47. package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/rhachet-roles-ehmpathy@1.39.1.patch +6 -0
  48. package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/patches/type-fns@1.21.3.patch +6 -0
  49. package/dist/practices/pnpm/.test/assets/repo-stale-patch-beside-bare/pnpm-lock.yaml +19 -0
  50. package/dist/practices/pnpm/__snapshots__/stale-patched-dependency.declapract.integration.test.ts.snap +39 -0
  51. package/dist/practices/pnpm/stale-patched-dependency.declapract.integration.test.ts +53 -0
  52. package/dist/typecheckBestPracticeTemplates.declapract.integration.test.ts +181 -19
  53. package/package.json +6 -6
@@ -0,0 +1,4 @@
1
+ # .note = declarations dir is symlinked by the test to src/.test/assets/pnpm/declarations
2
+ # .why = a repo with one patch the lockfile moved past and one it still resolves (#680)
3
+ declarations: 'declarations/declapract.declare.yml'
4
+ useCase: pnpm-repo
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "repo-stale-patch-beside-bare",
3
+ "version": "1.0.0",
4
+ "packageManager": "pnpm@10.34.5",
5
+ "pnpm": {
6
+ "overrides": {
7
+ "react-refresh": "0.14.2"
8
+ },
9
+ "patchedDependencies": {
10
+ "rhachet-roles-ehmpathy": "patches/rhachet-roles-ehmpathy.patch",
11
+ "rhachet-roles-ehmpathy@1.39.1": "patches/rhachet-roles-ehmpathy@1.39.1.patch",
12
+ "type-fns@1.21.3": "patches/type-fns@1.21.3.patch"
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ diff --git a/dist/index.js b/dist/index.js
2
+ --- a/dist/index.js
3
+ +++ b/dist/index.js
4
+ @@ -1 +1 @@
5
+ -module.exports = {};
6
+ +module.exports = { patched: true };
@@ -0,0 +1,6 @@
1
+ diff --git a/dist/index.js b/dist/index.js
2
+ --- a/dist/index.js
3
+ +++ b/dist/index.js
4
+ @@ -1 +1 @@
5
+ -module.exports = {};
6
+ +module.exports = { patched: true };
@@ -0,0 +1,6 @@
1
+ diff --git a/dist/index.js b/dist/index.js
2
+ --- a/dist/index.js
3
+ +++ b/dist/index.js
4
+ @@ -1 +1 @@
5
+ -module.exports = {};
6
+ +module.exports = { patched: true };
@@ -0,0 +1,19 @@
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ packages:
8
+
9
+ rhachet-roles-ehmpathy@1.40.0:
10
+ resolution: {integrity: sha512-a}
11
+
12
+ type-fns@1.21.3:
13
+ resolution: {integrity: sha512-b}
14
+
15
+ snapshots:
16
+
17
+ rhachet-roles-ehmpathy@1.40.0: {}
18
+
19
+ type-fns@1.21.3: {}
@@ -36,3 +36,42 @@ exports[`pnpm — a stale patch never blocks an install given: [case1] a repo wi
36
36
  }
37
37
  "
38
38
  `;
39
+
40
+ exports[`pnpm — a stale patch never blocks an install given: [case2] a repo with a bare-name patch key beside a stale exact key of the same package when: [t0] the pnpm practice is applied then: the before + after contents match snapshot: package.json — after 1`] = `
41
+ "{
42
+ "name": "repo-stale-patch-beside-bare",
43
+ "version": "1.0.0",
44
+ "packageManager": "pnpm@10.34.5",
45
+ "pnpm": {
46
+ "overrides": {
47
+ "react-refresh": "0.14.2"
48
+ },
49
+ "patchedDependencies": {
50
+ "rhachet-roles-ehmpathy": "patches/rhachet-roles-ehmpathy.patch",
51
+ "type-fns@1.21.3": "patches/type-fns@1.21.3.patch"
52
+ },
53
+ "allowUnusedPatches": true,
54
+ "ignorePatchFailures": true
55
+ }
56
+ }
57
+ "
58
+ `;
59
+
60
+ exports[`pnpm — a stale patch never blocks an install given: [case2] a repo with a bare-name patch key beside a stale exact key of the same package when: [t0] the pnpm practice is applied then: the before + after contents match snapshot: package.json — before 1`] = `
61
+ "{
62
+ "name": "repo-stale-patch-beside-bare",
63
+ "version": "1.0.0",
64
+ "packageManager": "pnpm@10.34.5",
65
+ "pnpm": {
66
+ "overrides": {
67
+ "react-refresh": "0.14.2"
68
+ },
69
+ "patchedDependencies": {
70
+ "rhachet-roles-ehmpathy": "patches/rhachet-roles-ehmpathy.patch",
71
+ "rhachet-roles-ehmpathy@1.39.1": "patches/rhachet-roles-ehmpathy@1.39.1.patch",
72
+ "type-fns@1.21.3": "patches/type-fns@1.21.3.patch"
73
+ }
74
+ }
75
+ }
76
+ "
77
+ `;
@@ -79,4 +79,57 @@ describe('pnpm — a stale patch never blocks an install', () => {
79
79
  });
80
80
  });
81
81
  });
82
+
83
+ given('[case2] a repo with a bare-name patch key beside a stale exact key of the same package', () => {
84
+ // the shape persist-with-rds ships for pg-pool: a bare key, which no lockfile can move past
85
+ const tempDir = genTempRepoWithDeclarationsOutside({
86
+ slug: 'pnpm-stale-patch-beside-bare',
87
+ clone: './src/practices/pnpm/.test/assets/repo-stale-patch-beside-bare',
88
+ declarations: './src/.test/assets/pnpm/declarations',
89
+ symlink: [{ at: 'node_modules', to: 'node_modules' }],
90
+ });
91
+ const stalePatch = 'patches/rhachet-roles-ehmpathy@1.39.1.patch';
92
+ const barePatch = 'patches/rhachet-roles-ehmpathy.patch';
93
+
94
+ const state = useBeforeAll(async () => {
95
+ const config = path.join(tempDir, 'declapract.use.yml');
96
+ const read = () =>
97
+ fs.readFile(path.join(tempDir, 'package.json'), 'utf-8');
98
+ const before = await read();
99
+ await executeApply({ config });
100
+ const after1 = await read();
101
+ const exists1 = {
102
+ stale: existsSync(path.join(tempDir, stalePatch)),
103
+ bare: existsSync(path.join(tempDir, barePatch)),
104
+ };
105
+ await executeApply({ config });
106
+ const after2 = await read();
107
+ return { before, after1, after2, exists1 };
108
+ }, 170_000);
109
+
110
+ when('[t0] the pnpm practice is applied', () => {
111
+ then('the stale exact key is retired; the bare-name key is kept', () => {
112
+ const patched = JSON.parse(state.after1).pnpm.patchedDependencies;
113
+ expect(patched).toEqual({
114
+ 'rhachet-roles-ehmpathy': barePatch,
115
+ 'type-fns@1.21.3': 'patches/type-fns@1.21.3.patch',
116
+ });
117
+ });
118
+
119
+ then('the stale patch file is deleted; the bare-name file is kept', () => {
120
+ expect(state.exists1).toEqual({ stale: false, bare: true });
121
+ });
122
+
123
+ then('the before + after contents match snapshot', () => {
124
+ expect(state.before).toMatchSnapshot('package.json — before');
125
+ expect(state.after1).toMatchSnapshot('package.json — after');
126
+ });
127
+ });
128
+
129
+ when('[t1] the practice is applied a second time', () => {
130
+ then('the second apply is a no-op', () => {
131
+ expect(state.after2).toEqual(state.after1);
132
+ });
133
+ });
134
+ });
82
135
  });
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
 
6
6
  import { executeApply, executePlan } from 'declapract';
7
+ import { ConstraintError, MalfunctionError } from 'helpful-errors';
7
8
  import { genTempDir, given, then, useBeforeAll, when } from 'test-fns';
8
9
 
9
10
  import { asPackageJSON } from './utils/asPackageJSON';
@@ -12,6 +13,10 @@ import { asPackageJSON } from './utils/asPackageJSON';
12
13
  * .what = the pnpm this repo pins in its own `packageManager`, run via npx
13
14
  * .why = a ci test job restores node_modules from cache and puts no pnpm on its PATH, so a bare
14
15
  * `pnpm` spawn fails ENOENT. npx fetches the pinned version, so the run needs no host pnpm
16
+ * .note = `--loglevel=error` sits before the package spec, so it quiets npx's own npm and reaches
17
+ * neither pnpm nor tsc: npx reads the emitted `.npmrc`, and a newer host npm warns on each
18
+ * pnpm-only key in it (`node-linker`, `public-hoist-pattern`), which says naught of the
19
+ * templates and would vary each snapshot with the host's npm
15
20
  */
16
21
  const PNPM_VIA_NPX = (() => {
17
22
  const packageManager: string = asPackageJSON(
@@ -23,7 +28,10 @@ const PNPM_VIA_NPX = (() => {
23
28
  },
24
29
  { practice: 'typecheck-best-practice-templates' },
25
30
  ).packageManager;
26
- return { command: 'npx', argsBefore: ['--yes', packageManager] };
31
+ return {
32
+ command: 'npx',
33
+ argsBefore: ['--yes', '--loglevel=error', packageManager],
34
+ };
27
35
  })();
28
36
 
29
37
  /**
@@ -55,6 +63,12 @@ const PNPM_VIA_NPX = (() => {
55
63
  * .teeth = restore `/\bs3\s*:/` as the s3 key pattern in `cache/bad-practices/cache-directory-options`
56
64
  * — the config template `src/utils/config/getOneCacheDirectory.ts` is flagged in each
57
65
  * usecase that ships it.
66
+ * .what = also runs each usecase's emitted cycle check (`test:lint:cycles`) on the installed tree,
67
+ * and clamps that no cycle runs through pg-pool. dpdm walks node_modules (`exclude: []`),
68
+ * so a cycle in a shipped prod dep reddens every adopter's lint gate at once. every other
69
+ * cycle it finds is held in a snapshot, so a new one shows in review.
70
+ * .teeth = drop the `pg-pool` key from `persist-with-rds/best-practice/package.json`
71
+ * `pnpm.patchedDependencies` — `lambda-service-with-rds` reports `pg -> pg-pool`.
58
72
  * .note = the test lives at `src/` (not under `src/.test/`): jest skips dot dirs, so a test there
59
73
  * would never run.
60
74
  */
@@ -70,6 +84,10 @@ const USECASES = [
70
84
  ] as const;
71
85
  type Usecase = (typeof USECASES)[number];
72
86
 
87
+ // the usecases whose install holds pg-pool; `[t3]` checks this set against each real install, so
88
+ // a usecase that gains pg reddens until it joins, and its pg-pool verdict is never vacuous
89
+ const USECASES_WITH_PG_POOL: readonly Usecase[] = ['lambda-service-with-rds'];
90
+
73
91
  // every `@declapract{variable.*}` any practice in any usecase references
74
92
  const VARIABLES_YML = `
75
93
  variables:
@@ -114,6 +132,8 @@ const runInRepo = (input: {
114
132
  const child = spawn(input.command, input.args, {
115
133
  cwd: input.cwd,
116
134
  env: { ...process.env, CI: 'true', FORCE_COLOR: '0' },
135
+ // a stalled registry kills the child (status null) instead of a silent park of the run
136
+ timeout: 900_000,
117
137
  });
118
138
  const chunks: string[] = [];
119
139
  child.stdout.on('data', (chunk) => chunks.push(String(chunk)));
@@ -144,6 +164,39 @@ const asMaskedOutput = (input: { output: string; dirs: string[] }): string =>
144
164
  input.output,
145
165
  );
146
166
 
167
+ /**
168
+ * .what = the package each segment of one dpdm cycle line sits in, joined in cycle order
169
+ * .why = a raw cycle line names pnpm store dirs whose suffix hashes shift with every peer set;
170
+ * the packages alone are the stable fact a snapshot can hold
171
+ */
172
+ const asCyclePackages = (input: { line: string }): string =>
173
+ input.line
174
+ .split(' -> ')
175
+ .map((segment) => {
176
+ const afterStore = segment.trim().split('node_modules/').pop() ?? '';
177
+ const match = afterStore.match(/^((?:@[^/]+\/)?[^/]+)/);
178
+ return match?.[1] ?? segment.trim();
179
+ })
180
+ .join(' -> ');
181
+
182
+ /**
183
+ * .what = the verdict of one `test:lint:cycles` run: whether dpdm analyzed a tree, and each
184
+ * cycle it found, as package chains
185
+ * .why = a bare repo of a usecase that emits no `src/**\/*.ts` gives dpdm no entry file, which it
186
+ * reports as a failure; that is not a cycle, so the summary keeps the two apart
187
+ */
188
+ const asCycleSummary = (input: {
189
+ output: string;
190
+ }): { analyzed: boolean; cycles: string[] } => ({
191
+ analyzed: input.output.includes('Analyze done!'),
192
+ cycles: input.output
193
+ .split('\n')
194
+ .map((line) => line.match(/^\s*\d+\)\s+(.+)$/)?.[1])
195
+ .filter((line): line is string => line !== undefined)
196
+ .map((line) => asCyclePackages({ line }))
197
+ .sort(), // dpdm reports cycles in traversal order, which shifts between runs
198
+ });
199
+
147
200
  /**
148
201
  * .what = applies one usecase into its temp repo, installs the emitted deps, and compiles
149
202
  * .why = the three steps an adopter takes before their first `tsc`
@@ -155,8 +208,11 @@ const genTypecheckResult = async (input: {
155
208
  }): Promise<{
156
209
  config: string;
157
210
  tsconfigFound: boolean;
211
+ holdsPgPool: boolean;
212
+ srcImportsPg: boolean;
158
213
  install: { status: number | null; output: string };
159
214
  compile: { status: number | null; output: string };
215
+ cycles: { status: number | null; output: string };
160
216
  }> => {
161
217
  // declare the usecase this repo adopts, from declarations that live outside the repo
162
218
  // .note = declapract joins `declarations` onto the config's dir, so the path is relative, and both
@@ -225,17 +281,48 @@ const genTypecheckResult = async (input: {
225
281
  ],
226
282
  });
227
283
 
284
+ const compiledAt = Date.now();
285
+
286
+ // run the emitted cycle check, as the adopter's `npm run test:lint` does
287
+ const cycles = await runInRepo({
288
+ cwd,
289
+ command: PNPM_VIA_NPX.command,
290
+ args: [...PNPM_VIA_NPX.argsBefore, 'run', 'test:lint:cycles'],
291
+ });
292
+
228
293
  // report the per-phase cost, so a slow usecase is visible in the run log
229
294
  console.log(`typecheck-templates ${input.usecase} phase cost`, {
230
295
  applyMs: appliedAt - startedAt,
231
296
  installMs: installedAt - appliedAt,
232
- compileMs: Date.now() - installedAt,
297
+ compileMs: compiledAt - installedAt,
298
+ cyclesMs: Date.now() - compiledAt,
233
299
  });
234
300
 
301
+ // whether the install holds pg-pool at all; pnpm keeps a transitive dep only in its store
302
+ const pnpmStore = path.join(cwd, 'node_modules', '.pnpm');
303
+ const holdsPgPool =
304
+ existsSync(pnpmStore) &&
305
+ (await fs.readdir(pnpmStore)).some((name) => name.startsWith('pg-pool@'));
306
+
307
+ // whether any emitted src file imports pg; without one, dpdm never reaches pg-pool
308
+ const srcDir = path.join(cwd, 'src');
309
+ const srcFiles = existsSync(srcDir)
310
+ ? (await fs.readdir(srcDir, { recursive: true }))
311
+ .map(String)
312
+ .filter((file) => file.endsWith('.ts'))
313
+ : [];
314
+ const srcImportsPg = (
315
+ await Promise.all(
316
+ srcFiles.map((file) => fs.readFile(path.join(srcDir, file), 'utf-8')),
317
+ )
318
+ ).some((contents) => /from 'pg'/.test(contents));
319
+
235
320
  const dirs = [cwd, input.tempDir];
236
321
  return {
237
322
  config,
238
323
  tsconfigFound,
324
+ holdsPgPool,
325
+ srcImportsPg,
239
326
  install: {
240
327
  status: install.status,
241
328
  output: asMaskedOutput({ output: install.output, dirs }),
@@ -244,6 +331,10 @@ const genTypecheckResult = async (input: {
244
331
  status: compile.status,
245
332
  output: asMaskedOutput({ output: compile.output, dirs }),
246
333
  },
334
+ cycles: {
335
+ status: cycles.status,
336
+ output: asMaskedOutput({ output: cycles.output, dirs }),
337
+ },
247
338
  };
248
339
  };
249
340
 
@@ -326,8 +417,8 @@ describe('typecheck best-practice templates', () => {
326
417
  });
327
418
 
328
419
  // read one usecase's result; a usecase whose preparation threw fails each of its thens loud
329
- const getScene = (usecase: Usecase) => {
330
- const result = settled[usecase];
420
+ const getScene = (input: { usecase: Usecase }) => {
421
+ const result = settled[input.usecase];
331
422
  if (result.status === 'rejected') throw result.reason;
332
423
  return result.value;
333
424
  };
@@ -358,8 +449,8 @@ describe('typecheck best-practice templates', () => {
358
449
  });
359
450
 
360
451
  // read one usecase's plan flags; a plan that threw fails each of its thens loud
361
- const getPlan = (usecase: Usecase) => {
362
- const result = plans[usecase];
452
+ const getPlan = (input: { usecase: Usecase }) => {
453
+ const result = plans[input.usecase];
363
454
  if (result.status === 'rejected') throw result.reason;
364
455
  return result.value;
365
456
  };
@@ -368,55 +459,126 @@ describe('typecheck best-practice templates', () => {
368
459
  given(`[usecase] ${usecase} applied to a bare repo`, () => {
369
460
  when('[t0] the emitted deps are installed', () => {
370
461
  then('the install exits 0', () => {
371
- const scene = getScene(usecase);
462
+ const scene = getScene({ usecase });
372
463
  if (scene.install.status !== 0)
373
- throw new Error(
374
- `pnpm install failed for ${usecase} (exit ${scene.install.status}):\n${scene.install.output}`,
375
- );
464
+ throw new MalfunctionError(`pnpm install failed for ${usecase}`, {
465
+ status: scene.install.status,
466
+ output: scene.install.output,
467
+ hint: 'read the install output above; a registry or cache fault is the usual cause',
468
+ });
376
469
  expect(scene.install.status).toEqual(0);
377
470
  });
378
471
  });
379
472
 
380
473
  when('[t1] the emitted tree is compiled with the emitted tsconfig', () => {
381
474
  then('the usecase emits a tsconfig.json', () => {
382
- expect(getScene(usecase).tsconfigFound).toEqual(true);
475
+ expect(getScene({ usecase }).tsconfigFound).toEqual(true);
383
476
  });
384
477
 
385
478
  then('the tsc diagnostics match snapshot', () => {
386
- expect(getScene(usecase).compile.output).toMatchSnapshot(
479
+ expect(getScene({ usecase }).compile.output).toMatchSnapshot(
387
480
  `${usecase} — tsc --noEmit diagnostics`,
388
481
  );
389
482
  });
390
483
 
391
484
  then('tsc --noEmit exits 0', () => {
392
- const scene = getScene(usecase);
485
+ const scene = getScene({ usecase });
393
486
  if (scene.compile.status !== 0)
394
- throw new Error(
395
- `tsc --noEmit failed for ${usecase} (exit ${scene.compile.status}):\n${scene.compile.output}`,
396
- );
487
+ throw new ConstraintError(`tsc --noEmit failed for ${usecase}`, {
488
+ status: scene.compile.status,
489
+ output: scene.compile.output,
490
+ hint: 'a template the usecase ships does not compile; the diagnostics name it',
491
+ });
397
492
  expect(scene.compile.status).toEqual(0);
398
493
  });
399
494
  });
400
495
 
401
496
  when('[t2] the twice-applied repo is planned again', () => {
402
497
  then('no file is flagged for an automatic fix (the apply converges)', () => {
403
- expect(getPlan(usecase).automatic).toEqual([]);
498
+ expect(getPlan({ usecase }).automatic).toEqual([]);
404
499
  });
405
500
 
406
501
  then('no file the apply wrote is flagged by its own usecase', () => {
407
502
  const cwd = realpathSync(tempDirs[usecase].repo);
408
- const flaggedWritten = getPlan(usecase).manual.filter((file) =>
503
+ const flaggedWritten = getPlan({ usecase }).manual.filter((file) =>
409
504
  existsSync(path.join(cwd, file)),
410
505
  );
411
506
  expect(flaggedWritten).toEqual([]);
412
507
  });
413
508
 
414
509
  then('the files left for a human to author match snapshot', () => {
415
- expect(getPlan(usecase).manual).toMatchSnapshot(
510
+ expect(getPlan({ usecase }).manual).toMatchSnapshot(
416
511
  `${usecase} — files flagged for a human`,
417
512
  );
418
513
  });
419
514
  });
515
+
516
+ when('[t3] the emitted cycle check runs, as `npm run test:lint` does', () => {
517
+ then('the install holds pg-pool exactly where the usecase is declared to', () => {
518
+ const scene = getScene({ usecase });
519
+ const declared = USECASES_WITH_PG_POOL.includes(usecase);
520
+ if (scene.holdsPgPool !== declared)
521
+ throw new ConstraintError(`pg-pool presence drifted for ${usecase}`, {
522
+ holdsPgPool: scene.holdsPgPool,
523
+ declared,
524
+ hint: 'update USECASES_WITH_PG_POOL to match the install, so the pg-pool verdict below stays real',
525
+ });
526
+ expect(scene.holdsPgPool).toEqual(declared);
527
+ });
528
+
529
+ then('it reports no cycle through pg-pool', () => {
530
+ const { cycles } = asCycleSummary({
531
+ output: getScene({ usecase }).cycles.output,
532
+ });
533
+ const cyclesThroughPgPool = cycles.filter((cycle) =>
534
+ cycle.includes('pg-pool'),
535
+ );
536
+ if (cyclesThroughPgPool.length > 0)
537
+ throw new ConstraintError(`pg-pool cycle found for ${usecase}`, {
538
+ cyclesThroughPgPool,
539
+ hint: 'the shipped patches/pg-pool.patch did not apply; check its key in persist-with-rds package.json, and whether pg-pool moved the line (pnpm warns by name)',
540
+ });
541
+ expect(cyclesThroughPgPool).toEqual([]);
542
+ });
543
+
544
+ if (USECASES_WITH_PG_POOL.includes(usecase))
545
+ then('it analyzes the tree, so the pg-pool verdict is real', () => {
546
+ expect(
547
+ asCycleSummary({ output: getScene({ usecase }).cycles.output })
548
+ .analyzed,
549
+ ).toEqual(true);
550
+ });
551
+
552
+ if (USECASES_WITH_PG_POOL.includes(usecase))
553
+ then('its emitted src imports pg, so dpdm can reach pg-pool', () => {
554
+ expect(getScene({ usecase }).srcImportsPg).toEqual(true);
555
+ });
556
+
557
+ // a drift in dpdm's cycle line format would parse to zero cycles and pass the pg-pool verdict blind
558
+ then('the parsed cycles agree with the verdict dpdm prints', () => {
559
+ const { output } = getScene({ usecase }).cycles;
560
+ const summary = asCycleSummary({ output });
561
+ if (!summary.analyzed) return expect(summary.cycles).toEqual([]);
562
+ const noneFound = output.includes('no circular dependency was found');
563
+ if (noneFound !== (summary.cycles.length === 0))
564
+ throw new MalfunctionError(`cycle parse drifted for ${usecase}`, {
565
+ noneFound,
566
+ cyclesParsed: summary.cycles.length,
567
+ hint: 'dpdm changed its cycle line format; update the line pattern in asCycleSummary',
568
+ });
569
+ expect(noneFound).toEqual(summary.cycles.length === 0);
570
+ });
571
+
572
+ // .note = the exit status rides in the snapshot, so a pass/fail flip shows in review. it is not
573
+ // asserted 0: a bare repo with no src entry, and cycles at floor versions outside pg-pool,
574
+ // each exit the check 1 for causes this clamp does not own
575
+ then('the exit status and the cycles it finds match snapshot', () => {
576
+ expect({
577
+ status: getScene({ usecase }).cycles.status,
578
+ ...asCycleSummary({ output: getScene({ usecase }).cycles.output }),
579
+ }).toMatchSnapshot(`${usecase} — test:lint:cycles summary`);
580
+ });
581
+ });
420
582
  });
421
583
  });
422
584
  });
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "author": "ehmpathy",
4
4
  "description": "declapract best practices declarations for typescript",
5
5
  "private": false,
6
- "version": "0.49.14",
6
+ "version": "0.49.15",
7
7
  "license": "MIT",
8
8
  "main": "src/index.js",
9
9
  "repository": "ehmpathy/declapract-typescript-ehmpathy",
@@ -77,15 +77,15 @@
77
77
  "esbuild-register": "3.6.0",
78
78
  "husky": "8.0.3",
79
79
  "jest": "30.2.0",
80
- "rhachet": "1.49.2",
80
+ "rhachet": "1.50.1",
81
81
  "rhachet-brains-anthropic": "0.4.3",
82
82
  "rhachet-brains-fireworksai": "0.2.3",
83
- "rhachet-brains-openrouter": "0.1.3",
83
+ "rhachet-brains-openrouter": "0.1.4",
84
84
  "rhachet-brains-xai": "0.3.3",
85
- "rhachet-roles-bhrain": "0.39.3",
85
+ "rhachet-roles-bhrain": "0.42.1",
86
86
  "rhachet-roles-bhrowser": "0.1.0",
87
- "rhachet-roles-bhuild": "0.23.2",
88
- "rhachet-roles-ehmpathy": "1.39.5",
87
+ "rhachet-roles-bhuild": "0.23.3",
88
+ "rhachet-roles-ehmpathy": "1.39.9",
89
89
  "rhachet-roles-ghlitch": "0.3.0",
90
90
  "rhachet-roles-rhachet": "0.1.7",
91
91
  "sdk-aws-lambda": "0.7.2",