@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
@@ -49,9 +49,16 @@ import {
49
49
  export function journeySlice(graph, journeyId) {
50
50
  const journey = getJourneyDoc(graph, journeyId);
51
51
  const capabilities = [...(journey.relatedCapabilities || [])].sort();
52
+ const entities = [...(journey.relatedEntities || [])].sort();
53
+ const rules = [...(journey.relatedRules || [])].sort();
52
54
  const workflows = [...(journey.relatedWorkflows || [])].sort();
53
55
  const projections = [...(journey.relatedProjections || [])].sort();
54
- const verifications = verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, journeyId]);
56
+ const widgets = [...(journey.relatedWidgets || [])].sort();
57
+ const declaredVerifications = [...(journey.relatedVerifications || [])].sort();
58
+ const verifications = [...new Set([
59
+ ...declaredVerifications,
60
+ ...verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, ...widgets, journeyId])
61
+ ])].sort();
55
62
 
56
63
  return {
57
64
  type: "context_slice",
@@ -63,14 +70,36 @@ export function journeySlice(graph, journeyId) {
63
70
  summary: summarizeById(graph, journeyId),
64
71
  depends_on: {
65
72
  capabilities,
73
+ entities,
74
+ rules,
66
75
  workflows,
67
76
  projections,
77
+ widgets,
68
78
  verifications
69
79
  },
80
+ steps: (journey.steps || []).map(/** @param {any} step */ (step) => ({
81
+ id: step.id,
82
+ intent: step.intent,
83
+ commands: [...(step.commands || [])],
84
+ expects: [...(step.expects || [])],
85
+ after: [...(step.after || [])],
86
+ notes: step.notes || null
87
+ })),
88
+ alternates: (journey.alternates || []).map(/** @param {any} alternate */ (alternate) => ({
89
+ id: alternate.id,
90
+ from: alternate.from,
91
+ condition: alternate.condition,
92
+ commands: [...(alternate.commands || [])],
93
+ expects: [...(alternate.expects || [])],
94
+ notes: alternate.notes || null
95
+ })),
70
96
  related: {
71
97
  capabilities: summarizeStatementsByIds(graph, capabilities),
98
+ entities: summarizeStatementsByIds(graph, entities),
99
+ rules: summarizeStatementsByIds(graph, rules),
72
100
  workflows: summarizeDocsByIds(graph, workflows),
73
- projections: summarizeStatementsByIds(graph, projections)
101
+ projections: summarizeStatementsByIds(graph, projections),
102
+ widgets: summarizeStatementsByIds(graph, widgets)
74
103
  },
75
104
  verification: summarizeStatementsByIds(graph, verifications),
76
105
  verification_targets: recommendedVerificationTargets(graph, [...capabilities, ...workflows, ...projections, journeyId], {
@@ -325,7 +325,7 @@ function importAdoptMode(graph, options = {}) {
325
325
  return {
326
326
  type: "context_task_mode",
327
327
  version: 1,
328
- mode: "import-adopt",
328
+ mode: "extract-adopt",
329
329
  summary: {
330
330
  focus: "Proposal review and adoption planning",
331
331
  preferred_start: "adoption-plan.agent.json",
@@ -450,7 +450,7 @@ function verificationMode(graph, options = {}) {
450
450
  export function generateContextTaskMode(graph, options = {}) {
451
451
  const mode = String(options.modeId || "").trim();
452
452
  if (!mode) {
453
- throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|import-adopt|diff-review|verification>");
453
+ throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|extract-adopt|diff-review|verification>");
454
454
  }
455
455
 
456
456
  if (mode === "modeling") {
@@ -459,7 +459,7 @@ export function generateContextTaskMode(graph, options = {}) {
459
459
  if (mode === "maintained-app-edit") {
460
460
  return maintainedAppEditMode(graph, options);
461
461
  }
462
- if (mode === "import-adopt") {
462
+ if (mode === "extract-adopt") {
463
463
  return importAdoptMode(graph, options);
464
464
  }
465
465
  if (mode === "diff-review") {
@@ -1,8 +1,11 @@
1
1
  // @ts-check
2
2
 
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { createRequire } from "node:module";
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 ? `npm install -D ${packageName}` : null;
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
- const command = packageGeneratorInstallCommand(packageName);
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
- const requireFromRoot = createRequire(packageResolutionBase(rootDir));
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
- const resolved = resolvePackageGeneratorManifestPath(packageName, rootDir);
286
- if (!resolved.manifestPath) {
287
- return {
288
- manifest: null,
289
- errors: [resolved.error || `Generator package '${packageName}' could not be resolved`],
290
- manifestPath: null,
291
- packageRoot: resolved.packageRoot
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
  /**
@@ -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 packageName.startsWith("@") ? packageName.split("/")[0] || null : null;
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
- if (policy.allowedPackages.includes(packageName)) {
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
- export const extractorRegistry = {
61
- db: [prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
62
- api: [openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
63
- ui: [nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
64
- cli: [genericCliExtractor],
65
- workflows: [genericWorkflowExtractor],
66
- verification: [genericVerificationExtractor]
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
+ }
@@ -17,12 +17,12 @@ export function reportMarkdown(track, candidates) {
17
17
  ` - manual next: ${(seam.manual_next_steps || []).slice(0, 2).join(" ")}`
18
18
  ]);
19
19
  return ensureTrailingNewline(
20
- `# DB Import Report\n\n- Entities: ${candidates.entities.length}\n- Enums: ${candidates.enums.length}\n- Relations: ${candidates.relations.length}\n- Indexes: ${candidates.indexes.length}\n- Maintained DB migration seams: ${(candidates.maintained_seams || []).length}\n\n## Maintained DB Migration Seam Candidates\n\n${seamLines.length ? seamLines.join("\n") : "- none"}\n`
20
+ `# DB Extract Report\n\n- Entities: ${candidates.entities.length}\n- Enums: ${candidates.enums.length}\n- Relations: ${candidates.relations.length}\n- Indexes: ${candidates.indexes.length}\n- Maintained DB migration seams: ${(candidates.maintained_seams || []).length}\n\n## Maintained DB Migration Seam Candidates\n\n${seamLines.length ? seamLines.join("\n") : "- none"}\n`
21
21
  );
22
22
  }
23
23
  if (track === "api") {
24
24
  return ensureTrailingNewline(
25
- `# API Import Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
25
+ `# API Extract Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
26
26
  );
27
27
  }
28
28
  if (track === "ui") {
@@ -36,7 +36,7 @@ export function reportMarkdown(track, candidates) {
36
36
  `- \`${flow.id_hint}\` type \`${flow.flow_type}\` confidence ${flow.confidence || "unknown"} routes ${(flow.route_paths || []).map((/** @type {string} */ route) => `\`${route}\``).join(", ") || "_none_"} missing decisions ${(flow.missing_decisions || []).length}`
37
37
  );
38
38
  return ensureTrailingNewline(
39
- `# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Flow candidates: ${flows.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Flow Candidates\n\n${flowLines.length ? flowLines.join("\n") : "- none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review flow candidates in \`topo/candidates/app/ui/candidates.json\` before adding shared UI contract behavior.\n- Review candidates under \`topo/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
39
+ `# UI Extract Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Flow candidates: ${flows.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Flow Candidates\n\n${flowLines.length ? flowLines.join("\n") : "- none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review flow candidates in \`topo/candidates/app/ui/candidates.json\` before adding shared UI contract behavior.\n- Review candidates under \`topo/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram extract plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
40
40
  );
41
41
  }
42
42
  if (track === "cli") {
@@ -44,7 +44,7 @@ export function reportMarkdown(track, candidates) {
44
44
  `- \`${command.command_id || command.id_hint}\` usage \`${command.usage || "unknown"}\` effects ${(command.effects || []).map((/** @type {any} */ effect) => `\`${effect}\``).join(", ") || "_none_"}`
45
45
  );
46
46
  return ensureTrailingNewline(
47
- `# CLI Import Report\n\n- Commands: ${candidates.commands.length}\n- Capabilities: ${candidates.capabilities.length}\n- CLI surfaces: ${candidates.surfaces.length}\n\n## Command Candidates\n\n${commandLines.length ? commandLines.join("\n") : "- none"}\n`
47
+ `# CLI Extract Report\n\n- Commands: ${candidates.commands.length}\n- Capabilities: ${candidates.capabilities.length}\n- CLI surfaces: ${candidates.surfaces.length}\n\n## Command Candidates\n\n${commandLines.length ? commandLines.join("\n") : "- none"}\n`
48
48
  );
49
49
  }
50
50
  if (track === "verification") {
@@ -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) => ({ extractor, detection: extractor.detect(context) || { score: 0, reasons: [] } }))
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 selectDetectionsForTrack(track, detections) {
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, getExtractorsForTrack(track)))) {
126
- const result = extractor.extract(context) || { findings: [], candidates: {} };
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,