@suss/extractor 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,8 +63,8 @@ const summary = assembleSummary(raw);
63
63
  ## More
64
64
 
65
65
  - [How the extractor assembles a summary](./DESIGN.md)
66
- - [Documentation](https://nimbuscloud-ai.github.io/suss/)
67
- - [Every package and pack suss ships](https://nimbuscloud-ai.github.io/suss/reference/packages)
66
+ - [Documentation](https://suss.sh/)
67
+ - [Every package and pack suss ships](https://suss.sh/packs/catalog)
68
68
  - [Source and issues](https://github.com/nimbuscloud-ai/suss)
69
69
 
70
70
  ## Coverage
@@ -77,4 +77,4 @@ Licensed under Apache 2.0. See [LICENSE](../../LICENSE).
77
77
 
78
78
  ---
79
79
 
80
- For the canonical design, see [docs/architecture.md](../../docs/architecture.md).
80
+ For the canonical design, see [docs/theory/architecture.md](../../docs/theory/architecture.md).
package/dist/index.cjs CHANGED
@@ -395,7 +395,7 @@ function stampModuleImports(summaries, importsOf) {
395
395
 
396
396
  // src/moduleInit.ts
397
397
  function moduleInitStructure(options) {
398
- const { name, file, range, effects } = options;
398
+ const { name, file, range, effects, calls } = options;
399
399
  return {
400
400
  identity: {
401
401
  name,
@@ -422,7 +422,7 @@ function moduleInitStructure(options) {
422
422
  emitEvent: null,
423
423
  location: range
424
424
  },
425
- effects: [],
425
+ effects: calls ?? [],
426
426
  extraEffects: effects,
427
427
  location: range,
428
428
  isDefault: true
@@ -1809,6 +1809,14 @@ function recognizersWithNoUnits(packs) {
1809
1809
  detail: `${funnel.candidateFiles} gated files, and no pack in this run discovered a unit in them. ${funnel.pack} reads calls inside units another pack finds, so add the pack that finds this project's handlers (-f express, -f fastify, ...), or run suss init to work out which.`
1810
1810
  }));
1811
1811
  }
1812
+ function gatedPacksWithNoFiles(packs) {
1813
+ return packs.filter(
1814
+ (funnel) => funnel.gates.length > 0 && funnel.unresolvedGates.length === 0 && funnel.candidateFiles === 0
1815
+ ).map((funnel) => ({
1816
+ label: funnel.pack,
1817
+ detail: `${funnel.gates.join(", ")} is installed and no file in this run imports it, directly or through a module of the project's own. Either this code does not use the library, or it reaches it some way the import gate does not follow.`
1818
+ }));
1819
+ }
1812
1820
  function helpersThatMatchedNothing(packs) {
1813
1821
  return packs.flatMap(
1814
1822
  (funnel) => funnel.helpersUnmatched.map((helper) => ({
@@ -1899,6 +1907,12 @@ function evaluatePackHealth(report) {
1899
1907
  audience: "run",
1900
1908
  violations: funnelDrops(report.packs)
1901
1909
  },
1910
+ {
1911
+ name: "every pack whose library is installed had files to read",
1912
+ code: "no-files",
1913
+ audience: "run",
1914
+ violations: gatedPacksWithNoFiles(report.packs)
1915
+ },
1902
1916
  {
1903
1917
  name: "every recognizer had units to look inside",
1904
1918
  code: "no-units",
@@ -2093,6 +2107,22 @@ function unreadSentences(raw) {
2093
2107
  ...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
2094
2108
  ];
2095
2109
  }
2110
+ function effectsOfBranch(branch) {
2111
+ const effects = [
2112
+ ...branch.effects.map(effectToIR),
2113
+ ...branch.extraEffects ?? []
2114
+ ];
2115
+ return (0, import_behavioral_ir5.foldRepeatedEffects)(effects.map(withNormalizedCallee));
2116
+ }
2117
+ function withNormalizedCallee(effect) {
2118
+ if (effect.type !== "invocation" && effect.type !== "interaction") {
2119
+ return effect;
2120
+ }
2121
+ if (effect.callee === void 0) {
2122
+ return effect;
2123
+ }
2124
+ return { ...effect, callee: (0, import_behavioral_ir5.normalizeCalleeText)(effect.callee) };
2125
+ }
2096
2126
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2097
2127
  const transitions = raw.branches.map((rawBranch) => {
2098
2128
  const branch = branchWithCollapsedReadings(rawBranch);
@@ -2103,10 +2133,7 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2103
2133
  id: makeTransitionId(raw.identity.name, branch),
2104
2134
  conditions,
2105
2135
  output: terminalToOutput(branch.terminal),
2106
- effects: [
2107
- ...branch.effects.map(effectToIR),
2108
- ...branch.extraEffects ?? []
2109
- ],
2136
+ effects: effectsOfBranch(branch),
2110
2137
  location: branch.location,
2111
2138
  isDefault: branch.isDefault
2112
2139
  };
@@ -2171,6 +2198,9 @@ function buildMetadata(raw) {
2171
2198
  if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
2172
2199
  metadata = (0, import_behavioral_ir5.withWrapperMetadata)(metadata, { applied: raw.wrappers });
2173
2200
  }
2201
+ if (raw.requestSpelling !== void 0) {
2202
+ metadata = (0, import_behavioral_ir5.withRequestSpellingMetadata)(metadata, raw.requestSpelling);
2203
+ }
2174
2204
  return Object.keys(metadata).length > 0 ? metadata : null;
2175
2205
  }
2176
2206
  function buildHttpMetadataValue(raw) {
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, MessageBusSemantics, DeployableUnit, CodeUnitKind, BehavioralSummary, Predicate, TypeShape, RenderNode, BoundaryBinding, WrapperReference, GraphqlDeclaredContract, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
1
+ import { Effect, MessageBusSemantics, DeployableUnit, CodeUnitKind, RequestSpellingMetadata, BehavioralSummary, Predicate, TypeShape, RenderNode, BoundaryBinding, WrapperReference, GraphqlDeclaredContract, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -173,6 +173,15 @@ type DiscoveryMatch = {
173
173
  * `axios.get(...)`.
174
174
  */
175
175
  factoryMethods?: string[];
176
+ /**
177
+ * The property of the factory call's config object that every
178
+ * request through the instance is sent under, `baseURL` for axios.
179
+ * Both sides of a boundary have to read one route the same way,
180
+ * and a spec's `servers[0].url` already goes in front of the
181
+ * provider's paths, so a base written here goes in front of the
182
+ * consumer's.
183
+ */
184
+ basePathOption?: string;
176
185
  /**
177
186
  * The import and every instance built from it can be called as a
178
187
  * function, so `axios(config)` and `api(config)` are requests the
@@ -1174,6 +1183,18 @@ interface PatternPack {
1174
1183
  terminals: TerminalPattern[];
1175
1184
  contractReading?: ContractPattern;
1176
1185
  inputMapping: InputMappingPattern;
1186
+ /**
1187
+ * For a REST pack, where in the handler each part of the request is
1188
+ * read, in the same words `inputMapping` gives the parameters: an
1189
+ * Express handler reads a header at `request.headers`, a Lambda
1190
+ * handler at `event.headers`. The adapter stamps this on every route
1191
+ * the pack recognizes, and the intent pass rewrites the route's reads
1192
+ * from it into the sections an author writes under `receives`.
1193
+ *
1194
+ * Leave it out on a pack whose handlers do not take a request, and
1195
+ * leave a section out when the framework has no path for it.
1196
+ */
1197
+ requestSpelling?: RequestSpellingMetadata;
1177
1198
  /**
1178
1199
  * Transport (wire protocol) used in the `BoundaryBinding.transport`
1179
1200
  * of discovered units. Every pack has to say what its transport is
@@ -1337,6 +1358,16 @@ interface PatternPack {
1337
1358
  * available without importing anything).
1338
1359
  */
1339
1360
  requiresImport?: string[];
1361
+ /**
1362
+ * Files this library's code generator writes beside the module it
1363
+ * generates, e.g. `["schema.prisma"]`. A project can point the
1364
+ * generator at a directory of its own, and then every consumer
1365
+ * imports the module by relative path and `requiresImport` matches
1366
+ * nothing. A directory containing one of these files counts as the
1367
+ * gated package, so those consumers reach the pack the way an
1368
+ * ordinary import would.
1369
+ */
1370
+ generatedModuleMarkers?: string[];
1340
1371
  /**
1341
1372
  * Functions the project itself wrote in front of this library, read
1342
1373
  * once across the whole project before any file is walked. What the
@@ -1974,12 +2005,12 @@ declare function stampModuleImports(summaries: BehavioralSummary[], importsOf: (
1974
2005
  * and a service that reads its configuration there reads it there for
1975
2006
  * good. Walking unit bodies alone never sees that read, so the read gets
1976
2007
  * a unit of its own, one per file, named after the file, with no
1977
- * boundary. Attributing it to each handler in the file would report one
1978
- * read as several, and a file with no handler would still report nothing.
2008
+ * boundary. Attributing it to each handler would report one read as
2009
+ * several. The calls those statements make go on the same unit, so the
2010
+ * closure can reach the functions behind them.
1979
2011
  *
1980
- * The structure is one default branch whose terminal is `void`, because
1981
- * module initialization returns to nobody. Every adapter builds it the
1982
- * same way, so it lives here.
2012
+ * The one default branch has a `void` terminal, because module
2013
+ * initialization returns to nobody. Every adapter builds it the same way.
1983
2014
  */
1984
2015
 
1985
2016
  interface ModuleInitOptions {
@@ -1991,6 +2022,8 @@ interface ModuleInitOptions {
1991
2022
  end: number;
1992
2023
  };
1993
2024
  effects: Effect[];
2025
+ /** The calls the top-level statements make while the module loads. */
2026
+ calls?: RawEffect[];
1994
2027
  }
1995
2028
  declare function moduleInitStructure(options: ModuleInitOptions): RawCodeStructure;
1996
2029
 
@@ -3032,6 +3065,17 @@ interface ValueOps {
3032
3065
  * there, so a query that hands over a table object can still be read.
3033
3066
  */
3034
3067
  holes(): readonly (CallOps | null)[];
3068
+ /**
3069
+ * The same holes as values, in the same order, for a reader that
3070
+ * wants what the source settled a hole to rather than what call was
3071
+ * written there. A table kept in a module constant is the case: the
3072
+ * hole is a name, not a call, so `holes` gives null for it and this
3073
+ * gives the constant.
3074
+ *
3075
+ * An adapter that has not implemented this leaves it out, and a
3076
+ * reader then has only the calls.
3077
+ */
3078
+ interpolated?(): readonly ValueOps[];
3035
3079
  }
3036
3080
  /** One entry of an object a call states. */
3037
3081
  interface ValueEntry {
@@ -3405,6 +3449,8 @@ interface RawCodeStructure {
3405
3449
  };
3406
3450
  /** The wrappers registered around this unit: middleware, error handlers. */
3407
3451
  wrappers?: WrapperReference[];
3452
+ /** Where a REST pack's handlers read each part of the request, from the pack. */
3453
+ requestSpelling?: RequestSpellingMetadata;
3408
3454
  /** The extractor cannot derive this. An adapter that has the SDL and knows
3409
3455
  * which field the resolver serves fills it in. */
3410
3456
  graphqlDeclaredContract?: GraphqlDeclaredContract;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, MessageBusSemantics, DeployableUnit, CodeUnitKind, BehavioralSummary, Predicate, TypeShape, RenderNode, BoundaryBinding, WrapperReference, GraphqlDeclaredContract, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
1
+ import { Effect, MessageBusSemantics, DeployableUnit, CodeUnitKind, RequestSpellingMetadata, BehavioralSummary, Predicate, TypeShape, RenderNode, BoundaryBinding, WrapperReference, GraphqlDeclaredContract, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -173,6 +173,15 @@ type DiscoveryMatch = {
173
173
  * `axios.get(...)`.
174
174
  */
175
175
  factoryMethods?: string[];
176
+ /**
177
+ * The property of the factory call's config object that every
178
+ * request through the instance is sent under, `baseURL` for axios.
179
+ * Both sides of a boundary have to read one route the same way,
180
+ * and a spec's `servers[0].url` already goes in front of the
181
+ * provider's paths, so a base written here goes in front of the
182
+ * consumer's.
183
+ */
184
+ basePathOption?: string;
176
185
  /**
177
186
  * The import and every instance built from it can be called as a
178
187
  * function, so `axios(config)` and `api(config)` are requests the
@@ -1174,6 +1183,18 @@ interface PatternPack {
1174
1183
  terminals: TerminalPattern[];
1175
1184
  contractReading?: ContractPattern;
1176
1185
  inputMapping: InputMappingPattern;
1186
+ /**
1187
+ * For a REST pack, where in the handler each part of the request is
1188
+ * read, in the same words `inputMapping` gives the parameters: an
1189
+ * Express handler reads a header at `request.headers`, a Lambda
1190
+ * handler at `event.headers`. The adapter stamps this on every route
1191
+ * the pack recognizes, and the intent pass rewrites the route's reads
1192
+ * from it into the sections an author writes under `receives`.
1193
+ *
1194
+ * Leave it out on a pack whose handlers do not take a request, and
1195
+ * leave a section out when the framework has no path for it.
1196
+ */
1197
+ requestSpelling?: RequestSpellingMetadata;
1177
1198
  /**
1178
1199
  * Transport (wire protocol) used in the `BoundaryBinding.transport`
1179
1200
  * of discovered units. Every pack has to say what its transport is
@@ -1337,6 +1358,16 @@ interface PatternPack {
1337
1358
  * available without importing anything).
1338
1359
  */
1339
1360
  requiresImport?: string[];
1361
+ /**
1362
+ * Files this library's code generator writes beside the module it
1363
+ * generates, e.g. `["schema.prisma"]`. A project can point the
1364
+ * generator at a directory of its own, and then every consumer
1365
+ * imports the module by relative path and `requiresImport` matches
1366
+ * nothing. A directory containing one of these files counts as the
1367
+ * gated package, so those consumers reach the pack the way an
1368
+ * ordinary import would.
1369
+ */
1370
+ generatedModuleMarkers?: string[];
1340
1371
  /**
1341
1372
  * Functions the project itself wrote in front of this library, read
1342
1373
  * once across the whole project before any file is walked. What the
@@ -1974,12 +2005,12 @@ declare function stampModuleImports(summaries: BehavioralSummary[], importsOf: (
1974
2005
  * and a service that reads its configuration there reads it there for
1975
2006
  * good. Walking unit bodies alone never sees that read, so the read gets
1976
2007
  * a unit of its own, one per file, named after the file, with no
1977
- * boundary. Attributing it to each handler in the file would report one
1978
- * read as several, and a file with no handler would still report nothing.
2008
+ * boundary. Attributing it to each handler would report one read as
2009
+ * several. The calls those statements make go on the same unit, so the
2010
+ * closure can reach the functions behind them.
1979
2011
  *
1980
- * The structure is one default branch whose terminal is `void`, because
1981
- * module initialization returns to nobody. Every adapter builds it the
1982
- * same way, so it lives here.
2012
+ * The one default branch has a `void` terminal, because module
2013
+ * initialization returns to nobody. Every adapter builds it the same way.
1983
2014
  */
1984
2015
 
1985
2016
  interface ModuleInitOptions {
@@ -1991,6 +2022,8 @@ interface ModuleInitOptions {
1991
2022
  end: number;
1992
2023
  };
1993
2024
  effects: Effect[];
2025
+ /** The calls the top-level statements make while the module loads. */
2026
+ calls?: RawEffect[];
1994
2027
  }
1995
2028
  declare function moduleInitStructure(options: ModuleInitOptions): RawCodeStructure;
1996
2029
 
@@ -3032,6 +3065,17 @@ interface ValueOps {
3032
3065
  * there, so a query that hands over a table object can still be read.
3033
3066
  */
3034
3067
  holes(): readonly (CallOps | null)[];
3068
+ /**
3069
+ * The same holes as values, in the same order, for a reader that
3070
+ * wants what the source settled a hole to rather than what call was
3071
+ * written there. A table kept in a module constant is the case: the
3072
+ * hole is a name, not a call, so `holes` gives null for it and this
3073
+ * gives the constant.
3074
+ *
3075
+ * An adapter that has not implemented this leaves it out, and a
3076
+ * reader then has only the calls.
3077
+ */
3078
+ interpolated?(): readonly ValueOps[];
3035
3079
  }
3036
3080
  /** One entry of an object a call states. */
3037
3081
  interface ValueEntry {
@@ -3405,6 +3449,8 @@ interface RawCodeStructure {
3405
3449
  };
3406
3450
  /** The wrappers registered around this unit: middleware, error handlers. */
3407
3451
  wrappers?: WrapperReference[];
3452
+ /** Where a REST pack's handlers read each part of the request, from the pack. */
3453
+ requestSpelling?: RequestSpellingMetadata;
3408
3454
  /** The extractor cannot derive this. An adapter that has the SDL and knows
3409
3455
  * which field the resolver serves fills it in. */
3410
3456
  graphqlDeclaredContract?: GraphqlDeclaredContract;
package/dist/index.js CHANGED
@@ -2,9 +2,12 @@
2
2
  import { createHash as createHash3 } from "crypto";
3
3
  import {
4
4
  exchangesHttpResponses as exchangesHttpResponses2,
5
+ foldRepeatedEffects,
6
+ normalizeCalleeText,
5
7
  withGraphqlMetadata,
6
8
  withHttpMetadata,
7
9
  withMountMetadata,
10
+ withRequestSpellingMetadata,
8
11
  withSourceDocumentMetadata,
9
12
  withWrapperMetadata as withWrapperMetadata2
10
13
  } from "@suss/behavioral-ir";
@@ -319,7 +322,7 @@ function stampModuleImports(summaries, importsOf) {
319
322
 
320
323
  // src/moduleInit.ts
321
324
  function moduleInitStructure(options) {
322
- const { name, file, range, effects } = options;
325
+ const { name, file, range, effects, calls } = options;
323
326
  return {
324
327
  identity: {
325
328
  name,
@@ -346,7 +349,7 @@ function moduleInitStructure(options) {
346
349
  emitEvent: null,
347
350
  location: range
348
351
  },
349
- effects: [],
352
+ effects: calls ?? [],
350
353
  extraEffects: effects,
351
354
  location: range,
352
355
  isDefault: true
@@ -1732,6 +1735,14 @@ function recognizersWithNoUnits(packs) {
1732
1735
  detail: `${funnel.candidateFiles} gated files, and no pack in this run discovered a unit in them. ${funnel.pack} reads calls inside units another pack finds, so add the pack that finds this project's handlers (-f express, -f fastify, ...), or run suss init to work out which.`
1733
1736
  }));
1734
1737
  }
1738
+ function gatedPacksWithNoFiles(packs) {
1739
+ return packs.filter(
1740
+ (funnel) => funnel.gates.length > 0 && funnel.unresolvedGates.length === 0 && funnel.candidateFiles === 0
1741
+ ).map((funnel) => ({
1742
+ label: funnel.pack,
1743
+ detail: `${funnel.gates.join(", ")} is installed and no file in this run imports it, directly or through a module of the project's own. Either this code does not use the library, or it reaches it some way the import gate does not follow.`
1744
+ }));
1745
+ }
1735
1746
  function helpersThatMatchedNothing(packs) {
1736
1747
  return packs.flatMap(
1737
1748
  (funnel) => funnel.helpersUnmatched.map((helper) => ({
@@ -1822,6 +1833,12 @@ function evaluatePackHealth(report) {
1822
1833
  audience: "run",
1823
1834
  violations: funnelDrops(report.packs)
1824
1835
  },
1836
+ {
1837
+ name: "every pack whose library is installed had files to read",
1838
+ code: "no-files",
1839
+ audience: "run",
1840
+ violations: gatedPacksWithNoFiles(report.packs)
1841
+ },
1825
1842
  {
1826
1843
  name: "every recognizer had units to look inside",
1827
1844
  code: "no-units",
@@ -2016,6 +2033,22 @@ function unreadSentences(raw) {
2016
2033
  ...handedOver.map(unreadReasonOf).filter((reason) => reason !== null)
2017
2034
  ];
2018
2035
  }
2036
+ function effectsOfBranch(branch) {
2037
+ const effects = [
2038
+ ...branch.effects.map(effectToIR),
2039
+ ...branch.extraEffects ?? []
2040
+ ];
2041
+ return foldRepeatedEffects(effects.map(withNormalizedCallee));
2042
+ }
2043
+ function withNormalizedCallee(effect) {
2044
+ if (effect.type !== "invocation" && effect.type !== "interaction") {
2045
+ return effect;
2046
+ }
2047
+ if (effect.callee === void 0) {
2048
+ return effect;
2049
+ }
2050
+ return { ...effect, callee: normalizeCalleeText(effect.callee) };
2051
+ }
2019
2052
  function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2020
2053
  const transitions = raw.branches.map((rawBranch) => {
2021
2054
  const branch = branchWithCollapsedReadings(rawBranch);
@@ -2026,10 +2059,7 @@ function assembleSummary(raw, options = DEFAULT_OPTIONS) {
2026
2059
  id: makeTransitionId(raw.identity.name, branch),
2027
2060
  conditions,
2028
2061
  output: terminalToOutput(branch.terminal),
2029
- effects: [
2030
- ...branch.effects.map(effectToIR),
2031
- ...branch.extraEffects ?? []
2032
- ],
2062
+ effects: effectsOfBranch(branch),
2033
2063
  location: branch.location,
2034
2064
  isDefault: branch.isDefault
2035
2065
  };
@@ -2094,6 +2124,9 @@ function buildMetadata(raw) {
2094
2124
  if (raw.wrappers !== void 0 && raw.wrappers.length > 0) {
2095
2125
  metadata = withWrapperMetadata2(metadata, { applied: raw.wrappers });
2096
2126
  }
2127
+ if (raw.requestSpelling !== void 0) {
2128
+ metadata = withRequestSpellingMetadata(metadata, raw.requestSpelling);
2129
+ }
2097
2130
  return Object.keys(metadata).length > 0 ? metadata : null;
2098
2131
  }
2099
2132
  function buildHttpMetadataValue(raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/extractor",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "description": "The suss extraction pipeline, which turns what a language adapter read into the summaries the checker pairs across a boundary.",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  "typecheck": "tsc --noEmit"
47
47
  },
48
48
  "dependencies": {
49
- "@suss/behavioral-ir": "0.32.0",
49
+ "@suss/behavioral-ir": "0.33.0",
50
50
  "zod": "^4.0.0"
51
51
  },
52
52
  "devDependencies": {