@topogram/cli 0.3.78 → 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.
Files changed (100) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +2 -2
  3. package/src/agent-brief.js +29 -23
  4. package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
  5. package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
  6. package/src/agent-ops/query-builders/change-risk.js +1 -1
  7. package/src/agent-ops/query-builders/common.js +2 -2
  8. package/src/agent-ops/query-builders/multi-agent.js +1 -1
  9. package/src/agent-ops/query-builders/workflow-context-shared.js +4 -4
  10. package/src/catalog/provenance.js +1 -1
  11. package/src/cli/catalog-alias.d.ts +2 -0
  12. package/src/cli/catalog-alias.js +2 -2
  13. package/src/cli/command-parser.js +2 -0
  14. package/src/cli/command-parsers/core.js +9 -5
  15. package/src/cli/command-parsers/extractor.js +40 -0
  16. package/src/cli/command-parsers/import.js +11 -17
  17. package/src/cli/command-parsers/project.js +0 -3
  18. package/src/cli/commands/catalog/copy.js +3 -3
  19. package/src/cli/commands/catalog/help.js +1 -2
  20. package/src/cli/commands/catalog/list.js +7 -4
  21. package/src/cli/commands/catalog/show.js +4 -4
  22. package/src/cli/commands/copy.js +356 -0
  23. package/src/cli/commands/doctor.js +1 -1
  24. package/src/cli/commands/extractor.js +451 -0
  25. package/src/cli/commands/import/adopt.js +9 -9
  26. package/src/cli/commands/import/check.js +15 -15
  27. package/src/cli/commands/import/diff.js +6 -6
  28. package/src/cli/commands/import/help.js +45 -34
  29. package/src/cli/commands/import/paths.js +3 -3
  30. package/src/cli/commands/import/plan.js +8 -8
  31. package/src/cli/commands/import/refresh.js +25 -24
  32. package/src/cli/commands/import/status-history.js +4 -4
  33. package/src/cli/commands/import/workspace.js +24 -18
  34. package/src/cli/commands/import-runner.js +10 -7
  35. package/src/cli/commands/import.js +4 -1
  36. package/src/cli/commands/init.js +67 -0
  37. package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
  38. package/src/cli/commands/query/runner/change.js +2 -2
  39. package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
  40. package/src/cli/commands/query/runner/index.js +1 -1
  41. package/src/cli/commands/query/runner/workflow.js +7 -7
  42. package/src/cli/commands/query/workspace.js +4 -4
  43. package/src/cli/commands/release-status.js +2 -2
  44. package/src/cli/commands/source.js +2 -2
  45. package/src/cli/commands/template/check.js +2 -2
  46. package/src/cli/commands/template/list-show.js +4 -4
  47. package/src/cli/dispatcher.js +32 -3
  48. package/src/cli/help-dispatch.js +33 -8
  49. package/src/cli/help.js +79 -52
  50. package/src/cli/migration-guidance.js +9 -0
  51. package/src/cli/options.js +17 -0
  52. package/src/extractor/check.js +155 -0
  53. package/src/extractor/packages.js +295 -0
  54. package/src/extractor/registry.js +196 -0
  55. package/src/extractor-policy.js +249 -0
  56. package/src/generator/check.js +24 -87
  57. package/src/generator/context/bundle.js +14 -7
  58. package/src/generator/context/diff.js +8 -1
  59. package/src/generator/context/digest.js +10 -1
  60. package/src/generator/context/shared/domain-sdlc.js +5 -1
  61. package/src/generator/context/shared/relationships.js +20 -5
  62. package/src/generator/context/shared/summaries.js +26 -0
  63. package/src/generator/context/shared.d.ts +1 -0
  64. package/src/generator/context/shared.js +1 -0
  65. package/src/generator/context/slice/core.js +9 -5
  66. package/src/generator/context/slice/sdlc.js +31 -2
  67. package/src/generator/context/task-mode.js +3 -3
  68. package/src/generator/registry/index.js +16 -75
  69. package/src/generator-policy.js +9 -57
  70. package/src/import/core/registry.d.ts +3 -0
  71. package/src/import/core/registry.js +82 -8
  72. package/src/import/core/runner/reports.js +4 -4
  73. package/src/import/core/runner/run.js +2 -0
  74. package/src/import/core/runner/tracks.js +66 -4
  75. package/src/import/provenance.js +18 -17
  76. package/src/init-project.js +215 -0
  77. package/src/new-project/constants.js +1 -1
  78. package/src/new-project/create.js +2 -2
  79. package/src/new-project/project-files.js +7 -7
  80. package/src/package-adapters/adapter.js +64 -0
  81. package/src/package-adapters/file-map.js +30 -0
  82. package/src/package-adapters/index.js +27 -0
  83. package/src/package-adapters/manifest.js +108 -0
  84. package/src/package-adapters/policy.js +81 -0
  85. package/src/package-adapters/spec.js +51 -0
  86. package/src/reconcile/journeys.js +8 -3
  87. package/src/record-blocks.js +125 -0
  88. package/src/resolver/index.js +3 -0
  89. package/src/resolver/journeys.js +74 -0
  90. package/src/resolver/normalize.js +25 -0
  91. package/src/sdlc/adopt.js +1 -1
  92. package/src/validator/common.js +34 -1
  93. package/src/validator/index.js +4 -0
  94. package/src/validator/kinds.d.ts +2 -0
  95. package/src/validator/kinds.js +34 -1
  96. package/src/validator/per-kind/journey.js +233 -0
  97. package/src/workflows/docs-generate.js +4 -1
  98. package/src/workflows/reconcile/bundle-core/index.js +4 -2
  99. package/src/workflows/reconcile/canonical-surface.js +4 -1
  100. package/src/cli/commands/new.js +0 -94
@@ -0,0 +1,295 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ isPathSpec,
9
+ loadInstalledPackageAdapter,
10
+ loadLocalPackageAdapter,
11
+ packageNameFromSpec
12
+ } from "../package-adapters/index.js";
13
+ import {
14
+ EXTRACTOR_MANIFESTS,
15
+ getExtractorManifest,
16
+ loadPackageExtractorManifest,
17
+ validateExtractorManifest
18
+ } from "./registry.js";
19
+ import {
20
+ effectiveExtractorPolicy,
21
+ extractorPackageAllowed,
22
+ extractorPolicyDiagnosticsForPackages,
23
+ loadExtractorPolicy
24
+ } from "../extractor-policy.js";
25
+ import { createImportContext } from "../import/core/context.js";
26
+ import { getBundledExtractorById, getBundledExtractorPack } from "../import/core/registry.js";
27
+
28
+ /**
29
+ * @typedef {import("./registry.js").ExtractorManifest} ExtractorManifest
30
+ */
31
+
32
+ /**
33
+ * @param {string} packageRoot
34
+ * @returns {string|null}
35
+ */
36
+ function packageNameForRoot(packageRoot) {
37
+ const packageJsonPath = path.join(packageRoot, "package.json");
38
+ if (!fs.existsSync(packageJsonPath)) {
39
+ return null;
40
+ }
41
+ try {
42
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
43
+ return typeof packageJson.name === "string" && packageJson.name.length > 0 ? packageJson.name : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {any} adapter
51
+ * @param {ExtractorManifest} manifest
52
+ * @returns {{ extractors: any[], errors: string[] }}
53
+ */
54
+ export function validateExtractorAdapter(adapter, manifest) {
55
+ const errors = [];
56
+ if (!adapter || typeof adapter !== "object" || Array.isArray(adapter)) {
57
+ return { extractors: [], errors: ["Extractor adapter must export an object."] };
58
+ }
59
+ const adapterManifest = adapter.manifest;
60
+ if (!adapterManifest || adapterManifest.id !== manifest.id || adapterManifest.version !== manifest.version) {
61
+ errors.push("Extractor adapter must export manifest matching topogram-extractor.json.");
62
+ }
63
+ if (!Array.isArray(adapter.extractors) || adapter.extractors.length === 0) {
64
+ errors.push("Extractor adapter must export a non-empty extractors array.");
65
+ }
66
+ const manifestExtractorIds = new Set(manifest.extractors || []);
67
+ const manifestTracks = new Set(manifest.tracks || []);
68
+ const extractors = Array.isArray(adapter.extractors) ? adapter.extractors : [];
69
+ for (const extractor of extractors) {
70
+ if (!extractor || typeof extractor !== "object") {
71
+ errors.push("Extractor entries must be objects.");
72
+ continue;
73
+ }
74
+ if (typeof extractor.id !== "string" || extractor.id.length === 0) {
75
+ errors.push("Extractor entries must have a non-empty id.");
76
+ } else if (!manifestExtractorIds.has(extractor.id)) {
77
+ errors.push(`Extractor '${extractor.id}' is not declared by manifest extractors.`);
78
+ }
79
+ if (typeof extractor.track !== "string" || !manifestTracks.has(extractor.track)) {
80
+ errors.push(`Extractor '${extractor.id || "unknown"}' has track '${extractor.track || "unknown"}' not declared by manifest tracks.`);
81
+ }
82
+ if (typeof extractor.detect !== "function") {
83
+ errors.push(`Extractor '${extractor.id || "unknown"}' must expose detect(context).`);
84
+ }
85
+ if (typeof extractor.extract !== "function") {
86
+ errors.push(`Extractor '${extractor.id || "unknown"}' must expose extract(context).`);
87
+ }
88
+ }
89
+ return { extractors, errors };
90
+ }
91
+
92
+ /**
93
+ * @param {string} sourceSpec
94
+ * @param {{ cwd?: string }} [options]
95
+ * @returns {{ source: "path"|"package", packageName: string|null, packageRoot: string|null, manifestPath: string|null, manifest: ExtractorManifest|null, errors: string[] }}
96
+ */
97
+ export function loadExtractorPackageManifestForSpec(sourceSpec, options = {}) {
98
+ const cwd = path.resolve(options.cwd || process.cwd());
99
+ if (isPathSpec(sourceSpec, cwd)) {
100
+ const packageRoot = path.resolve(cwd, sourceSpec);
101
+ const manifestPath = path.join(packageRoot, "topogram-extractor.json");
102
+ if (!fs.existsSync(packageRoot) || !fs.statSync(packageRoot).isDirectory()) {
103
+ return { source: "path", packageName: null, packageRoot, manifestPath, manifest: null, errors: [`Extractor path '${packageRoot}' must be a directory.`] };
104
+ }
105
+ if (!fs.existsSync(manifestPath)) {
106
+ return { source: "path", packageName: packageNameForRoot(packageRoot), packageRoot, manifestPath, manifest: null, errors: [`Extractor path '${packageRoot}' is missing topogram-extractor.json.`] };
107
+ }
108
+ try {
109
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
110
+ const validation = validateExtractorManifest(manifest);
111
+ return {
112
+ source: "path",
113
+ packageName: typeof manifest.package === "string" ? manifest.package : packageNameForRoot(packageRoot),
114
+ packageRoot,
115
+ manifestPath,
116
+ manifest: validation.ok ? manifest : null,
117
+ errors: validation.errors
118
+ };
119
+ } catch (error) {
120
+ return {
121
+ source: "path",
122
+ packageName: packageNameForRoot(packageRoot),
123
+ packageRoot,
124
+ manifestPath,
125
+ manifest: null,
126
+ errors: [`Extractor manifest could not be read: ${error instanceof Error ? error.message : String(error)}`]
127
+ };
128
+ }
129
+ }
130
+
131
+ const packageName = packageNameFromSpec(sourceSpec);
132
+ const loaded = loadPackageExtractorManifest(packageName, cwd);
133
+ return {
134
+ source: "package",
135
+ packageName,
136
+ packageRoot: loaded.packageRoot,
137
+ manifestPath: loaded.manifestPath,
138
+ manifest: loaded.manifest,
139
+ errors: loaded.errors
140
+ };
141
+ }
142
+
143
+ /**
144
+ * @param {string} sourceSpec
145
+ * @param {{ cwd?: string }} [options]
146
+ * @returns {{ adapter: any|null, source: "path"|"package", packageName: string|null, packageRoot: string|null, manifestPath: string|null, manifest: ExtractorManifest|null, errors: string[] }}
147
+ */
148
+ export function loadExtractorPackageAdapterForSpec(sourceSpec, options = {}) {
149
+ const loadedManifest = loadExtractorPackageManifestForSpec(sourceSpec, options);
150
+ if (!loadedManifest.manifest) {
151
+ return { ...loadedManifest, adapter: null };
152
+ }
153
+ const loadedAdapter = loadedManifest.source === "path"
154
+ ? loadLocalPackageAdapter({
155
+ packageRoot: loadedManifest.packageRoot || path.resolve(options.cwd || process.cwd(), sourceSpec),
156
+ exportName: loadedManifest.manifest.export,
157
+ packageLabel: "Extractor package"
158
+ })
159
+ : loadInstalledPackageAdapter({
160
+ packageName: loadedManifest.packageName || packageNameFromSpec(sourceSpec),
161
+ rootDir: path.resolve(options.cwd || process.cwd()),
162
+ exportName: loadedManifest.manifest.export,
163
+ packageLabel: "Extractor package"
164
+ });
165
+ return {
166
+ ...loadedManifest,
167
+ adapter: loadedAdapter.adapter,
168
+ errors: [...loadedManifest.errors, ...(loadedAdapter.error ? [loadedAdapter.error] : [])]
169
+ };
170
+ }
171
+
172
+ /**
173
+ * @param {string} packageName
174
+ * @param {string} version
175
+ * @returns {import("../extractor-policy.js").PackageExtractorBinding}
176
+ */
177
+ function packageBinding(packageName, version) {
178
+ return {
179
+ packageName,
180
+ version
181
+ };
182
+ }
183
+
184
+ /**
185
+ * @param {any} context
186
+ * @returns {any}
187
+ */
188
+ function packageExtractorContext(context) {
189
+ return {
190
+ paths: context.paths,
191
+ options: context.options || {},
192
+ priorResults: context.priorResults || {},
193
+ scanDocsSummary: context.scanDocsSummary || null,
194
+ helpers: {
195
+ path: context.helpers?.path,
196
+ readTextIfExists: context.helpers?.readTextIfExists,
197
+ readJsonIfExists: context.helpers?.readJsonIfExists
198
+ }
199
+ };
200
+ }
201
+
202
+ /**
203
+ * @param {any} context
204
+ * @returns {{ extractors: any[], diagnostics: any[], provenance: any[] }}
205
+ */
206
+ export function packageExtractorsForContext(context) {
207
+ if (context.packageExtractorState) {
208
+ return context.packageExtractorState;
209
+ }
210
+ const cwd = path.resolve(context.options?.cwd || process.cwd());
211
+ const policyInfo = loadExtractorPolicy(cwd, context.options?.extractorPolicyPath || null);
212
+ const policy = effectiveExtractorPolicy(policyInfo);
213
+ const specs = [
214
+ ...(Array.isArray(policy.enabledPackages) ? policy.enabledPackages : []),
215
+ ...(Array.isArray(context.options?.extractorSpecs) ? context.options.extractorSpecs : [])
216
+ ].filter(Boolean);
217
+ /** @type {any[]} */
218
+ const extractors = [];
219
+ /** @type {any[]} */
220
+ const diagnostics = [...policyInfo.diagnostics];
221
+ /** @type {any[]} */
222
+ const provenance = [];
223
+ const seen = new Set();
224
+
225
+ for (const spec of specs) {
226
+ if (seen.has(spec)) continue;
227
+ seen.add(spec);
228
+ const bundledPack = getBundledExtractorPack(spec);
229
+ if (bundledPack) {
230
+ extractors.push(...bundledPack.extractors);
231
+ provenance.push({ source: "bundled", id: bundledPack.manifest.id, version: bundledPack.manifest.version, extractors: bundledPack.manifest.extractors });
232
+ continue;
233
+ }
234
+ const bundledExtractor = getBundledExtractorById(spec);
235
+ if (bundledExtractor) {
236
+ extractors.push(bundledExtractor);
237
+ provenance.push({ source: "bundled", id: bundledExtractor.id, version: "1", extractors: [bundledExtractor.id] });
238
+ continue;
239
+ }
240
+ const packageManifest = loadExtractorPackageManifestForSpec(spec, { cwd });
241
+ if (!packageManifest.manifest) {
242
+ diagnostics.push({ severity: "error", code: "extractor_package_manifest_failed", message: packageManifest.errors.join(" "), packageName: packageManifest.packageName, spec });
243
+ continue;
244
+ }
245
+ const packageName = packageManifest.packageName || packageManifest.manifest.package || spec;
246
+ const policyDiagnostics = extractorPolicyDiagnosticsForPackages(policyInfo, [packageBinding(packageName, packageManifest.manifest.version)], "extractor-policy");
247
+ const errors = policyDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
248
+ diagnostics.push(...policyDiagnostics);
249
+ if (errors.length > 0 || !extractorPackageAllowed(policy, packageName)) {
250
+ continue;
251
+ }
252
+ const adapter = loadExtractorPackageAdapterForSpec(spec, { cwd });
253
+ if (!adapter.adapter || adapter.errors.length > 0) {
254
+ diagnostics.push(...adapter.errors.map((message) => ({ severity: "error", code: "extractor_package_load_failed", message, packageName, spec })));
255
+ continue;
256
+ }
257
+ const adapterValidation = validateExtractorAdapter(adapter.adapter, packageManifest.manifest);
258
+ if (adapterValidation.errors.length > 0) {
259
+ diagnostics.push(...adapterValidation.errors.map((message) => ({ severity: "error", code: "extractor_adapter_invalid", message, packageName, spec })));
260
+ continue;
261
+ }
262
+ for (const extractor of adapterValidation.extractors) {
263
+ extractors.push({
264
+ ...extractor,
265
+ source: "package",
266
+ manifestId: packageManifest.manifest.id,
267
+ packageName,
268
+ packageContext: packageExtractorContext
269
+ });
270
+ }
271
+ provenance.push({
272
+ source: "package",
273
+ id: packageManifest.manifest.id,
274
+ version: packageManifest.manifest.version,
275
+ packageName,
276
+ manifestPath: packageManifest.manifestPath,
277
+ packageRoot: packageManifest.packageRoot,
278
+ extractors: packageManifest.manifest.extractors
279
+ });
280
+ }
281
+
282
+ context.packageExtractorState = { extractors, diagnostics, provenance };
283
+ return context.packageExtractorState;
284
+ }
285
+
286
+ /**
287
+ * @returns {any}
288
+ */
289
+ export function createExtractorSmokeContext() {
290
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "topogram-extractor-check."));
291
+ fs.writeFileSync(path.join(root, "package.json"), "{\"scripts\":{\"check\":\"topogram check\"}}\n", "utf8");
292
+ return packageExtractorContext(createImportContext(root, { from: "cli" }));
293
+ }
294
+
295
+ export { EXTRACTOR_MANIFESTS, getExtractorManifest };
@@ -0,0 +1,196 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ loadPackageManifest,
5
+ packageInstallCommand,
6
+ packageInstallHint,
7
+ resolvePackageManifestPath
8
+ } from "../package-adapters/index.js";
9
+ import { BUILTIN_EXTRACTOR_PACKS } from "../import/core/registry.js";
10
+
11
+ export const EXTRACTOR_TRACKS = ["db", "api", "ui", "cli", "workflows", "verification"];
12
+
13
+ /**
14
+ * @typedef {Object} ExtractorManifest
15
+ * @property {string} id
16
+ * @property {string} version
17
+ * @property {string[]} tracks
18
+ * @property {"bundled"|"package"} source
19
+ * @property {string[]} extractors
20
+ * @property {Record<string, string>} stack
21
+ * @property {Record<string, boolean>} capabilities
22
+ * @property {string[]} candidateKinds
23
+ * @property {string[]} evidenceTypes
24
+ * @property {string} [package]
25
+ * @property {string} [export]
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} ExtractorBinding
30
+ * @property {string} id
31
+ * @property {string} track
32
+ * @property {string} packageName
33
+ * @property {string} version
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} ResolvedExtractorManifest
38
+ * @property {ExtractorManifest|null} manifest
39
+ * @property {string[]} errors
40
+ * @property {"bundled"|"package"|null} source
41
+ * @property {string|null} manifestPath
42
+ * @property {string|null} packageRoot
43
+ */
44
+
45
+ /** @type {ExtractorManifest[]} */
46
+ export const EXTRACTOR_MANIFESTS = BUILTIN_EXTRACTOR_PACKS.map((pack) => pack.manifest);
47
+
48
+ const EXTRACTOR_MANIFEST_BY_ID = new Map(EXTRACTOR_MANIFESTS.map((manifest) => [manifest.id, manifest]));
49
+
50
+ /**
51
+ * @param {any} value
52
+ * @param {boolean} [nonEmpty]
53
+ * @returns {boolean}
54
+ */
55
+ function isStringArray(value, nonEmpty = false) {
56
+ return Array.isArray(value) &&
57
+ (!nonEmpty || value.length > 0) &&
58
+ value.every((entry) => typeof entry === "string" && entry.length > 0);
59
+ }
60
+
61
+ /**
62
+ * @param {any} value
63
+ * @returns {value is Record<string, string>}
64
+ */
65
+ function isStringRecord(value) {
66
+ return Boolean(value) &&
67
+ typeof value === "object" &&
68
+ !Array.isArray(value) &&
69
+ Object.values(value).every((entry) => typeof entry === "string");
70
+ }
71
+
72
+ /**
73
+ * @param {any} value
74
+ * @returns {value is Record<string, boolean>}
75
+ */
76
+ function isBooleanRecord(value) {
77
+ return Boolean(value) &&
78
+ typeof value === "object" &&
79
+ !Array.isArray(value) &&
80
+ Object.values(value).every((entry) => typeof entry === "boolean");
81
+ }
82
+
83
+ /**
84
+ * @param {any} manifest
85
+ * @returns {{ ok: boolean, errors: string[] }}
86
+ */
87
+ export function validateExtractorManifest(manifest) {
88
+ const errors = [];
89
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
90
+ return { ok: false, errors: ["Extractor manifest must be an object."] };
91
+ }
92
+ if (typeof manifest.id !== "string" || manifest.id.length === 0) {
93
+ errors.push("Extractor manifest id must be a non-empty string.");
94
+ }
95
+ if (typeof manifest.version !== "string" || manifest.version.length === 0) {
96
+ errors.push("Extractor manifest version must be a non-empty string.");
97
+ }
98
+ if (!isStringArray(manifest.tracks, true)) {
99
+ errors.push("Extractor manifest tracks must be a non-empty array of strings.");
100
+ } else {
101
+ for (const track of manifest.tracks) {
102
+ if (!EXTRACTOR_TRACKS.includes(track)) {
103
+ errors.push(`Extractor manifest track '${track}' is not supported.`);
104
+ }
105
+ }
106
+ }
107
+ if (manifest.source !== "bundled" && manifest.source !== "package") {
108
+ errors.push("Extractor manifest source must be 'bundled' or 'package'.");
109
+ }
110
+ if (!isStringArray(manifest.extractors, true)) {
111
+ errors.push("Extractor manifest extractors must be a non-empty array of strings.");
112
+ }
113
+ if (!isStringRecord(manifest.stack)) {
114
+ errors.push("Extractor manifest stack must be an object of string values.");
115
+ }
116
+ if (!isBooleanRecord(manifest.capabilities)) {
117
+ errors.push("Extractor manifest capabilities must be an object of boolean values.");
118
+ }
119
+ if (!isStringArray(manifest.candidateKinds)) {
120
+ errors.push("Extractor manifest candidateKinds must be an array of strings.");
121
+ }
122
+ if (!isStringArray(manifest.evidenceTypes)) {
123
+ errors.push("Extractor manifest evidenceTypes must be an array of strings.");
124
+ }
125
+ if (manifest.package != null && (typeof manifest.package !== "string" || manifest.package.length === 0)) {
126
+ errors.push("Extractor manifest package must be a non-empty string when present.");
127
+ }
128
+ if (manifest.export != null && (typeof manifest.export !== "string" || manifest.export.length === 0)) {
129
+ errors.push("Extractor manifest export must be a non-empty string when present.");
130
+ }
131
+ return { ok: errors.length === 0, errors };
132
+ }
133
+
134
+ /**
135
+ * @param {string} extractorId
136
+ * @returns {ExtractorManifest|null}
137
+ */
138
+ export function getExtractorManifest(extractorId) {
139
+ return EXTRACTOR_MANIFEST_BY_ID.get(extractorId) || null;
140
+ }
141
+
142
+ /**
143
+ * @param {string|null|undefined} packageName
144
+ * @returns {string|null}
145
+ */
146
+ export function packageExtractorInstallCommand(packageName) {
147
+ return packageInstallCommand(packageName);
148
+ }
149
+
150
+ /**
151
+ * @param {string|null|undefined} packageName
152
+ * @returns {string|null}
153
+ */
154
+ export function packageExtractorInstallHint(packageName) {
155
+ return packageInstallHint(packageName);
156
+ }
157
+
158
+ /**
159
+ * @param {string} packageName
160
+ * @param {string|null|undefined} rootDir
161
+ * @returns {{ manifestPath: string|null, packageRoot: string|null, error: string|null }}
162
+ */
163
+ export function resolvePackageExtractorManifestPath(packageName, rootDir = process.cwd()) {
164
+ return resolvePackageManifestPath(packageName, "topogram-extractor.json", rootDir, "Extractor package");
165
+ }
166
+
167
+ /**
168
+ * @param {string} packageName
169
+ * @param {string|null|undefined} rootDir
170
+ * @returns {{ manifest: ExtractorManifest|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
171
+ */
172
+ export function loadPackageExtractorManifest(packageName, rootDir = process.cwd()) {
173
+ return loadPackageManifest({
174
+ packageName,
175
+ rootDir,
176
+ manifestFile: "topogram-extractor.json",
177
+ packageLabel: "Extractor package",
178
+ validateManifest: validateExtractorManifest
179
+ });
180
+ }
181
+
182
+ /**
183
+ * @param {Record<string, any>|null|undefined} manifest
184
+ * @returns {ExtractorBinding[]}
185
+ */
186
+ export function extractorBindingsForManifest(manifest) {
187
+ if (!manifest || manifest.source !== "package" || typeof manifest.package !== "string") {
188
+ return [];
189
+ }
190
+ return (manifest.extractors || []).map((/** @type {any} */ extractorId) => ({
191
+ id: String(extractorId),
192
+ track: Array.isArray(manifest.tracks) && manifest.tracks.length === 1 ? String(manifest.tracks[0]) : "multiple",
193
+ packageName: String(manifest.package),
194
+ version: String(manifest.version || "unknown")
195
+ }));
196
+ }