@happyvertical/smrt-scanner 0.40.69 → 0.41.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/AGENTS.md CHANGED
@@ -18,6 +18,18 @@ executes the source.
18
18
  - `parseFile` / `parseSource` — parse a single file or a source string to a
19
19
  `FileScanResult` (classes, errors, type aliases, SMRT imports).
20
20
  - `extractSmrtImports` — pull SMRT-related imports from a parsed file.
21
+ - `lintNumericPrecision(classes, sourceText?)` — flags persisted `number` fields
22
+ whose declared precision contradicts their name (#2361), returning a `kind`
23
+ per finding. **Money is exact** and stored as integer minor units, so
24
+ `subtotal = 0.0` is a `money` finding; **rates are fractional**, so
25
+ `taxRate = 0` is a `rate` finding. `classifyNumericFieldName` does head-noun
26
+ matching and lets a rate word win outright, so `taxRate` is a rate even though
27
+ `tax` is money, while `amountCents` and `totalTokensUsed` classify as neither.
28
+ `weight`/`score`/`factor`/`percent` are deliberately unclassified — they are
29
+ commonly whole numbers. Explicit `@field({ type })`, `@meta`/`Meta<T>`,
30
+ transient, relationship, and static fields are exempt.
31
+ `sourceMayContainNumericPrecisionIssue(source)` is the cheap pre-filter
32
+ callers use to avoid parsing every file; `dev:knowledge-check` drives both.
21
33
  - `verifyManifestCompleteness({ packageDir })` — publish guard: re-scans `src/`
22
34
  and asserts every `@smrt()` object reached `dist/manifest.json` (issue #1483).
23
35
  Returns `ok` / `incomplete` / `missing-manifest` / `scan-error` / `skipped`.
@@ -89,9 +101,13 @@ exhausts the heap when the scanner is pointed at an application root (#2275):
89
101
  - **`ManifestBuilder` / `discoverBaseClasses` live in `@happyvertical/smrt-core`,
90
102
  not here.** This package is the lower-level AST layer; core orchestrates
91
103
  manifest generation and base-class discovery on top of it.
92
- - **0 vs 0.0 heuristic**: `count = 0` → integer, `price = 0.0` → decimal; the
104
+ - **0 vs 0.0 heuristic**: `count = 0` → integer, `ratio = 0.0` → decimal; the
93
105
  raw initializer text (not the parsed value) decides, and negative defaults are
94
- unwrapped from their `UnaryExpression` before the check.
106
+ unwrapped from their `UnaryExpression` before the check. The rule is silent
107
+ and SQLite's affinity masks the consequence, so money- and rate-shaped fields
108
+ are gated by `lintNumericPrecision` rather than left to review (#2361).
109
+ - **`RawFieldDefinition.line` is `0`**: the OXC AST nodes reaching this package
110
+ carry no `loc`. Pass the source text to a consumer that needs a real line.
95
111
  - **Static property capture**: captures `uiSlots` and `adminRoutes` for agent
96
112
  manifest generation.
97
113
  - **`@smrt()` config spreads resolve only against unescaped same-file `const`s** (issue
@@ -112,3 +128,11 @@ exhausts the heap when the scanner is pointed at an application root (#2275):
112
128
  but its taint replays into the diagnostics of every decorator that spreads
113
129
  it, transitively through constant chains. Without that the silent drop simply
114
130
  moves one level up. An unused tainted constant reports nothing.
131
+ - **Relationship targets are resolved, not copied**: `@foreignKey`,
132
+ `@oneToMany` and `@manyToMany` arguments arrive as raw source text.
133
+ `'Target'`/`Target` pass through and a forward-reference thunk
134
+ (`() => Target`, including the dotted `() => Target.column` form) is unwrapped
135
+ to its target, matching the runtime decorator, which resolves the thunk by
136
+ invoking it. Any other expression (a call, a computed reference) yields
137
+ `related: undefined` — writing the raw source through produced a garbage FK
138
+ table name and, once FK columns are indexed, a garbage index (#2379).
@@ -1261,6 +1261,18 @@ function stripQuotes(value) {
1261
1261
  const match = value.match(/^(['"`])(.+)\1$/);
1262
1262
  return match ? match[2] : value;
1263
1263
  }
1264
+ var QUOTED_LITERAL_PATTERN = /^(['"`])(.*)\1$/s;
1265
+ var RELATED_TARGET_PATTERN = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
1266
+ var RELATED_THUNK_PATTERN = /^\(\s*\)\s*=>\s*(?:([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)|\{\s*return\s+([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*;?\s*\})$/;
1267
+ function resolveRelatedArgument(raw) {
1268
+ const value = raw?.trim();
1269
+ if (!value) return void 0;
1270
+ const quoted = value.match(QUOTED_LITERAL_PATTERN);
1271
+ if (quoted) return quoted[2].trim() || void 0;
1272
+ if (RELATED_TARGET_PATTERN.test(value)) return value;
1273
+ const thunk = value.match(RELATED_THUNK_PATTERN);
1274
+ return thunk ? thunk[1] ?? thunk[2] : void 0;
1275
+ }
1264
1276
  function createQualifiedName(packageName, className) {
1265
1277
  return `${packageName}:${className}`;
1266
1278
  }
@@ -1569,7 +1581,7 @@ var ManifestAdapter = class ManifestAdapter {
1569
1581
  };
1570
1582
  }
1571
1583
  if (decorator.name === "foreignKey") {
1572
- const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
1584
+ const relatedClass = resolveRelatedArgument(decorator.arguments[0]);
1573
1585
  const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1574
1586
  const meta = {};
1575
1587
  const META_KEYS = [
@@ -1640,7 +1652,7 @@ var ManifestAdapter = class ManifestAdapter {
1640
1652
  };
1641
1653
  }
1642
1654
  if (decorator.name === "oneToMany") {
1643
- const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
1655
+ const relatedClass = resolveRelatedArgument(decorator.arguments[0]);
1644
1656
  const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1645
1657
  const meta = {};
1646
1658
  if (parsedOptions?.foreignKey !== void 0) meta.foreignKey = parsedOptions.foreignKey;
@@ -1653,7 +1665,7 @@ var ManifestAdapter = class ManifestAdapter {
1653
1665
  };
1654
1666
  }
1655
1667
  if (decorator.name === "manyToMany") {
1656
- const relatedClass = stripQuotes(decorator.arguments[0]?.trim());
1668
+ const relatedClass = resolveRelatedArgument(decorator.arguments[0]);
1657
1669
  const parsedOptions = this.parseFieldDecoratorOptions(decorator.arguments[1]);
1658
1670
  const meta = {};
1659
1671
  if (parsedOptions?.through !== void 0) meta.through = parsedOptions.through;
@@ -2168,4 +2180,4 @@ var OxcScanner = class {
2168
2180
  //#endregion
2169
2181
  export { parseSource as a, normalizeGlobSeparators as c, parseFile as i, relativeGlobToCwd as l, ManifestAdapter as n, InheritanceResolver as o, extractSmrtImports as r, discoverSourceFiles as s, OxcScanner as t };
2170
2182
 
2171
- //# sourceMappingURL=scanner-LeMvLJ6X.js.map
2183
+ //# sourceMappingURL=scanner-jUIN438d.js.map