@suss/packs 0.33.0 → 0.34.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 (63) hide show
  1. package/README.md +10 -11
  2. package/dist/activerecord.d.ts +7 -0
  3. package/dist/activerecord.js +34 -4
  4. package/dist/apollo-client.js +26 -39
  5. package/dist/apollo.js +12 -20
  6. package/dist/aws-dynamodb.js +4 -4
  7. package/dist/aws-eventbridge.js +3 -3
  8. package/dist/aws-lambda.js +31 -54
  9. package/dist/aws-s3.js +1 -1
  10. package/dist/aws-secrets-manager.js +1 -1
  11. package/dist/aws-sns.js +2 -2
  12. package/dist/aws-sqs.js +20 -11
  13. package/dist/aws-ssm.js +1 -1
  14. package/dist/axios.js +13 -27
  15. package/dist/bigquery-python.js +3 -3
  16. package/dist/bigquery.js +1 -1
  17. package/dist/{chunk-NYWZHQBJ.js → chunk-GK4BILHB.js} +8 -9
  18. package/dist/cloudflare-workers.js +26 -35
  19. package/dist/drizzle.js +15 -20
  20. package/dist/express.js +3 -6
  21. package/dist/fabrication.d.ts +1 -0
  22. package/dist/fabrication.js +36 -0
  23. package/dist/factory-bot.d.ts +1 -0
  24. package/dist/factory-bot.js +36 -0
  25. package/dist/faraday.js +4 -5
  26. package/dist/fastapi.js +21 -5
  27. package/dist/fastify.js +10 -14
  28. package/dist/fetch.js +3 -4
  29. package/dist/flask-restx.js +19 -12
  30. package/dist/gcs.js +1 -1
  31. package/dist/graphql-ruby.js +20 -16
  32. package/dist/hono.js +17 -27
  33. package/dist/httpx.js +3 -5
  34. package/dist/minitest.d.ts +1 -0
  35. package/dist/minitest.js +59 -0
  36. package/dist/mongoose.js +7 -7
  37. package/dist/nestjs-graphql.js +14 -25
  38. package/dist/nestjs-microservices.js +9 -10
  39. package/dist/nestjs-rest.js +42 -22
  40. package/dist/net-http.js +5 -6
  41. package/dist/nextjs.js +16 -17
  42. package/dist/node.js +144 -28
  43. package/dist/package-exports.js +1 -1
  44. package/dist/pg.js +3 -2
  45. package/dist/prisma.js +281 -48
  46. package/dist/pytest.d.ts +1 -0
  47. package/dist/pytest.js +81 -0
  48. package/dist/rails.js +46 -27
  49. package/dist/react-query.js +1 -1
  50. package/dist/react-router.js +21 -48
  51. package/dist/react.js +14 -5
  52. package/dist/redis.js +1 -1
  53. package/dist/requests.js +4 -5
  54. package/dist/rspec.d.ts +1 -0
  55. package/dist/rspec.js +135 -0
  56. package/dist/sqlalchemy.js +1 -1
  57. package/dist/sqlmodel.js +7 -8
  58. package/dist/ts-rest.js +6 -8
  59. package/dist/typeorm.d.ts +1 -0
  60. package/dist/typeorm.js +291 -0
  61. package/dist/vitest.d.ts +1 -0
  62. package/dist/vitest.js +64 -0
  63. package/package.json +47 -11
package/dist/prisma.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // ../framework/prisma/dist/index.js
2
- import fs from "fs";
3
- import path from "path";
2
+ import fs2 from "fs";
3
+ import path2 from "path";
4
4
  import {
5
5
  Node as N
6
6
  } from "ts-morph";
7
7
  import { z } from "zod";
8
+ import { receiverTypesOf, writtenNodeOf } from "@suss/adapter-typescript";
8
9
  import { storageBinding } from "@suss/behavioral-ir";
9
10
  import { scopeOption, storageSystemOption } from "@suss/extractor";
10
11
  import {
@@ -13,6 +14,191 @@ import {
13
14
  declaredBy,
14
15
  sqlStatements
15
16
  } from "@suss/recognize";
17
+ import fs from "fs";
18
+ import path from "path";
19
+ import { getSchema } from "@mrleebo/prisma-ast";
20
+ import { ts } from "ts-morph";
21
+ var CONFIG_FILES = [
22
+ "prisma.config.ts",
23
+ "prisma.config.mts",
24
+ "prisma.config.cts",
25
+ "prisma.config.js",
26
+ "prisma.config.mjs",
27
+ "prisma.config.cjs",
28
+ path.join(".config", "prisma.ts")
29
+ ];
30
+ var DEFAULT_SCHEMAS = [path.join("prisma", "schema.prisma"), "schema.prisma"];
31
+ var readProjects = /* @__PURE__ */ new Map();
32
+ function generatedClientDirs(fromDir) {
33
+ const root = projectRootOf(fromDir);
34
+ if (root === null) {
35
+ return [];
36
+ }
37
+ const known = readProjects.get(root);
38
+ if (known !== void 0 && unchanged(known.readFrom)) {
39
+ return known.dirs;
40
+ }
41
+ const schemaFiles = schemaFilesOf(root);
42
+ const readFrom = /* @__PURE__ */ new Map();
43
+ for (const file of [...projectFilesOf(root), ...schemaFiles]) {
44
+ readFrom.set(file, changedAt(file));
45
+ }
46
+ const dirs = schemaFiles.flatMap(outputDirsIn);
47
+ readProjects.set(root, { dirs, readFrom });
48
+ return dirs;
49
+ }
50
+ function isInGeneratedClient(filePath) {
51
+ return generatedClientDirs(path.dirname(filePath)).some(
52
+ (dir) => filePath.startsWith(`${dir}${path.sep}`)
53
+ );
54
+ }
55
+ function projectFilesOf(root) {
56
+ return [
57
+ path.join(root, "package.json"),
58
+ ...CONFIG_FILES.map((name) => path.join(root, name)),
59
+ ...DEFAULT_SCHEMAS.map((name) => path.join(root, name))
60
+ ];
61
+ }
62
+ function changedAt(file) {
63
+ try {
64
+ return fs.statSync(file).mtimeMs;
65
+ } catch {
66
+ return -1;
67
+ }
68
+ }
69
+ function unchanged(readFrom) {
70
+ for (const [file, at] of readFrom) {
71
+ if (changedAt(file) !== at) {
72
+ return false;
73
+ }
74
+ }
75
+ return true;
76
+ }
77
+ var rootsByDir = /* @__PURE__ */ new Map();
78
+ function projectRootOf(fromDir) {
79
+ const known = rootsByDir.get(fromDir);
80
+ if (known !== void 0) {
81
+ return known;
82
+ }
83
+ const root = findProjectRoot(fromDir);
84
+ rootsByDir.set(fromDir, root);
85
+ return root;
86
+ }
87
+ function findProjectRoot(fromDir) {
88
+ let dir = path.resolve(fromDir);
89
+ while (true) {
90
+ if (fs.existsSync(path.join(dir, "package.json")) || configFileIn(dir) !== null) {
91
+ return dir;
92
+ }
93
+ const parent = path.dirname(dir);
94
+ if (parent === dir) {
95
+ return null;
96
+ }
97
+ dir = parent;
98
+ }
99
+ }
100
+ function configFileIn(dir) {
101
+ for (const name of CONFIG_FILES) {
102
+ const candidate = path.join(dir, name);
103
+ if (fs.existsSync(candidate)) {
104
+ return candidate;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ function schemaFilesOf(root) {
110
+ const stated = [configuredSchema(root), packageJsonSchema(root)];
111
+ for (const schema of [
112
+ ...stated.filter((one) => one !== null),
113
+ ...DEFAULT_SCHEMAS.map((name) => path.join(root, name))
114
+ ]) {
115
+ const files = prismaFilesAt(schema);
116
+ if (files.length > 0) {
117
+ return files;
118
+ }
119
+ }
120
+ return [];
121
+ }
122
+ function prismaFilesAt(schemaPath) {
123
+ let stat;
124
+ try {
125
+ stat = fs.statSync(schemaPath);
126
+ } catch {
127
+ return [];
128
+ }
129
+ if (stat.isFile()) {
130
+ return [schemaPath];
131
+ }
132
+ if (!stat.isDirectory()) {
133
+ return [];
134
+ }
135
+ return fs.readdirSync(schemaPath, { recursive: true, encoding: "utf8" }).filter((name) => name.endsWith(".prisma")).map((name) => path.join(schemaPath, name)).sort();
136
+ }
137
+ function configuredSchema(root) {
138
+ const config = configFileIn(root);
139
+ if (config === null) {
140
+ return null;
141
+ }
142
+ const source = ts.createSourceFile(
143
+ config,
144
+ fs.readFileSync(config, "utf8"),
145
+ ts.ScriptTarget.Latest
146
+ );
147
+ let found = null;
148
+ const visit = (node) => {
149
+ if (found !== null) {
150
+ return;
151
+ }
152
+ if (ts.isPropertyAssignment(node) && node.name.getText(source) === "schema" && ts.isStringLiteralLike(node.initializer)) {
153
+ found = node.initializer.text;
154
+ return;
155
+ }
156
+ ts.forEachChild(node, visit);
157
+ };
158
+ visit(source);
159
+ return found === null ? null : path.resolve(root, found);
160
+ }
161
+ function packageJsonSchema(root) {
162
+ const file = path.join(root, "package.json");
163
+ if (!fs.existsSync(file)) {
164
+ return null;
165
+ }
166
+ try {
167
+ const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
168
+ const schema = manifest.prisma?.schema;
169
+ return typeof schema === "string" ? path.resolve(root, schema) : null;
170
+ } catch {
171
+ return null;
172
+ }
173
+ }
174
+ function outputDirsIn(schemaFile) {
175
+ let list;
176
+ try {
177
+ list = getSchema(fs.readFileSync(schemaFile, "utf8")).list;
178
+ } catch {
179
+ return [];
180
+ }
181
+ const dirs = [];
182
+ for (const block of list) {
183
+ const output = generatorOutput(block);
184
+ if (output !== null) {
185
+ dirs.push(path.resolve(path.dirname(schemaFile), output));
186
+ }
187
+ }
188
+ return dirs;
189
+ }
190
+ function generatorOutput(block) {
191
+ const generator = block;
192
+ if (generator.type !== "generator") {
193
+ return null;
194
+ }
195
+ for (const assignment of generator.assignments ?? []) {
196
+ if (assignment.type === "assignment" && assignment.key === "output" && typeof assignment.value === "string") {
197
+ return assignment.value.replace(/^"|"$/g, "");
198
+ }
199
+ }
200
+ return null;
201
+ }
16
202
  var PRISMA_READ_METHODS = /* @__PURE__ */ new Set([
17
203
  "findUnique",
18
204
  "findFirst",
@@ -33,15 +219,14 @@ var PRISMA_WRITE_METHODS = /* @__PURE__ */ new Set([
33
219
  ]);
34
220
  var optionsSchema = z.object({
35
221
  /**
36
- * Storage system the recognized calls target. Must match the
37
- * `storageSystem` on schema-reader provider summaries; otherwise
38
- * pairing keys won't match. Defaults to `"postgresql"` since that's
39
- * the dominant Prisma deployment.
222
+ * The storage system the calls target, `"postgresql"` when unset. It
223
+ * has to match the `storageSystem` on the schema reader's summaries,
224
+ * or the calls do not pair.
40
225
  */
41
226
  storageSystem: storageSystemOption.optional(),
42
227
  /**
43
- * Scope label that must match the schema reader's scope. Defaults
44
- * to `"default"` to align with `prismaSchemaToSummaries`'s default.
228
+ * Has to match the schema reader's scope. It is `"default"` when
229
+ * unset, the same default `prismaSchemaToSummaries` uses.
45
230
  */
46
231
  scope: scopeOption.optional()
47
232
  }).strict();
@@ -68,7 +253,7 @@ function recognizePrismaCall(call, ctx, storageSystem, scope) {
68
253
  return null;
69
254
  }
70
255
  const receiverExpr = delegateExpr.getExpression();
71
- if (!isPrismaClientReceiver(receiverExpr)) {
256
+ if (!isPrismaClientReceiver(receiverExpr, recognizerCtx.resolution)) {
72
257
  return null;
73
258
  }
74
259
  const delegateName = delegateExpr.getName();
@@ -88,19 +273,24 @@ function recognizePrismaCall(call, ctx, storageSystem, scope) {
88
273
  container: tableName
89
274
  });
90
275
  const callee = callNode.getExpression().getText();
276
+ const access = {
277
+ type: "interaction",
278
+ binding,
279
+ callee,
280
+ interaction: {
281
+ class: "storage-access",
282
+ kind,
283
+ fields,
284
+ ...selector !== null ? { selector } : {},
285
+ operation: method
286
+ }
287
+ };
288
+ statePayloadSlots(recognizerCtx.ops, access, {
289
+ fields: kind === "write" ? fields : [],
290
+ selector: selector ?? []
291
+ });
91
292
  return [
92
- {
93
- type: "interaction",
94
- binding,
95
- callee,
96
- interaction: {
97
- class: "storage-access",
98
- kind,
99
- fields,
100
- ...selector !== null ? { selector } : {},
101
- operation: method
102
- }
103
- },
293
+ access,
104
294
  ...nestedReads(optionsArg).map(
105
295
  (nested) => ({
106
296
  type: "interaction",
@@ -126,15 +316,34 @@ function recognizePrismaCall(call, ctx, storageSystem, scope) {
126
316
  fields: nested.fields,
127
317
  relationPath: nested.relationPath,
128
318
  ...nested.relationKey === true ? { relationKey: true } : {},
129
- // What reaches the other model is the nested operation, so
130
- // that is what the effect records. The outer method belongs
131
- // to the model in the binding.
319
+ // The outer method acts on the model in the binding. The model
320
+ // across the relation gets the nested operation.
132
321
  operation: nested.operation
133
322
  }
134
323
  })
135
324
  )
136
325
  ];
137
326
  }
327
+ function statePayloadSlots(ops, effect, columns) {
328
+ const options = ops?.statesSlots === void 0 ? null : ops.valueAt(0);
329
+ if (ops === void 0 || options === null) {
330
+ return;
331
+ }
332
+ const slots = [
333
+ ...slotsUnder(options, ["where"], "selector", columns.selector),
334
+ ...slotsUnder(options, WRITE_PAYLOAD_KEYS, "field", columns.fields)
335
+ ];
336
+ if (slots.length > 0) {
337
+ ops.statesSlots?.(effect, slots);
338
+ }
339
+ }
340
+ function slotsUnder(options, keys, slot, columns) {
341
+ return keys.flatMap(
342
+ (key) => (options.property(key)?.entries("nothing") ?? []).flatMap(
343
+ (entry) => entry.key !== null && columns.includes(entry.key) ? [{ slot, name: entry.key, value: entry.value }] : []
344
+ )
345
+ );
346
+ }
138
347
  function nestedReads(optionsArg) {
139
348
  if (optionsArg === null) {
140
349
  return [];
@@ -143,7 +352,7 @@ function nestedReads(optionsArg) {
143
352
  collectRelations(optionsArg, [], found);
144
353
  return found;
145
354
  }
146
- function collectRelations(shape, path2, found) {
355
+ function collectRelations(shape, path3, found) {
147
356
  const select = readObjectArg(shape.fields.select);
148
357
  if (select !== null) {
149
358
  for (const [name, value] of Object.entries(select.fields)) {
@@ -151,7 +360,7 @@ function collectRelations(shape, path2, found) {
151
360
  if (nested === null) {
152
361
  continue;
153
362
  }
154
- recordRelation(name, nested, path2, found);
363
+ recordRelation(name, nested, path3, found);
155
364
  }
156
365
  }
157
366
  const include = readObjectArg(shape.fields.include);
@@ -161,14 +370,14 @@ function collectRelations(shape, path2, found) {
161
370
  for (const [name, value] of Object.entries(include.fields)) {
162
371
  const nested = readObjectArg(value);
163
372
  if (nested === null) {
164
- found.push({ relationPath: [...path2, name], fields: ["*"] });
373
+ found.push({ relationPath: [...path3, name], fields: ["*"] });
165
374
  continue;
166
375
  }
167
- recordRelation(name, nested, path2, found);
376
+ recordRelation(name, nested, path3, found);
168
377
  }
169
378
  }
170
- function recordRelation(name, nested, path2, found) {
171
- const relationPath = [...path2, name];
379
+ function recordRelation(name, nested, path3, found) {
380
+ const relationPath = [...path3, name];
172
381
  found.push({ relationPath, fields: extractFields(nested, "read") });
173
382
  collectRelations(nested, relationPath, found);
174
383
  }
@@ -222,13 +431,13 @@ function nestedWrites(optionsArg) {
222
431
  }
223
432
  return found;
224
433
  }
225
- function collectNestedWrites(row, path2, found) {
434
+ function collectNestedWrites(row, path3, found) {
226
435
  for (const [field, value] of Object.entries(row.fields)) {
227
436
  const nested = readObjectArg(value);
228
437
  if (nested === null) {
229
438
  continue;
230
439
  }
231
- const relationPath = [...path2, field];
440
+ const relationPath = [...path3, field];
232
441
  for (const [operation, payload] of Object.entries(nested.fields)) {
233
442
  const does = NESTED_OPERATIONS.get(operation);
234
443
  if (does === void 0) {
@@ -279,29 +488,51 @@ function fieldsOfRows(read) {
279
488
  }
280
489
  return out.size === 0 ? ["*"] : [...out];
281
490
  }
282
- function isPrismaClientReceiver(node) {
283
- return extendsPrismaClient(node.getType(), /* @__PURE__ */ new Set());
491
+ function isPrismaClientReceiver(node, resolution) {
492
+ if (receiverTypesOf(node).some(
493
+ (type) => declaredByPrisma(type) || extendsImportedClient(type, resolution)
494
+ )) {
495
+ return true;
496
+ }
497
+ return node.getType().isAny() && madeFromImportedClient(node, resolution);
284
498
  }
285
- function extendsPrismaClient(type, seen) {
286
- if (seen.has(type)) {
499
+ function madeFromImportedClient(node, resolution) {
500
+ if (resolution === void 0) {
287
501
  return false;
288
502
  }
289
- seen.add(type);
290
- const declarations = type.getSymbol()?.getDeclarations() ?? [];
291
- if (declarations.some(
292
- (declaration) => isPrismaClientPath(declaration.getSourceFile().getFilePath())
293
- )) {
503
+ if (importedAsClient(node, resolution)) {
294
504
  return true;
295
505
  }
296
- return type.getBaseTypes().some((base) => extendsPrismaClient(base, seen));
506
+ const written = writtenNodeOf(node, resolution);
507
+ return written !== null && written !== node && importedAsClient(written, resolution);
508
+ }
509
+ function importedAsClient(value, resolution) {
510
+ return resolution.importOriginsOf(value, clientModulesFor(value)).some((origin) => origin.path.join(".") === CLIENT_CLASS);
511
+ }
512
+ function clientModulesFor(value) {
513
+ return [
514
+ CLIENT_MODULE,
515
+ ...generatedClientDirs(path2.dirname(value.getSourceFile().getFilePath()))
516
+ ];
517
+ }
518
+ function declaredByPrisma(type) {
519
+ return type.declaredIn.some((filePath) => isPrismaClientPath(filePath));
520
+ }
521
+ var CLIENT_MODULE = "@prisma/client";
522
+ var CLIENT_CLASS = "PrismaClient";
523
+ function extendsImportedClient(type, resolution) {
524
+ if (resolution === void 0) {
525
+ return false;
526
+ }
527
+ return type.extendsWritten.some((base) => importedAsClient(base, resolution));
297
528
  }
298
529
  var GENERATED_CLIENT_MARKER = "schema.prisma";
299
530
  function isPrismaClientPath(filePath) {
300
531
  if (filePath.includes("/@prisma/client/") || filePath.includes("/.prisma/client/")) {
301
532
  return true;
302
533
  }
303
- const beside = path.join(path.dirname(filePath), GENERATED_CLIENT_MARKER);
304
- return fs.existsSync(beside);
534
+ const beside = path2.join(path2.dirname(filePath), GENERATED_CLIENT_MARKER);
535
+ return fs2.existsSync(beside) || isInGeneratedClient(filePath);
305
536
  }
306
537
  function capitalizeFirst(name) {
307
538
  if (name.length === 0) {
@@ -406,12 +637,14 @@ function prismaFramework(options = {}) {
406
637
  discovery: [],
407
638
  terminals: [],
408
639
  inputMapping: { type: "positionalParams", params: [] },
409
- // Skip files that don't import from @prisma/client: the
410
- // recognizer's type-resolution check would reject them anyway.
640
+ // The type check would reject every call in a file that does not
641
+ // import `@prisma/client`, so those files are skipped.
411
642
  requiresImport: ["@prisma/client"],
412
643
  // A generator with its own `output` puts the client in the project,
413
- // where the only way to reach it is a relative path.
644
+ // where the only way to reach it is a relative path. Prisma 7's
645
+ // `prisma-client` generator writes no schema beside it.
414
646
  generatedModuleMarkers: [GENERATED_CLIENT_MARKER],
647
+ generatedModuleDirs: generatedClientDirs,
415
648
  invocationRecognizers: [makeRecognizer(options)],
416
649
  // A tagged template is not an invocation, so the raw chain runs on
417
650
  // the access walk, which visits calls as well.
@@ -423,7 +656,7 @@ var declares = {
423
656
  kind: "effects",
424
657
  package: "@suss/framework-prisma",
425
658
  dependencies: [{ ecosystem: "npm", name: "@prisma/client" }],
426
- reads: "Prisma client calls, emits storage-access interactions per read / write."
659
+ reads: "Prisma client calls. Each read and each write becomes a storage-access interaction."
427
660
  };
428
661
  var index_default = prismaFramework;
429
662
  export {
@@ -0,0 +1 @@
1
+ export { declares, default, optionsSchema } from '@suss/framework-pytest';
package/dist/pytest.js ADDED
@@ -0,0 +1,81 @@
1
+ // ../framework/pytest/dist/index.js
2
+ import path from "path";
3
+ import { z } from "zod";
4
+ var optionsSchema = z.object({
5
+ /**
6
+ * The test files to read, matched on whole path segments from the
7
+ * end. `suss extract --intent` fills it in from the tests the PRDs
8
+ * list, so a run reads those files and no others. Left out, every
9
+ * file pytest would collect is read.
10
+ */
11
+ files: z.array(z.string().min(1)).optional()
12
+ }).strict();
13
+ var SHARED_FIXTURE_FILE = "conftest.py";
14
+ function pytestFramework(options = {}) {
15
+ return {
16
+ name: "pytest",
17
+ protocol: "in-process",
18
+ discovery: [],
19
+ // A test's fixtures can come from a conftest.py in any directory
20
+ // above it, so an edit to one has to re-read the tests below it.
21
+ discoveryInputs: (files) => files.filter((file) => path.basename(file) === SHARED_FIXTURE_FILE),
22
+ tests: [
23
+ {
24
+ filePatterns: ["test_*.py", "*_test.py"],
25
+ functionPrefix: "test",
26
+ classPrefix: "Test",
27
+ caseBaseClasses: [
28
+ "unittest.TestCase",
29
+ "unittest.IsolatedAsyncioTestCase"
30
+ ],
31
+ setUp: {
32
+ module: ["setup_module"],
33
+ function: ["setup_function"],
34
+ testClass: ["setup_class", "setup_method"],
35
+ caseClass: ["setUpClass", "setUp", "asyncSetUp"]
36
+ },
37
+ usesFixtureMarkers: ["pytest.mark.usefixtures"],
38
+ fixtureDecorators: ["pytest.fixture"],
39
+ fixtureNameKeyword: "name",
40
+ autouseKeyword: "autouse",
41
+ sharedFixtureFiles: [SHARED_FIXTURE_FILE],
42
+ skipDecorators: [
43
+ "pytest.mark.skip",
44
+ "pytest.mark.skipif",
45
+ "pytest.mark.xfail",
46
+ "unittest.skip",
47
+ "unittest.skipIf",
48
+ "unittest.skipUnless",
49
+ "unittest.expectedFailure"
50
+ ],
51
+ markerVariable: "pytestmark",
52
+ reservedParameters: ["self", "cls", "request"],
53
+ mocks: {
54
+ patchers: [
55
+ "unittest.mock.patch",
56
+ "unittest.mock.patch.object",
57
+ "mock.patch",
58
+ "mock.patch.object"
59
+ ],
60
+ fixturePatchers: [
61
+ { fixture: "mocker", methods: ["patch", "patch.object"] },
62
+ { fixture: "monkeypatch", methods: ["setattr"] }
63
+ ]
64
+ },
65
+ ...options.files !== void 0 ? { files: options.files } : {}
66
+ }
67
+ ]
68
+ };
69
+ }
70
+ var declares = {
71
+ kind: "framework",
72
+ package: "@suss/framework-pytest",
73
+ dependencies: [],
74
+ reads: "pytest tests (Python), named by their node ids, so a PRD scenario can say which test covers it."
75
+ };
76
+ var index_default = pytestFramework;
77
+ export {
78
+ declares,
79
+ index_default as default,
80
+ optionsSchema
81
+ };
package/dist/rails.js CHANGED
@@ -304,20 +304,20 @@ var RouteAccumulator = class {
304
304
  drawDirectory;
305
305
  byKey = /* @__PURE__ */ new Map();
306
306
  unread = /* @__PURE__ */ new Map();
307
- /** Per file, each gem block call whose body was walked as though the call changed nothing about it. */
307
+ /** Per file, the gem block calls whose bodies were walked as if the call were not there. */
308
308
  walkedBlocks = /* @__PURE__ */ new Map();
309
309
  missingDrawn = [];
310
310
  /** The block each `concern :name do ... end` declared, for `concerns` to replay. */
311
311
  concerns = /* @__PURE__ */ new Map();
312
312
  /** Files `draw(:name)` has already read, so a file drawing itself stops. */
313
313
  drawn = /* @__PURE__ */ new Set();
314
- /** Every engine a `mount` may refer to, by the class name it is written with. */
314
+ /** Every engine a `mount` may refer to, keyed by class name. */
315
315
  engines = /* @__PURE__ */ new Map();
316
316
  /** Engines whose route set is being walked, so an engine that mounts itself stops. */
317
317
  mounting = /* @__PURE__ */ new Set();
318
- /** The file whose statements are being walked, as a gap writes it. */
318
+ /** The file whose statements are being walked, as a gap message prints it. */
319
319
  file = "";
320
- /** The first route written for an action wins, the way Rails matches the first route it declared. */
320
+ /** The first route written for an action wins, because Rails matches routes in the order they are declared. */
321
321
  add(controllerKey, action, route) {
322
322
  const key = `${controllerKey}#${action}`;
323
323
  if (!this.byKey.has(key)) {
@@ -880,37 +880,46 @@ var RACK_STATUS_CODE_NAMES = {
880
880
  network_authentication_required: 511
881
881
  };
882
882
  var optionsSchema = z.object({
883
- /** The app directory a bare controller name is looked up under. Every Rails app scaffolds this at `app`. */
883
+ /**
884
+ * The directory a bare controller name is looked up under, `app` in a
885
+ * scaffolded Rails app.
886
+ */
884
887
  root: z.string().optional(),
885
888
  /**
886
- * The directory a relative `root` or `routesFile` is read against: the
887
- * config file's own directory, or the directory the run reads when the
888
- * options came without one. The CLI supplies this; it is not written in
889
- * the file itself.
889
+ * The CLI fills this in with the config file's directory, or with the
890
+ * directory the run reads when there is no file. A relative `root` or
891
+ * `routesFile` resolves against it. A config file does not set it.
890
892
  */
891
893
  configDirectory: z.string().optional(),
892
- /** Base classes beyond `ApplicationController` that also mark a class as a controller. */
894
+ /**
895
+ * Base classes beyond `ApplicationController` whose subclasses are
896
+ * controllers. A config file may not set this; a dependency stub
897
+ * fills it.
898
+ */
893
899
  baseClassNames: z.array(z.string()).optional(),
894
900
  /**
895
- * Methods beyond Rails' own that every controller in this project
896
- * gets without defining them, from a gem such as Devise. They are
897
- * left off an action's effects the same way Rails' own are.
901
+ * Controller methods a gem such as Devise adds, beyond the ones Rails
902
+ * defines. They are left off an action's effects the same way Rails'
903
+ * own are.
898
904
  */
899
905
  inheritedMethodNames: z.array(z.string()).optional(),
900
- /** Where this project's routes live, relative to `configDirectory` when there is one. Every Rails app scaffolds this at `config/routes.rb`. */
906
+ /**
907
+ * The routes file, `config/routes.rb` in a scaffolded app. A relative
908
+ * path resolves against `configDirectory` when there is one.
909
+ */
901
910
  routesFile: z.string().optional(),
902
911
  /**
903
- * Directories the project keeps its own engines under, each one the
904
- * root a `Rails::Engine` subclass has its `lib/` and `config/routes.rb`
905
- * in. A `*` in a segment matches any one directory, so `plugins/*`
906
- * covers every plugin. Relative to `configDirectory`.
912
+ * Directories that each contain an engine, with its `Rails::Engine`
913
+ * class under `lib/` and its routes in `config/routes.rb`. A `*` in a
914
+ * segment matches any one directory, so `plugins/*` covers every
915
+ * plugin. Relative paths resolve against `configDirectory`.
907
916
  */
908
917
  engineRoots: z.array(z.string()).optional(),
909
918
  /**
910
- * Files beyond the routes file that add routes through a
911
- * `Rails.application.routes.draw`, `.append` or `.prepend` block,
912
- * a plugin's `plugin.rb` being the usual one. Same `*` rule as
913
- * `engineRoots`. Relative to `configDirectory`.
919
+ * Other files that add routes through a `Rails.application.routes.draw`,
920
+ * `.append` or `.prepend` block, most often a plugin's `plugin.rb`.
921
+ * They take `*` the same way `engineRoots` does, and relative paths
922
+ * resolve against `configDirectory`.
914
923
  */
915
924
  routesFiles: z.array(z.string()).optional()
916
925
  }).strict();
@@ -1063,9 +1072,18 @@ function railsFramework(options = {}) {
1063
1072
  discovery: [pattern],
1064
1073
  bodyBlocks: BODY_BLOCKS,
1065
1074
  inflections,
1066
- // Every file routing or naming is read from decides an action's
1067
- // binding without being walked, so the cache key has to read them
1068
- // here. This runs before the grammar loads, so nothing parses Ruby.
1075
+ // An action reads the request through these, and is handed nothing.
1076
+ requestAccessors: ["params", "request"],
1077
+ // Rails merges the path, the query string and the body into `params`,
1078
+ // so an action's read of one cannot be told from the others and all
1079
+ // of them are spelled `params`.
1080
+ requestSpelling: {
1081
+ headers: { path: ["request", "headers"], saysWhichField: true },
1082
+ params: { path: ["params"], saysWhichField: true }
1083
+ },
1084
+ // These files change an action's binding without being walked, so the
1085
+ // cache key has to list them. This runs before the grammar loads, so
1086
+ // nothing here parses Ruby.
1069
1087
  discoveryInputs: () => [
1070
1088
  routesFile,
1071
1089
  ...drawableRoutesFiles(routesFile),
@@ -1096,7 +1114,7 @@ var declares = {
1096
1114
  { ecosystem: "rubygems", name: "rails" },
1097
1115
  { ecosystem: "rubygems", name: "railties" }
1098
1116
  ],
1099
- reads: "Rails controller actions (Ruby), bound to the method and path `config/routes.rb` gives each one; an action the routes file does not reach is discovered with no boundary.",
1117
+ reads: "Rails controller actions (Ruby), bound to the method and path `config/routes.rb` gives each one. An action the routes file does not reach is still discovered, with no boundary.",
1100
1118
  configuration: {
1101
1119
  file: "suss.rails.json",
1102
1120
  example: {
@@ -1106,7 +1124,8 @@ var declares = {
1106
1124
  routesFiles: ["plugins/*/plugin.rb"]
1107
1125
  },
1108
1126
  required: false,
1109
- why: "the app directory a controller is defined under and the routes file suss reads each action's method and path from; rails new scaffolds both at these paths. engineRoots lists where the project keeps engines it mounts, and routesFiles any other file that adds routes, when it has either."
1127
+ why: "the app directory a controller is defined under and the routes file suss reads each action's method and path from; rails new scaffolds both at these paths. engineRoots lists where the project keeps engines it mounts, and routesFiles any other file that adds routes, when it has either.",
1128
+ defaults: { root: "app", routesFile: "config/routes.rb" }
1110
1129
  }
1111
1130
  };
1112
1131
  var index_default = railsFramework;
@@ -134,7 +134,7 @@ var declares = {
134
134
  kind: "framework",
135
135
  package: "@suss/framework-react-query",
136
136
  dependencies: [{ ecosystem: "npm", name: "@tanstack/react-query" }],
137
- reads: "TanStack Query hooks: ties a component to the query function its `useQuery` / `useMutation` call runs."
137
+ reads: "TanStack Query hooks. The pack links a component to the query function that its `useQuery` or `useMutation` call runs."
138
138
  };
139
139
  var index_default = reactQueryFramework;
140
140
  export {