@happyvertical/smrt-scanner 0.37.2 → 0.37.4
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/dist/chunks/scanner-Caea1XyJ.js +1819 -0
- package/dist/chunks/scanner-Caea1XyJ.js.map +1 -0
- package/dist/cli.js +139 -150
- package/dist/cli.js.map +1 -1
- package/dist/index.js +135 -156
- package/dist/index.js.map +1 -1
- package/dist/types.js +0 -2
- package/package.json +7 -7
- package/dist/chunks/scanner-KKnhqxsr.js +0 -2204
- package/dist/chunks/scanner-KKnhqxsr.js.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,163 +1,142 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { a as parseSource, i as parseFile, n as ManifestAdapter, o as InheritanceResolver, r as extractSmrtImports, t as OxcScanner } from "./chunks/scanner-Caea1XyJ.js";
|
|
2
|
+
import "./types.js";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
|
+
//#region src/verify-completeness.ts
|
|
6
|
+
var DEFAULT_INCLUDE = ["src/**/*.ts"];
|
|
7
|
+
var DEFAULT_EXCLUDE = [
|
|
8
|
+
"**/*.test.ts",
|
|
9
|
+
"**/*.spec.ts",
|
|
10
|
+
"**/__tests__/**",
|
|
11
|
+
"**/*.svelte",
|
|
12
|
+
"**/node_modules/**",
|
|
13
|
+
"**/dist/**",
|
|
14
|
+
"**/*.d.ts"
|
|
14
15
|
];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
var DEFAULT_SKIP_PACKAGES = [
|
|
17
|
+
"core",
|
|
18
|
+
"types",
|
|
19
|
+
"config",
|
|
20
|
+
"scanner",
|
|
21
|
+
"vitest",
|
|
22
|
+
"smrt-playground"
|
|
22
23
|
];
|
|
23
24
|
function resolveManifestPath(packageDir, pkgJson) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (!relativeTarget) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
return resolve(packageDir, relativeTarget);
|
|
25
|
+
const exportsMap = pkgJson.exports ?? {};
|
|
26
|
+
const entry = exportsMap["./manifest"] ?? exportsMap["./manifest.json"];
|
|
27
|
+
let relativeTarget;
|
|
28
|
+
if (typeof entry === "string") relativeTarget = entry;
|
|
29
|
+
else if (entry && typeof entry === "object") {
|
|
30
|
+
const conditional = entry;
|
|
31
|
+
const candidate = conditional.default ?? conditional.import ?? conditional.types;
|
|
32
|
+
if (typeof candidate === "string") relativeTarget = candidate;
|
|
33
|
+
}
|
|
34
|
+
if (!relativeTarget) return null;
|
|
35
|
+
return resolve(packageDir, relativeTarget);
|
|
40
36
|
}
|
|
41
37
|
async function verifyManifestCompleteness(options) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
const distKeys = new Set(Object.keys(distObjects));
|
|
144
|
-
const missing = expectedKeys.filter((key) => !distKeys.has(key));
|
|
145
|
-
return {
|
|
146
|
-
status: missing.length > 0 ? "incomplete" : "ok",
|
|
147
|
-
packageName,
|
|
148
|
-
manifestPath,
|
|
149
|
-
missing,
|
|
150
|
-
expectedCount: expectedKeys.length,
|
|
151
|
-
distCount: distKeys.size
|
|
152
|
-
};
|
|
38
|
+
const packageDir = resolve(options.packageDir);
|
|
39
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
40
|
+
const exclude = options.exclude ?? DEFAULT_EXCLUDE;
|
|
41
|
+
const skipPackages = options.skipPackages ?? DEFAULT_SKIP_PACKAGES;
|
|
42
|
+
const packageJsonPath = resolve(packageDir, "package.json");
|
|
43
|
+
if (!existsSync(packageJsonPath)) return {
|
|
44
|
+
status: "skipped",
|
|
45
|
+
missing: [],
|
|
46
|
+
expectedCount: 0,
|
|
47
|
+
distCount: 0,
|
|
48
|
+
reason: `no package.json at ${packageDir}`
|
|
49
|
+
};
|
|
50
|
+
const pkgJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
51
|
+
const packageName = pkgJson.name;
|
|
52
|
+
if (skipPackages.includes(basename(packageDir))) return {
|
|
53
|
+
status: "skipped",
|
|
54
|
+
packageName,
|
|
55
|
+
missing: [],
|
|
56
|
+
expectedCount: 0,
|
|
57
|
+
distCount: 0,
|
|
58
|
+
reason: "framework infrastructure package (does not use scanner manifest)"
|
|
59
|
+
};
|
|
60
|
+
const manifestPath = resolveManifestPath(packageDir, pkgJson);
|
|
61
|
+
if (!manifestPath) return {
|
|
62
|
+
status: "skipped",
|
|
63
|
+
packageName,
|
|
64
|
+
missing: [],
|
|
65
|
+
expectedCount: 0,
|
|
66
|
+
distCount: 0,
|
|
67
|
+
reason: "package does not publish a ./manifest export"
|
|
68
|
+
};
|
|
69
|
+
const { results, resolved } = await new OxcScanner({
|
|
70
|
+
cwd: packageDir,
|
|
71
|
+
include,
|
|
72
|
+
exclude
|
|
73
|
+
}).scanAndResolve();
|
|
74
|
+
const scanErrors = results.errors.filter((e) => e.severity === "error");
|
|
75
|
+
if (scanErrors.length > 0) {
|
|
76
|
+
const detail = scanErrors.map((e) => {
|
|
77
|
+
return `${e.line ? `${e.filePath}:${e.line}` : e.filePath} \u2014 ${e.message}`;
|
|
78
|
+
}).join("; ");
|
|
79
|
+
return {
|
|
80
|
+
status: "scan-error",
|
|
81
|
+
packageName,
|
|
82
|
+
manifestPath,
|
|
83
|
+
missing: [],
|
|
84
|
+
expectedCount: 0,
|
|
85
|
+
distCount: 0,
|
|
86
|
+
reason: `source scan reported ${scanErrors.length} parse error(s): ${detail}`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const expected = new ManifestAdapter().toManifest(resolved, {
|
|
90
|
+
packageName,
|
|
91
|
+
packageVersion: pkgJson.version,
|
|
92
|
+
typeAliases: results.typeAliases
|
|
93
|
+
});
|
|
94
|
+
const expectedKeys = Object.keys(expected.objects);
|
|
95
|
+
if (expectedKeys.length === 0) return {
|
|
96
|
+
status: "skipped",
|
|
97
|
+
packageName,
|
|
98
|
+
manifestPath,
|
|
99
|
+
missing: [],
|
|
100
|
+
expectedCount: 0,
|
|
101
|
+
distCount: 0,
|
|
102
|
+
reason: "no @smrt() objects found in source"
|
|
103
|
+
};
|
|
104
|
+
if (!existsSync(manifestPath)) return {
|
|
105
|
+
status: "missing-manifest",
|
|
106
|
+
packageName,
|
|
107
|
+
manifestPath,
|
|
108
|
+
missing: expectedKeys,
|
|
109
|
+
expectedCount: expectedKeys.length,
|
|
110
|
+
distCount: 0,
|
|
111
|
+
reason: `published manifest not found at ${manifestPath}`
|
|
112
|
+
};
|
|
113
|
+
let distObjects = {};
|
|
114
|
+
try {
|
|
115
|
+
const distManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
116
|
+
distObjects = distManifest && typeof distManifest.objects === "object" ? distManifest.objects : {};
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return {
|
|
119
|
+
status: "missing-manifest",
|
|
120
|
+
packageName,
|
|
121
|
+
manifestPath,
|
|
122
|
+
missing: expectedKeys,
|
|
123
|
+
expectedCount: expectedKeys.length,
|
|
124
|
+
distCount: 0,
|
|
125
|
+
reason: `published manifest is not valid JSON: ${error.message}`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const distKeys = new Set(Object.keys(distObjects));
|
|
129
|
+
const missing = expectedKeys.filter((key) => !distKeys.has(key));
|
|
130
|
+
return {
|
|
131
|
+
status: missing.length > 0 ? "incomplete" : "ok",
|
|
132
|
+
packageName,
|
|
133
|
+
manifestPath,
|
|
134
|
+
missing,
|
|
135
|
+
expectedCount: expectedKeys.length,
|
|
136
|
+
distCount: distKeys.size
|
|
137
|
+
};
|
|
153
138
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
e as extractSmrtImports,
|
|
159
|
-
p as parseFile,
|
|
160
|
-
a as parseSource,
|
|
161
|
-
verifyManifestCompleteness
|
|
162
|
-
};
|
|
163
|
-
//# sourceMappingURL=index.js.map
|
|
139
|
+
//#endregion
|
|
140
|
+
export { InheritanceResolver, ManifestAdapter, OxcScanner, extractSmrtImports, parseFile, parseSource, verifyManifestCompleteness };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/verify-completeness.ts"],"sourcesContent":["/**\n * Manifest completeness verification.\n *\n * Re-scans a package's `src/` with the same OXC scanner the build uses and\n * asserts that every `@smrt()`-decorated object (and its collection) is present\n * in the package's published `dist/manifest.json`. This is a publish-time guard:\n * downstream schema migration is manifest-driven, so a stale manifest that omits\n * an object means consumers can never create its table via `smrt db:migrate`.\n *\n * Root cause it guards against (issue #1483): `@happyvertical/smrt-jobs@0.27.41`\n * shipped a `dist/manifest.json` generated before `SmrtWorker` existed. The\n * source, exports, and `__smrt-register__` path were all correct, but the\n * published manifest had only 4 of 6 objects, so `_smrt_workers` was never\n * created and `TaskRunner.start()` threw on every consumer that upgraded.\n *\n * The check compares object-key SETS only. The downstream manifest-enrichment\n * passes (schema/validation/agent generation) mutate object entries but never\n * add or remove object keys, so the scanner + adapter object set is the source\n * of truth for \"which objects the published manifest must contain\". The\n * comparison is `expected ⊆ dist`: enrichment passes that add keys to `dist`\n * (e.g. STI children) never trigger a false failure.\n *\n * Parse errors are handled before the set comparison: a syntactically broken\n * source file drops its objects from `expected` AND `dist` symmetrically, which\n * a naive `expected ⊆ dist` would wave through as `ok`. The scan's\n * `severity:'error'` entries therefore short-circuit to a distinct `scan-error`\n * status so a broken source can never masquerade as a complete manifest.\n *\n * @see https://github.com/happyvertical/smrt/issues/1483\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { basename, resolve } from 'node:path';\nimport { ManifestAdapter } from './manifest-adapter.js';\nimport { OxcScanner } from './scanner.js';\n\n/**\n * Source globs for the completeness scan. The build (`vite.config.base.ts`)\n * passes `include: ['src/**\\/*.ts']` and excludes `*.test.ts` / `*.spec.ts` (the\n * plugin also drops `*.svelte`); we reproduce that so the expected object set\n * matches what the build would publish. We additionally exclude\n * `**\\/__tests__\\/**` so any `@smrt()` fixtures in non-test helper modules under\n * `src/__tests__/` (e.g. `src/__tests__/helpers/*.ts`) are never required in the\n * published manifest. These extra exclusions can only shrink `expected` relative\n * to the build, so they can never cause a false failure — the check is\n * `expected ⊆ dist`.\n */\nconst DEFAULT_INCLUDE = ['src/**/*.ts'];\nconst DEFAULT_EXCLUDE = [\n '**/*.test.ts',\n '**/*.spec.ts',\n '**/__tests__/**',\n '**/*.svelte',\n '**/node_modules/**',\n '**/dist/**',\n '**/*.d.ts',\n];\n\n/**\n * Framework-infrastructure packages whose published manifest is NOT produced by\n * the OXC scanner path (e.g. `@happyvertical/smrt-core` ships a curated static\n * manifest). Mirrors `skipSmrtPlugin` in `vite.config.base.ts`. Keyed by package\n * directory basename.\n */\nconst DEFAULT_SKIP_PACKAGES = [\n 'core',\n 'types',\n 'config',\n 'scanner',\n 'vitest',\n 'smrt-playground',\n];\n\nexport type VerifyManifestStatus =\n | 'ok'\n | 'incomplete'\n | 'missing-manifest'\n | 'scan-error'\n | 'skipped';\n\nexport interface VerifyManifestCompletenessOptions {\n /** Absolute or relative path to the package directory to verify. */\n packageDir: string;\n /** Source include globs (default mirrors the build). */\n include?: string[];\n /** Source exclude globs (default mirrors the build). */\n exclude?: string[];\n /** Package directory basenames to skip (default: framework infrastructure). */\n skipPackages?: string[];\n}\n\nexport interface VerifyManifestCompletenessResult {\n status: VerifyManifestStatus;\n packageName?: string;\n manifestPath?: string;\n /** Qualified object keys present in source but missing from the manifest. */\n missing: string[];\n /** Number of objects the source is expected to contribute. */\n expectedCount: number;\n /** Number of objects present in the published manifest. */\n distCount: number;\n /** Human-readable explanation for `skipped` / `missing-manifest`. */\n reason?: string;\n}\n\ninterface MinimalPackageJson {\n name?: string;\n version?: string;\n exports?: Record<string, unknown>;\n}\n\n/**\n * Resolve the `./manifest` (or `./manifest.json`) export target to an absolute\n * path. Returns `null` when the package does not publish a manifest.\n */\nfunction resolveManifestPath(\n packageDir: string,\n pkgJson: MinimalPackageJson,\n): string | null {\n const exportsMap = pkgJson.exports ?? {};\n const entry = exportsMap['./manifest'] ?? exportsMap['./manifest.json'];\n\n let relativeTarget: string | undefined;\n if (typeof entry === 'string') {\n relativeTarget = entry;\n } else if (entry && typeof entry === 'object') {\n const conditional = entry as Record<string, unknown>;\n const candidate =\n conditional.default ?? conditional.import ?? conditional.types;\n if (typeof candidate === 'string') {\n relativeTarget = candidate;\n }\n }\n\n if (!relativeTarget) {\n return null;\n }\n\n return resolve(packageDir, relativeTarget);\n}\n\n/**\n * Verify that `dist/manifest.json` contains every `@smrt()` object declared in\n * the package source. See module docs for the rationale and guarantees.\n */\nexport async function verifyManifestCompleteness(\n options: VerifyManifestCompletenessOptions,\n): Promise<VerifyManifestCompletenessResult> {\n const packageDir = resolve(options.packageDir);\n const include = options.include ?? DEFAULT_INCLUDE;\n const exclude = options.exclude ?? DEFAULT_EXCLUDE;\n const skipPackages = options.skipPackages ?? DEFAULT_SKIP_PACKAGES;\n\n const packageJsonPath = resolve(packageDir, 'package.json');\n if (!existsSync(packageJsonPath)) {\n return {\n status: 'skipped',\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: `no package.json at ${packageDir}`,\n };\n }\n\n const pkgJson: MinimalPackageJson = JSON.parse(\n readFileSync(packageJsonPath, 'utf-8'),\n );\n const packageName = pkgJson.name;\n\n if (skipPackages.includes(basename(packageDir))) {\n return {\n status: 'skipped',\n packageName,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason:\n 'framework infrastructure package (does not use scanner manifest)',\n };\n }\n\n const manifestPath = resolveManifestPath(packageDir, pkgJson);\n if (!manifestPath) {\n return {\n status: 'skipped',\n packageName,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: 'package does not publish a ./manifest export',\n };\n }\n\n // Derive the object set the source SHOULD produce, using the same scanner and\n // adapter the build uses (vite-plugin scanWithOxc).\n const scanner = new OxcScanner({ cwd: packageDir, include, exclude });\n const { results, resolved } = await scanner.scanAndResolve();\n\n // A source file with a syntax error parses to `errors:[...], body:[]`, so any\n // @smrt() class in it is dropped from BOTH the re-scanned `expected` set and\n // (having never built) the published `dist`. Because the completeness check is\n // `expected ⊆ dist`, the object vanishes symmetrically and the guard would\n // return `ok` — silently defeating the #1483 guarantee for the exact case\n // (broken source) that most needs catching. Surface scan errors as a distinct\n // non-`ok` status instead of computing `expected` from a partial scan.\n const scanErrors = results.errors.filter((e) => e.severity === 'error');\n if (scanErrors.length > 0) {\n const detail = scanErrors\n .map((e) => {\n const where = e.line ? `${e.filePath}:${e.line}` : e.filePath;\n return `${where} — ${e.message}`;\n })\n .join('; ');\n return {\n status: 'scan-error',\n packageName,\n manifestPath,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: `source scan reported ${scanErrors.length} parse error(s): ${detail}`,\n };\n }\n\n const adapter = new ManifestAdapter();\n const expected = adapter.toManifest(resolved, {\n packageName,\n packageVersion: pkgJson.version,\n typeAliases: results.typeAliases,\n });\n const expectedKeys = Object.keys(expected.objects);\n\n // Nothing to verify if the package declares no SMRT objects (e.g. a package\n // that publishes ./manifest but only ships base classes).\n if (expectedKeys.length === 0) {\n return {\n status: 'skipped',\n packageName,\n manifestPath,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: 'no @smrt() objects found in source',\n };\n }\n\n if (!existsSync(manifestPath)) {\n return {\n status: 'missing-manifest',\n packageName,\n manifestPath,\n missing: expectedKeys,\n expectedCount: expectedKeys.length,\n distCount: 0,\n reason: `published manifest not found at ${manifestPath}`,\n };\n }\n\n let distObjects: Record<string, unknown> = {};\n try {\n const distManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n distObjects =\n distManifest && typeof distManifest.objects === 'object'\n ? distManifest.objects\n : {};\n } catch (error) {\n return {\n status: 'missing-manifest',\n packageName,\n manifestPath,\n missing: expectedKeys,\n expectedCount: expectedKeys.length,\n distCount: 0,\n reason: `published manifest is not valid JSON: ${(error as Error).message}`,\n };\n }\n\n const distKeys = new Set(Object.keys(distObjects));\n const missing = expectedKeys.filter((key) => !distKeys.has(key));\n\n return {\n status: missing.length > 0 ? 'incomplete' : 'ok',\n packageName,\n manifestPath,\n missing,\n expectedCount: expectedKeys.length,\n distCount: distKeys.size,\n };\n}\n"],"names":["e"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/verify-completeness.ts"],"sourcesContent":["/**\n * Manifest completeness verification.\n *\n * Re-scans a package's `src/` with the same OXC scanner the build uses and\n * asserts that every `@smrt()`-decorated object (and its collection) is present\n * in the package's published `dist/manifest.json`. This is a publish-time guard:\n * downstream schema migration is manifest-driven, so a stale manifest that omits\n * an object means consumers can never create its table via `smrt db:migrate`.\n *\n * Root cause it guards against (issue #1483): `@happyvertical/smrt-jobs@0.27.41`\n * shipped a `dist/manifest.json` generated before `SmrtWorker` existed. The\n * source, exports, and `__smrt-register__` path were all correct, but the\n * published manifest had only 4 of 6 objects, so `_smrt_workers` was never\n * created and `TaskRunner.start()` threw on every consumer that upgraded.\n *\n * The check compares object-key SETS only. The downstream manifest-enrichment\n * passes (schema/validation/agent generation) mutate object entries but never\n * add or remove object keys, so the scanner + adapter object set is the source\n * of truth for \"which objects the published manifest must contain\". The\n * comparison is `expected ⊆ dist`: enrichment passes that add keys to `dist`\n * (e.g. STI children) never trigger a false failure.\n *\n * Parse errors are handled before the set comparison: a syntactically broken\n * source file drops its objects from `expected` AND `dist` symmetrically, which\n * a naive `expected ⊆ dist` would wave through as `ok`. The scan's\n * `severity:'error'` entries therefore short-circuit to a distinct `scan-error`\n * status so a broken source can never masquerade as a complete manifest.\n *\n * @see https://github.com/happyvertical/smrt/issues/1483\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { basename, resolve } from 'node:path';\nimport { ManifestAdapter } from './manifest-adapter.js';\nimport { OxcScanner } from './scanner.js';\n\n/**\n * Source globs for the completeness scan. The build (`vite.config.base.ts`)\n * passes `include: ['src/**\\/*.ts']` and excludes `*.test.ts` / `*.spec.ts` (the\n * plugin also drops `*.svelte`); we reproduce that so the expected object set\n * matches what the build would publish. We additionally exclude\n * `**\\/__tests__\\/**` so any `@smrt()` fixtures in non-test helper modules under\n * `src/__tests__/` (e.g. `src/__tests__/helpers/*.ts`) are never required in the\n * published manifest. These extra exclusions can only shrink `expected` relative\n * to the build, so they can never cause a false failure — the check is\n * `expected ⊆ dist`.\n */\nconst DEFAULT_INCLUDE = ['src/**/*.ts'];\nconst DEFAULT_EXCLUDE = [\n '**/*.test.ts',\n '**/*.spec.ts',\n '**/__tests__/**',\n '**/*.svelte',\n '**/node_modules/**',\n '**/dist/**',\n '**/*.d.ts',\n];\n\n/**\n * Framework-infrastructure packages whose published manifest is NOT produced by\n * the OXC scanner path (e.g. `@happyvertical/smrt-core` ships a curated static\n * manifest). Mirrors `skipSmrtPlugin` in `vite.config.base.ts`. Keyed by package\n * directory basename.\n */\nconst DEFAULT_SKIP_PACKAGES = [\n 'core',\n 'types',\n 'config',\n 'scanner',\n 'vitest',\n 'smrt-playground',\n];\n\nexport type VerifyManifestStatus =\n | 'ok'\n | 'incomplete'\n | 'missing-manifest'\n | 'scan-error'\n | 'skipped';\n\nexport interface VerifyManifestCompletenessOptions {\n /** Absolute or relative path to the package directory to verify. */\n packageDir: string;\n /** Source include globs (default mirrors the build). */\n include?: string[];\n /** Source exclude globs (default mirrors the build). */\n exclude?: string[];\n /** Package directory basenames to skip (default: framework infrastructure). */\n skipPackages?: string[];\n}\n\nexport interface VerifyManifestCompletenessResult {\n status: VerifyManifestStatus;\n packageName?: string;\n manifestPath?: string;\n /** Qualified object keys present in source but missing from the manifest. */\n missing: string[];\n /** Number of objects the source is expected to contribute. */\n expectedCount: number;\n /** Number of objects present in the published manifest. */\n distCount: number;\n /** Human-readable explanation for `skipped` / `missing-manifest`. */\n reason?: string;\n}\n\ninterface MinimalPackageJson {\n name?: string;\n version?: string;\n exports?: Record<string, unknown>;\n}\n\n/**\n * Resolve the `./manifest` (or `./manifest.json`) export target to an absolute\n * path. Returns `null` when the package does not publish a manifest.\n */\nfunction resolveManifestPath(\n packageDir: string,\n pkgJson: MinimalPackageJson,\n): string | null {\n const exportsMap = pkgJson.exports ?? {};\n const entry = exportsMap['./manifest'] ?? exportsMap['./manifest.json'];\n\n let relativeTarget: string | undefined;\n if (typeof entry === 'string') {\n relativeTarget = entry;\n } else if (entry && typeof entry === 'object') {\n const conditional = entry as Record<string, unknown>;\n const candidate =\n conditional.default ?? conditional.import ?? conditional.types;\n if (typeof candidate === 'string') {\n relativeTarget = candidate;\n }\n }\n\n if (!relativeTarget) {\n return null;\n }\n\n return resolve(packageDir, relativeTarget);\n}\n\n/**\n * Verify that `dist/manifest.json` contains every `@smrt()` object declared in\n * the package source. See module docs for the rationale and guarantees.\n */\nexport async function verifyManifestCompleteness(\n options: VerifyManifestCompletenessOptions,\n): Promise<VerifyManifestCompletenessResult> {\n const packageDir = resolve(options.packageDir);\n const include = options.include ?? DEFAULT_INCLUDE;\n const exclude = options.exclude ?? DEFAULT_EXCLUDE;\n const skipPackages = options.skipPackages ?? DEFAULT_SKIP_PACKAGES;\n\n const packageJsonPath = resolve(packageDir, 'package.json');\n if (!existsSync(packageJsonPath)) {\n return {\n status: 'skipped',\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: `no package.json at ${packageDir}`,\n };\n }\n\n const pkgJson: MinimalPackageJson = JSON.parse(\n readFileSync(packageJsonPath, 'utf-8'),\n );\n const packageName = pkgJson.name;\n\n if (skipPackages.includes(basename(packageDir))) {\n return {\n status: 'skipped',\n packageName,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason:\n 'framework infrastructure package (does not use scanner manifest)',\n };\n }\n\n const manifestPath = resolveManifestPath(packageDir, pkgJson);\n if (!manifestPath) {\n return {\n status: 'skipped',\n packageName,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: 'package does not publish a ./manifest export',\n };\n }\n\n // Derive the object set the source SHOULD produce, using the same scanner and\n // adapter the build uses (vite-plugin scanWithOxc).\n const scanner = new OxcScanner({ cwd: packageDir, include, exclude });\n const { results, resolved } = await scanner.scanAndResolve();\n\n // A source file with a syntax error parses to `errors:[...], body:[]`, so any\n // @smrt() class in it is dropped from BOTH the re-scanned `expected` set and\n // (having never built) the published `dist`. Because the completeness check is\n // `expected ⊆ dist`, the object vanishes symmetrically and the guard would\n // return `ok` — silently defeating the #1483 guarantee for the exact case\n // (broken source) that most needs catching. Surface scan errors as a distinct\n // non-`ok` status instead of computing `expected` from a partial scan.\n const scanErrors = results.errors.filter((e) => e.severity === 'error');\n if (scanErrors.length > 0) {\n const detail = scanErrors\n .map((e) => {\n const where = e.line ? `${e.filePath}:${e.line}` : e.filePath;\n return `${where} — ${e.message}`;\n })\n .join('; ');\n return {\n status: 'scan-error',\n packageName,\n manifestPath,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: `source scan reported ${scanErrors.length} parse error(s): ${detail}`,\n };\n }\n\n const adapter = new ManifestAdapter();\n const expected = adapter.toManifest(resolved, {\n packageName,\n packageVersion: pkgJson.version,\n typeAliases: results.typeAliases,\n });\n const expectedKeys = Object.keys(expected.objects);\n\n // Nothing to verify if the package declares no SMRT objects (e.g. a package\n // that publishes ./manifest but only ships base classes).\n if (expectedKeys.length === 0) {\n return {\n status: 'skipped',\n packageName,\n manifestPath,\n missing: [],\n expectedCount: 0,\n distCount: 0,\n reason: 'no @smrt() objects found in source',\n };\n }\n\n if (!existsSync(manifestPath)) {\n return {\n status: 'missing-manifest',\n packageName,\n manifestPath,\n missing: expectedKeys,\n expectedCount: expectedKeys.length,\n distCount: 0,\n reason: `published manifest not found at ${manifestPath}`,\n };\n }\n\n let distObjects: Record<string, unknown> = {};\n try {\n const distManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n distObjects =\n distManifest && typeof distManifest.objects === 'object'\n ? distManifest.objects\n : {};\n } catch (error) {\n return {\n status: 'missing-manifest',\n packageName,\n manifestPath,\n missing: expectedKeys,\n expectedCount: expectedKeys.length,\n distCount: 0,\n reason: `published manifest is not valid JSON: ${(error as Error).message}`,\n };\n }\n\n const distKeys = new Set(Object.keys(distObjects));\n const missing = expectedKeys.filter((key) => !distKeys.has(key));\n\n return {\n status: missing.length > 0 ? 'incomplete' : 'ok',\n packageName,\n manifestPath,\n missing,\n expectedCount: expectedKeys.length,\n distCount: distKeys.size,\n };\n}\n"],"mappings":";;;;;AA+CA,IAAM,kBAAkB,CAAC,aAAa;AACtC,IAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAQA,IAAM,wBAAwB;CAC5B;CACA;CACA;CACA;CACA;CACA;AACF;AA4CA,SAAS,oBACP,YACA,SACe;CACf,MAAM,aAAa,QAAQ,WAAW,CAAC;CACvC,MAAM,QAAQ,WAAW,iBAAiB,WAAW;CAErD,IAAI;CACJ,IAAI,OAAO,UAAU,UACnB,iBAAiB;MACnB,IAAW,SAAS,OAAO,UAAU,UAAU;EAC7C,MAAM,cAAc;EACpB,MAAM,YACJ,YAAY,WAAW,YAAY,UAAU,YAAY;EAC3D,IAAI,OAAO,cAAc,UACvB,iBAAiB;CAErB;CAEA,IAAI,CAAC,gBACH,OAAO;CAGT,OAAO,QAAQ,YAAY,cAAc;AAC3C;AAMA,eAAsB,2BACpB,SAC2C;CAC3C,MAAM,aAAa,QAAQ,QAAQ,UAAU;CAC7C,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,eAAe,QAAQ,gBAAgB;CAE7C,MAAM,kBAAkB,QAAQ,YAAY,cAAc;CAC1D,IAAI,CAAC,WAAW,eAAe,GAC7B,OAAO;EACL,QAAQ;EACR,SAAS,CAAC;EACV,eAAe;EACf,WAAW;EACX,QAAQ,sBAAsB;CAChC;CAGF,MAAM,UAA8B,KAAK,MACvC,aAAa,iBAAiB,OAAO,CACvC;CACA,MAAM,cAAc,QAAQ;CAE5B,IAAI,aAAa,SAAS,SAAS,UAAU,CAAC,GAC5C,OAAO;EACL,QAAQ;EACR;EACA,SAAS,CAAC;EACV,eAAe;EACf,WAAW;EACX,QACE;CACJ;CAGF,MAAM,eAAe,oBAAoB,YAAY,OAAO;CAC5D,IAAI,CAAC,cACH,OAAO;EACL,QAAQ;EACR;EACA,SAAS,CAAC;EACV,eAAe;EACf,WAAW;EACX,QAAQ;CACV;CAMF,MAAM,EAAE,SAAS,aAAa,MAAM,IADhB,WAAW;EAAE,KAAK;EAAY;EAAS;CAAQ,CAC/B,CAAA,CAAQ,eAAe;CAS3D,MAAM,aAAa,QAAQ,OAAO,QAAQ,MAAM,EAAE,aAAa,OAAO;CACtE,IAAI,WAAW,SAAS,GAAG;EACzB,MAAM,SAAS,WACZ,KAAK,MAAM;GAEV,OAAO,GADO,EAAE,OAAO,GAAG,EAAE,SAAQ,GAAI,EAAE,SAAS,EAAE,SACtC,UAAM,EAAE;EACzB,CAAC,CAAA,CACA,KAAK,IAAI;EACZ,OAAO;GACL,QAAQ;GACR;GACA;GACA,SAAS,CAAC;GACV,eAAe;GACf,WAAW;GACX,QAAQ,wBAAwB,WAAW,OAAM,mBAAoB;EACvE;CACF;CAGA,MAAM,WAAW,IADG,gBACH,CAAA,CAAQ,WAAW,UAAU;EAC5C;EACA,gBAAgB,QAAQ;EACxB,aAAa,QAAQ;CACvB,CAAC;CACD,MAAM,eAAe,OAAO,KAAK,SAAS,OAAO;CAIjD,IAAI,aAAa,WAAW,GAC1B,OAAO;EACL,QAAQ;EACR;EACA;EACA,SAAS,CAAC;EACV,eAAe;EACf,WAAW;EACX,QAAQ;CACV;CAGF,IAAI,CAAC,WAAW,YAAY,GAC1B,OAAO;EACL,QAAQ;EACR;EACA;EACA,SAAS;EACT,eAAe,aAAa;EAC5B,WAAW;EACX,QAAQ,mCAAmC;CAC7C;CAGF,IAAI,cAAuC,CAAC;CAC5C,IAAI;EACF,MAAM,eAAe,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;EACnE,cACE,gBAAgB,OAAO,aAAa,YAAY,WAC5C,aAAa,UACb,CAAC;CACT,SAAS,OAAO;EACd,OAAO;GACL,QAAQ;GACR;GACA;GACA,SAAS;GACT,eAAe,aAAa;GAC5B,WAAW;GACX,QAAQ,yCAA0C,MAAgB;EACpE;CACF;CAEA,MAAM,WAAW,IAAI,IAAI,OAAO,KAAK,WAAW,CAAC;CACjD,MAAM,UAAU,aAAa,QAAQ,QAAQ,CAAC,SAAS,IAAI,GAAG,CAAC;CAE/D,OAAO;EACL,QAAQ,QAAQ,SAAS,IAAI,eAAe;EAC5C;EACA;EACA;EACA,eAAe,aAAa;EAC5B,WAAW,SAAS;CACtB;AACF"}
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-scanner",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.4",
|
|
4
4
|
"description": "High-performance TypeScript scanner using OXC for SMRT manifest generation",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"fast-glob": "3.3.3",
|
|
34
34
|
"minimatch": "10.2.5",
|
|
35
|
-
"oxc-parser": "^0.
|
|
36
|
-
"oxc-resolver": "^11.
|
|
35
|
+
"oxc-parser": "^0.138.0",
|
|
36
|
+
"oxc-resolver": "^11.22.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "
|
|
40
|
-
"tsx": "^4.
|
|
41
|
-
"vite": "
|
|
42
|
-
"vitest": "^4.
|
|
39
|
+
"@types/node": "24.13.2",
|
|
40
|
+
"tsx": "^4.22.4",
|
|
41
|
+
"vite": "8.1.2",
|
|
42
|
+
"vitest": "^4.1.9"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"registry": "https://registry.npmjs.org",
|