@topogram/cli 0.3.79 → 0.3.80
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/package.json +1 -1
- package/src/cli/command-parser.js +2 -0
- package/src/cli/command-parsers/extractor.js +40 -0
- package/src/cli/commands/extractor.js +451 -0
- package/src/cli/commands/import/help.js +3 -1
- package/src/cli/commands/import/workspace.js +8 -2
- package/src/cli/commands/import-runner.js +4 -2
- package/src/cli/dispatcher.js +14 -0
- package/src/cli/help-dispatch.js +11 -0
- package/src/cli/help.js +12 -1
- package/src/cli/options.js +17 -0
- package/src/extractor/check.js +155 -0
- package/src/extractor/packages.js +295 -0
- package/src/extractor/registry.js +196 -0
- package/src/extractor-policy.js +249 -0
- package/src/generator/check.js +24 -87
- package/src/generator/registry/index.js +16 -75
- package/src/generator-policy.js +9 -57
- package/src/import/core/registry.d.ts +3 -0
- package/src/import/core/registry.js +82 -8
- package/src/import/core/runner/run.js +2 -0
- package/src/import/core/runner/tracks.js +66 -4
- package/src/import/provenance.js +2 -1
- package/src/package-adapters/adapter.js +64 -0
- package/src/package-adapters/file-map.js +30 -0
- package/src/package-adapters/index.js +27 -0
- package/src/package-adapters/manifest.js +108 -0
- package/src/package-adapters/policy.js +81 -0
- package/src/package-adapters/spec.js +51 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
loadPackageManifest,
|
|
5
|
+
packageInstallCommand,
|
|
6
|
+
packageInstallHint,
|
|
7
|
+
resolvePackageManifestPath
|
|
8
|
+
} from "../../package-adapters/index.js";
|
|
6
9
|
import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../../ui/taxonomy.js";
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -208,20 +211,12 @@ export function getGeneratorManifest(generatorId) {
|
|
|
208
211
|
return GENERATOR_BY_ID.get(generatorId) || null;
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
/**
|
|
212
|
-
* @param {string|null|undefined} rootDir
|
|
213
|
-
* @returns {string}
|
|
214
|
-
*/
|
|
215
|
-
function packageResolutionBase(rootDir) {
|
|
216
|
-
return path.join(rootDir || process.cwd(), "package.json");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
214
|
/**
|
|
220
215
|
* @param {string|null|undefined} packageName
|
|
221
216
|
* @returns {string|null}
|
|
222
217
|
*/
|
|
223
218
|
export function packageGeneratorInstallCommand(packageName) {
|
|
224
|
-
return packageName
|
|
219
|
+
return packageInstallCommand(packageName);
|
|
225
220
|
}
|
|
226
221
|
|
|
227
222
|
/**
|
|
@@ -229,8 +224,7 @@ export function packageGeneratorInstallCommand(packageName) {
|
|
|
229
224
|
* @returns {string|null}
|
|
230
225
|
*/
|
|
231
226
|
export function packageGeneratorInstallHint(packageName) {
|
|
232
|
-
|
|
233
|
-
return command ? `Install it from the project root with: ${command}` : null;
|
|
227
|
+
return packageInstallHint(packageName);
|
|
234
228
|
}
|
|
235
229
|
|
|
236
230
|
/**
|
|
@@ -239,41 +233,7 @@ export function packageGeneratorInstallHint(packageName) {
|
|
|
239
233
|
* @returns {{ manifestPath: string|null, packageRoot: string|null, error: string|null }}
|
|
240
234
|
*/
|
|
241
235
|
export function resolvePackageGeneratorManifestPath(packageName, rootDir = process.cwd()) {
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const manifestPath = requireFromRoot.resolve(`${packageName}/topogram-generator.json`);
|
|
245
|
-
return {
|
|
246
|
-
manifestPath,
|
|
247
|
-
packageRoot: path.dirname(manifestPath),
|
|
248
|
-
error: null
|
|
249
|
-
};
|
|
250
|
-
} catch (manifestError) {
|
|
251
|
-
try {
|
|
252
|
-
const packageJsonPath = requireFromRoot.resolve(`${packageName}/package.json`);
|
|
253
|
-
const packageRoot = path.dirname(packageJsonPath);
|
|
254
|
-
const manifestPath = path.join(packageRoot, "topogram-generator.json");
|
|
255
|
-
if (!fs.existsSync(manifestPath)) {
|
|
256
|
-
return {
|
|
257
|
-
manifestPath: null,
|
|
258
|
-
packageRoot,
|
|
259
|
-
error: `Generator package '${packageName}' is missing topogram-generator.json`
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
return {
|
|
263
|
-
manifestPath,
|
|
264
|
-
packageRoot,
|
|
265
|
-
error: null
|
|
266
|
-
};
|
|
267
|
-
} catch {
|
|
268
|
-
const detail = manifestError instanceof Error ? manifestError.message : String(manifestError);
|
|
269
|
-
const installHint = packageGeneratorInstallHint(packageName);
|
|
270
|
-
return {
|
|
271
|
-
manifestPath: null,
|
|
272
|
-
packageRoot: null,
|
|
273
|
-
error: `Generator package '${packageName}' could not be resolved from '${rootDir || process.cwd()}': ${detail}${installHint ? `. ${installHint}` : ""}`
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
236
|
+
return resolvePackageManifestPath(packageName, "topogram-generator.json", rootDir, "Generator package");
|
|
277
237
|
}
|
|
278
238
|
|
|
279
239
|
/**
|
|
@@ -282,32 +242,13 @@ export function resolvePackageGeneratorManifestPath(packageName, rootDir = proce
|
|
|
282
242
|
* @returns {{ manifest: GeneratorManifest|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
|
|
283
243
|
*/
|
|
284
244
|
export function loadPackageGeneratorManifest(packageName, rootDir = process.cwd()) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
try {
|
|
295
|
-
const manifest = JSON.parse(fs.readFileSync(resolved.manifestPath, "utf8"));
|
|
296
|
-
const validation = validateGeneratorManifest(manifest);
|
|
297
|
-
return {
|
|
298
|
-
manifest: validation.ok ? manifest : null,
|
|
299
|
-
errors: validation.errors,
|
|
300
|
-
manifestPath: resolved.manifestPath,
|
|
301
|
-
packageRoot: resolved.packageRoot
|
|
302
|
-
};
|
|
303
|
-
} catch (error) {
|
|
304
|
-
return {
|
|
305
|
-
manifest: null,
|
|
306
|
-
errors: [`Generator package '${packageName}' manifest could not be read: ${error instanceof Error ? error.message : String(error)}`],
|
|
307
|
-
manifestPath: resolved.manifestPath,
|
|
308
|
-
packageRoot: resolved.packageRoot
|
|
309
|
-
};
|
|
310
|
-
}
|
|
245
|
+
return loadPackageManifest({
|
|
246
|
+
packageName,
|
|
247
|
+
rootDir,
|
|
248
|
+
manifestFile: "topogram-generator.json",
|
|
249
|
+
packageLabel: "Generator package",
|
|
250
|
+
validateManifest: validateGeneratorManifest
|
|
251
|
+
});
|
|
311
252
|
}
|
|
312
253
|
|
|
313
254
|
/**
|
package/src/generator-policy.js
CHANGED
|
@@ -4,6 +4,12 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { stableStringify } from "./format.js";
|
|
7
|
+
import {
|
|
8
|
+
optionalStringArray,
|
|
9
|
+
optionalStringRecord,
|
|
10
|
+
packageAllowedByPolicy,
|
|
11
|
+
packageScopeFromName as sharedPackageScopeFromName
|
|
12
|
+
} from "./package-adapters/index.js";
|
|
7
13
|
|
|
8
14
|
export const GENERATOR_POLICY_FILE = "topogram.generator-policy.json";
|
|
9
15
|
|
|
@@ -66,47 +72,6 @@ function generatorPolicyDiagnostic(input) {
|
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
/**
|
|
70
|
-
* @param {unknown} value
|
|
71
|
-
* @param {string} fieldName
|
|
72
|
-
* @param {string} policyPath
|
|
73
|
-
* @returns {string[]}
|
|
74
|
-
*/
|
|
75
|
-
function optionalStringArray(value, fieldName, policyPath) {
|
|
76
|
-
if (value == null) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
if (!Array.isArray(value)) {
|
|
80
|
-
throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
|
|
81
|
-
}
|
|
82
|
-
return value.map((item) => {
|
|
83
|
-
if (typeof item !== "string" || item.length === 0) {
|
|
84
|
-
throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
|
|
85
|
-
}
|
|
86
|
-
return item;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @param {unknown} value
|
|
92
|
-
* @param {string} policyPath
|
|
93
|
-
* @returns {Record<string, string>}
|
|
94
|
-
*/
|
|
95
|
-
function optionalStringRecord(value, policyPath) {
|
|
96
|
-
if (value == null) {
|
|
97
|
-
return {};
|
|
98
|
-
}
|
|
99
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
100
|
-
throw new Error(`${policyPath} pinnedVersions must be an object of package-or-generator ids to versions.`);
|
|
101
|
-
}
|
|
102
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
103
|
-
if (typeof item !== "string" || item.length === 0) {
|
|
104
|
-
throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
|
|
105
|
-
}
|
|
106
|
-
return [key, item];
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
75
|
/**
|
|
111
76
|
* @returns {GeneratorPolicy}
|
|
112
77
|
*/
|
|
@@ -136,7 +101,7 @@ export function validateGeneratorPolicy(value, policyPath) {
|
|
|
136
101
|
? defaults.allowedPackageScopes
|
|
137
102
|
: optionalStringArray(raw.allowedPackageScopes, "allowedPackageScopes", policyPath),
|
|
138
103
|
allowedPackages: optionalStringArray(raw.allowedPackages, "allowedPackages", policyPath),
|
|
139
|
-
pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath)
|
|
104
|
+
pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath, "package-or-generator ids")
|
|
140
105
|
};
|
|
141
106
|
}
|
|
142
107
|
|
|
@@ -145,16 +110,7 @@ export function validateGeneratorPolicy(value, policyPath) {
|
|
|
145
110
|
* @returns {string|null}
|
|
146
111
|
*/
|
|
147
112
|
export function packageScopeFromName(packageName) {
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {string} allowed
|
|
153
|
-
* @param {string|null} scope
|
|
154
|
-
* @returns {boolean}
|
|
155
|
-
*/
|
|
156
|
-
function packageScopeMatches(allowed, scope) {
|
|
157
|
-
return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
|
|
113
|
+
return sharedPackageScopeFromName(packageName);
|
|
158
114
|
}
|
|
159
115
|
|
|
160
116
|
/**
|
|
@@ -163,11 +119,7 @@ function packageScopeMatches(allowed, scope) {
|
|
|
163
119
|
* @returns {boolean}
|
|
164
120
|
*/
|
|
165
121
|
export function generatorPackageAllowed(policy, packageName) {
|
|
166
|
-
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
const scope = packageScopeFromName(packageName);
|
|
170
|
-
return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
|
|
122
|
+
return packageAllowedByPolicy(policy, packageName);
|
|
171
123
|
}
|
|
172
124
|
|
|
173
125
|
/**
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const extractorRegistry: Record<string, any[]>;
|
|
2
2
|
export const enricherRegistry: Record<string, any[]>;
|
|
3
|
+
export const BUILTIN_EXTRACTOR_PACKS: any[];
|
|
3
4
|
export function getExtractorsForTrack(...args: any[]): any[];
|
|
4
5
|
export function getEnrichersForTrack(...args: any[]): any[];
|
|
6
|
+
export function getBundledExtractorPack(...args: any[]): any;
|
|
7
|
+
export function getBundledExtractorById(...args: any[]): any;
|
|
@@ -57,14 +57,80 @@ import { railsControllerEnricher } from "../enrichers/rails-controllers.js";
|
|
|
57
57
|
import { workflowTargetStateEnricher } from "../enrichers/workflow-target-state.js";
|
|
58
58
|
import { docLinkingEnricher } from "../enrichers/doc-linking.js";
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
function extractorPack(id, tracks, extractors, candidateKinds, stack = {}, capabilities = {}) {
|
|
61
|
+
return {
|
|
62
|
+
manifest: {
|
|
63
|
+
id,
|
|
64
|
+
version: "1",
|
|
65
|
+
tracks,
|
|
66
|
+
source: "bundled",
|
|
67
|
+
extractors: extractors.map((extractor) => extractor.id),
|
|
68
|
+
stack,
|
|
69
|
+
capabilities,
|
|
70
|
+
candidateKinds,
|
|
71
|
+
evidenceTypes: ["runtime_source", "parser_config", "docs", "tests", "fixtures"]
|
|
72
|
+
},
|
|
73
|
+
extractors
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const BUILTIN_EXTRACTOR_PACKS = [
|
|
78
|
+
extractorPack(
|
|
79
|
+
"topogram/db-extractors",
|
|
80
|
+
["db"],
|
|
81
|
+
[prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
|
|
82
|
+
["entity", "enum", "relation", "index", "maintained_db_migration_seam"],
|
|
83
|
+
{ domain: "database" },
|
|
84
|
+
{ schema: true, migrations: true, maintainedSeams: true }
|
|
85
|
+
),
|
|
86
|
+
extractorPack(
|
|
87
|
+
"topogram/api-extractors",
|
|
88
|
+
["api"],
|
|
89
|
+
[openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
|
|
90
|
+
["capability", "route", "stack"],
|
|
91
|
+
{ domain: "api" },
|
|
92
|
+
{ routes: true, openapi: true, graphql: true }
|
|
93
|
+
),
|
|
94
|
+
extractorPack(
|
|
95
|
+
"topogram/ui-extractors",
|
|
96
|
+
["ui"],
|
|
97
|
+
[nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
|
|
98
|
+
["screen", "route", "action", "flow", "widget", "shape", "stack"],
|
|
99
|
+
{ domain: "ui" },
|
|
100
|
+
{ screens: true, widgets: true, flows: true }
|
|
101
|
+
),
|
|
102
|
+
extractorPack(
|
|
103
|
+
"topogram/cli-extractors",
|
|
104
|
+
["cli"],
|
|
105
|
+
[genericCliExtractor],
|
|
106
|
+
["command", "capability", "cli_surface"],
|
|
107
|
+
{ domain: "cli" },
|
|
108
|
+
{ commands: true, options: true, effects: true }
|
|
109
|
+
),
|
|
110
|
+
extractorPack(
|
|
111
|
+
"topogram/workflow-extractors",
|
|
112
|
+
["workflows"],
|
|
113
|
+
[genericWorkflowExtractor],
|
|
114
|
+
["workflow", "workflow_state", "workflow_transition"],
|
|
115
|
+
{ domain: "workflow" },
|
|
116
|
+
{ workflows: true }
|
|
117
|
+
),
|
|
118
|
+
extractorPack(
|
|
119
|
+
"topogram/verification-extractors",
|
|
120
|
+
["verification"],
|
|
121
|
+
[genericVerificationExtractor],
|
|
122
|
+
["verification", "scenario", "framework", "script"],
|
|
123
|
+
{ domain: "verification" },
|
|
124
|
+
{ verifications: true }
|
|
125
|
+
)
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export const extractorRegistry = Object.fromEntries(
|
|
129
|
+
["db", "api", "ui", "cli", "workflows", "verification"].map((track) => [
|
|
130
|
+
track,
|
|
131
|
+
BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).filter((extractor) => extractor.track === track)
|
|
132
|
+
])
|
|
133
|
+
);
|
|
68
134
|
|
|
69
135
|
export const enricherRegistry = {
|
|
70
136
|
db: [railsModelEnricher],
|
|
@@ -82,3 +148,11 @@ export function getExtractorsForTrack(track) {
|
|
|
82
148
|
export function getEnrichersForTrack(track) {
|
|
83
149
|
return enricherRegistry[track] || [];
|
|
84
150
|
}
|
|
151
|
+
|
|
152
|
+
export function getBundledExtractorPack(id) {
|
|
153
|
+
return BUILTIN_EXTRACTOR_PACKS.find((pack) => pack.manifest.id === id) || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getBundledExtractorById(id) {
|
|
157
|
+
return BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).find((extractor) => extractor.id === id) || null;
|
|
158
|
+
}
|
|
@@ -6,6 +6,7 @@ import { parseImportTracks } from "./options.js";
|
|
|
6
6
|
import { appReportMarkdown, reportMarkdown } from "./reports.js";
|
|
7
7
|
import { runTrack } from "./tracks.js";
|
|
8
8
|
import { draftUiProjectionFiles } from "./ui-drafts.js";
|
|
9
|
+
import { packageExtractorsForContext } from "../../../extractor/packages.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {string} inputPath
|
|
@@ -64,6 +65,7 @@ export function runImportApp(inputPath, options = {}) {
|
|
|
64
65
|
tracks,
|
|
65
66
|
findings_count: Object.values(findings).reduce((total, entries) => total + entries.length, 0),
|
|
66
67
|
extractor_detections: Object.fromEntries(Object.entries(resultsByTrack).map(([track, result]) => [track, result.extractor_detections])),
|
|
68
|
+
package_extractors: packageExtractorsForContext(context).provenance,
|
|
67
69
|
candidates
|
|
68
70
|
};
|
|
69
71
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { getEnrichersForTrack, getExtractorsForTrack } from "../registry.js";
|
|
4
4
|
import { normalizeCandidatesForTrack } from "./candidates.js";
|
|
5
|
+
import { packageExtractorsForContext } from "../../../extractor/packages.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @param {any} context
|
|
@@ -10,7 +11,12 @@ import { normalizeCandidatesForTrack } from "./candidates.js";
|
|
|
10
11
|
*/
|
|
11
12
|
function sortExtractors(context, extractors) {
|
|
12
13
|
return extractors
|
|
13
|
-
.map((extractor) =>
|
|
14
|
+
.map((extractor) => {
|
|
15
|
+
const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
|
|
16
|
+
? extractor.packageContext(context)
|
|
17
|
+
: context;
|
|
18
|
+
return { extractor, detection: extractor.detect(extractorContext) || { score: 0, reasons: [] } };
|
|
19
|
+
})
|
|
14
20
|
.filter((entry) => entry.detection.score > 0)
|
|
15
21
|
.sort((a, b) => b.detection.score - a.detection.score || a.extractor.id.localeCompare(b.extractor.id));
|
|
16
22
|
}
|
|
@@ -20,7 +26,7 @@ function sortExtractors(context, extractors) {
|
|
|
20
26
|
* @param {Array<{ extractor: any, detection: any }>} detections
|
|
21
27
|
* @returns {Array<{ extractor: any, detection: any }>}
|
|
22
28
|
*/
|
|
23
|
-
function
|
|
29
|
+
function selectBundledDetectionsForTrack(track, detections) {
|
|
24
30
|
if (track === "db") {
|
|
25
31
|
const prisma = detections.find((entry) => entry.extractor.id === "db.prisma");
|
|
26
32
|
if (prisma) return [prisma];
|
|
@@ -90,6 +96,20 @@ function selectDetectionsForTrack(track, detections) {
|
|
|
90
96
|
return detections;
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} track
|
|
101
|
+
* @param {Array<{ extractor: any, detection: any }>} detections
|
|
102
|
+
* @returns {Array<{ extractor: any, detection: any }>}
|
|
103
|
+
*/
|
|
104
|
+
function selectDetectionsForTrack(track, detections) {
|
|
105
|
+
const packageDetections = detections.filter((entry) => entry.extractor.source === "package");
|
|
106
|
+
const bundledDetections = selectBundledDetectionsForTrack(
|
|
107
|
+
track,
|
|
108
|
+
detections.filter((entry) => entry.extractor.source !== "package")
|
|
109
|
+
);
|
|
110
|
+
return [...bundledDetections, ...packageDetections];
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
/**
|
|
94
114
|
* @param {string} track
|
|
95
115
|
* @returns {any}
|
|
@@ -113,6 +133,35 @@ function initialCandidatesForTrack(track) {
|
|
|
113
133
|
return { workflows: [], workflow_states: [], workflow_transitions: [] };
|
|
114
134
|
}
|
|
115
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @param {any} extractor
|
|
138
|
+
* @param {any} result
|
|
139
|
+
* @returns {void}
|
|
140
|
+
*/
|
|
141
|
+
function assertExtractorResultShape(extractor, result) {
|
|
142
|
+
const label = extractor?.id || "unknown";
|
|
143
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
144
|
+
throw new Error(`Extractor '${label}' extract(context) must return an object.`);
|
|
145
|
+
}
|
|
146
|
+
if (result.findings != null && !Array.isArray(result.findings)) {
|
|
147
|
+
throw new Error(`Extractor '${label}' extract(context) findings must be an array when present.`);
|
|
148
|
+
}
|
|
149
|
+
if (result.diagnostics != null && !Array.isArray(result.diagnostics)) {
|
|
150
|
+
throw new Error(`Extractor '${label}' extract(context) diagnostics must be an array when present.`);
|
|
151
|
+
}
|
|
152
|
+
if (result.candidates == null) {
|
|
153
|
+
result.candidates = {};
|
|
154
|
+
}
|
|
155
|
+
if (!result.candidates || typeof result.candidates !== "object" || Array.isArray(result.candidates)) {
|
|
156
|
+
throw new Error(`Extractor '${label}' extract(context) candidates must be an object.`);
|
|
157
|
+
}
|
|
158
|
+
for (const [key, value] of Object.entries(result.candidates)) {
|
|
159
|
+
if (!Array.isArray(value)) {
|
|
160
|
+
throw new Error(`Extractor '${label}' extract(context) candidates.${key} must be an array.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
116
165
|
/**
|
|
117
166
|
* @param {any} context
|
|
118
167
|
* @param {string} track
|
|
@@ -121,9 +170,22 @@ function initialCandidatesForTrack(track) {
|
|
|
121
170
|
export function runTrack(context, track) {
|
|
122
171
|
const findings = [];
|
|
123
172
|
const rawCandidates = initialCandidatesForTrack(track);
|
|
173
|
+
const packageState = packageExtractorsForContext(context);
|
|
174
|
+
const packageErrors = packageState.diagnostics.filter((diagnostic) => diagnostic.severity !== "warning");
|
|
175
|
+
if (packageErrors.length > 0) {
|
|
176
|
+
throw new Error(packageErrors.map((diagnostic) => diagnostic.message || String(diagnostic)).join("\n"));
|
|
177
|
+
}
|
|
178
|
+
const extractors = [
|
|
179
|
+
...getExtractorsForTrack(track),
|
|
180
|
+
...packageState.extractors.filter((extractor) => extractor.track === track)
|
|
181
|
+
];
|
|
124
182
|
|
|
125
|
-
for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context,
|
|
126
|
-
const
|
|
183
|
+
for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, extractors))) {
|
|
184
|
+
const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
|
|
185
|
+
? extractor.packageContext(context)
|
|
186
|
+
: context;
|
|
187
|
+
const result = extractor.extract(extractorContext) || { findings: [], candidates: {} };
|
|
188
|
+
assertExtractorResultShape(extractor, result);
|
|
127
189
|
findings.push({
|
|
128
190
|
extractor: extractor.id,
|
|
129
191
|
detection,
|
package/src/import/provenance.js
CHANGED
|
@@ -53,7 +53,8 @@ export function writeTopogramImportRecord(projectRoot, input) {
|
|
|
53
53
|
extract: {
|
|
54
54
|
tracks: input.tracks || [],
|
|
55
55
|
findingsCount: input.findingsCount || 0,
|
|
56
|
-
candidateCounts: input.candidateCounts || {}
|
|
56
|
+
candidateCounts: input.candidateCounts || {},
|
|
57
|
+
extractorPackages: input.extractorPackages || []
|
|
57
58
|
},
|
|
58
59
|
ownership: {
|
|
59
60
|
extractedArtifacts: "project-owned",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {any} moduleValue
|
|
8
|
+
* @param {string|null|undefined} exportName
|
|
9
|
+
* @returns {any}
|
|
10
|
+
*/
|
|
11
|
+
export function selectPackageExport(moduleValue, exportName) {
|
|
12
|
+
if (exportName) {
|
|
13
|
+
return moduleValue?.[exportName] || moduleValue?.default?.[exportName] || null;
|
|
14
|
+
}
|
|
15
|
+
return moduleValue?.default || moduleValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{
|
|
20
|
+
* packageRoot: string,
|
|
21
|
+
* exportName?: string|null,
|
|
22
|
+
* packageLabel: string
|
|
23
|
+
* }} options
|
|
24
|
+
* @returns {{ adapter: any|null, error: string|null }}
|
|
25
|
+
*/
|
|
26
|
+
export function loadLocalPackageAdapter(options) {
|
|
27
|
+
try {
|
|
28
|
+
const packageJsonPath = path.join(options.packageRoot, "package.json");
|
|
29
|
+
const requireFromPackage = createRequire(packageJsonPath);
|
|
30
|
+
return {
|
|
31
|
+
adapter: selectPackageExport(requireFromPackage(options.packageRoot), options.exportName),
|
|
32
|
+
error: null
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
adapter: null,
|
|
37
|
+
error: `${options.packageLabel} export could not be loaded from '${options.packageRoot}': ${error instanceof Error ? error.message : String(error)}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {{
|
|
44
|
+
* packageName: string,
|
|
45
|
+
* rootDir: string,
|
|
46
|
+
* exportName?: string|null,
|
|
47
|
+
* packageLabel: string
|
|
48
|
+
* }} options
|
|
49
|
+
* @returns {{ adapter: any|null, error: string|null }}
|
|
50
|
+
*/
|
|
51
|
+
export function loadInstalledPackageAdapter(options) {
|
|
52
|
+
try {
|
|
53
|
+
const requireFromRoot = createRequire(path.join(options.rootDir, "package.json"));
|
|
54
|
+
return {
|
|
55
|
+
adapter: selectPackageExport(requireFromRoot(options.packageName), options.exportName),
|
|
56
|
+
error: null
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
adapter: null,
|
|
61
|
+
error: `${options.packageLabel} '${options.packageName}' export could not be loaded from '${options.rootDir}': ${error instanceof Error ? error.message : String(error)}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {any} files
|
|
7
|
+
* @param {{ filePathMessage: string, contentMessage: (filePath: string) => string }} messages
|
|
8
|
+
* @returns {{ ok: boolean, message: string }}
|
|
9
|
+
*/
|
|
10
|
+
export function validateRelativeStringFileMap(files, messages) {
|
|
11
|
+
if (!files || typeof files !== "object" || Array.isArray(files)) {
|
|
12
|
+
return { ok: false, message: "files must be an object" };
|
|
13
|
+
}
|
|
14
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
15
|
+
const normalizedPath = typeof filePath === "string" ? path.normalize(filePath) : "";
|
|
16
|
+
if (
|
|
17
|
+
typeof filePath !== "string" ||
|
|
18
|
+
filePath.length === 0 ||
|
|
19
|
+
path.isAbsolute(filePath) ||
|
|
20
|
+
normalizedPath === ".." ||
|
|
21
|
+
normalizedPath.startsWith(`..${path.sep}`)
|
|
22
|
+
) {
|
|
23
|
+
return { ok: false, message: messages.filePathMessage };
|
|
24
|
+
}
|
|
25
|
+
if (typeof content !== "string") {
|
|
26
|
+
return { ok: false, message: messages.contentMessage(filePath) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, message: "files are valid" };
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
loadInstalledPackageAdapter,
|
|
5
|
+
loadLocalPackageAdapter,
|
|
6
|
+
selectPackageExport
|
|
7
|
+
} from "./adapter.js";
|
|
8
|
+
export {
|
|
9
|
+
loadPackageManifest,
|
|
10
|
+
resolvePackageManifestPath
|
|
11
|
+
} from "./manifest.js";
|
|
12
|
+
export {
|
|
13
|
+
isPathSpec,
|
|
14
|
+
packageInstallCommand,
|
|
15
|
+
packageInstallHint,
|
|
16
|
+
packageNameFromSpec,
|
|
17
|
+
packageResolutionBase
|
|
18
|
+
} from "./spec.js";
|
|
19
|
+
export {
|
|
20
|
+
optionalStringArray,
|
|
21
|
+
optionalStringRecord,
|
|
22
|
+
packageAllowedByPolicy,
|
|
23
|
+
packageScopeFromName
|
|
24
|
+
} from "./policy.js";
|
|
25
|
+
export {
|
|
26
|
+
validateRelativeStringFileMap
|
|
27
|
+
} from "./file-map.js";
|