@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.
- package/CHANGELOG.md +20 -0
- package/package.json +2 -2
- package/src/agent-brief.js +29 -23
- package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
- package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
- package/src/agent-ops/query-builders/change-risk.js +1 -1
- package/src/agent-ops/query-builders/common.js +2 -2
- package/src/agent-ops/query-builders/multi-agent.js +1 -1
- package/src/agent-ops/query-builders/workflow-context-shared.js +4 -4
- package/src/catalog/provenance.js +1 -1
- package/src/cli/catalog-alias.d.ts +2 -0
- package/src/cli/catalog-alias.js +2 -2
- package/src/cli/command-parser.js +2 -0
- package/src/cli/command-parsers/core.js +9 -5
- package/src/cli/command-parsers/extractor.js +40 -0
- package/src/cli/command-parsers/import.js +11 -17
- package/src/cli/command-parsers/project.js +0 -3
- package/src/cli/commands/catalog/copy.js +3 -3
- package/src/cli/commands/catalog/help.js +1 -2
- package/src/cli/commands/catalog/list.js +7 -4
- package/src/cli/commands/catalog/show.js +4 -4
- package/src/cli/commands/copy.js +356 -0
- package/src/cli/commands/doctor.js +1 -1
- package/src/cli/commands/extractor.js +451 -0
- package/src/cli/commands/import/adopt.js +9 -9
- package/src/cli/commands/import/check.js +15 -15
- package/src/cli/commands/import/diff.js +6 -6
- package/src/cli/commands/import/help.js +45 -34
- package/src/cli/commands/import/paths.js +3 -3
- package/src/cli/commands/import/plan.js +8 -8
- package/src/cli/commands/import/refresh.js +25 -24
- package/src/cli/commands/import/status-history.js +4 -4
- package/src/cli/commands/import/workspace.js +24 -18
- package/src/cli/commands/import-runner.js +10 -7
- package/src/cli/commands/import.js +4 -1
- package/src/cli/commands/init.js +67 -0
- package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
- package/src/cli/commands/query/runner/change.js +2 -2
- package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
- package/src/cli/commands/query/runner/index.js +1 -1
- package/src/cli/commands/query/runner/workflow.js +7 -7
- package/src/cli/commands/query/workspace.js +4 -4
- package/src/cli/commands/release-status.js +2 -2
- package/src/cli/commands/source.js +2 -2
- package/src/cli/commands/template/check.js +2 -2
- package/src/cli/commands/template/list-show.js +4 -4
- package/src/cli/dispatcher.js +32 -3
- package/src/cli/help-dispatch.js +33 -8
- package/src/cli/help.js +79 -52
- package/src/cli/migration-guidance.js +9 -0
- 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/context/bundle.js +14 -7
- package/src/generator/context/diff.js +8 -1
- package/src/generator/context/digest.js +10 -1
- package/src/generator/context/shared/domain-sdlc.js +5 -1
- package/src/generator/context/shared/relationships.js +20 -5
- package/src/generator/context/shared/summaries.js +26 -0
- package/src/generator/context/shared.d.ts +1 -0
- package/src/generator/context/shared.js +1 -0
- package/src/generator/context/slice/core.js +9 -5
- package/src/generator/context/slice/sdlc.js +31 -2
- package/src/generator/context/task-mode.js +3 -3
- 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/reports.js +4 -4
- package/src/import/core/runner/run.js +2 -0
- package/src/import/core/runner/tracks.js +66 -4
- package/src/import/provenance.js +18 -17
- package/src/init-project.js +215 -0
- package/src/new-project/constants.js +1 -1
- package/src/new-project/create.js +2 -2
- package/src/new-project/project-files.js +7 -7
- 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
- package/src/reconcile/journeys.js +8 -3
- package/src/record-blocks.js +125 -0
- package/src/resolver/index.js +3 -0
- package/src/resolver/journeys.js +74 -0
- package/src/resolver/normalize.js +25 -0
- package/src/sdlc/adopt.js +1 -1
- package/src/validator/common.js +34 -1
- package/src/validator/index.js +4 -0
- package/src/validator/kinds.d.ts +2 -0
- package/src/validator/kinds.js +34 -1
- package/src/validator/per-kind/journey.js +233 -0
- package/src/workflows/docs-generate.js +4 -1
- package/src/workflows/reconcile/bundle-core/index.js +4 -2
- package/src/workflows/reconcile/canonical-surface.js +4 -1
- package/src/cli/commands/new.js +0 -94
|
@@ -3,46 +3,57 @@
|
|
|
3
3
|
import { TOPOGRAM_IMPORT_FILE } from "../../../import/provenance.js";
|
|
4
4
|
import { TOPOGRAM_IMPORT_ADOPTIONS_FILE } from "./paths.js";
|
|
5
5
|
|
|
6
|
-
export function
|
|
7
|
-
console.log("Usage: topogram
|
|
8
|
-
console.log(" or: topogram
|
|
9
|
-
console.log(" or: topogram
|
|
10
|
-
console.log(" or: topogram
|
|
11
|
-
console.log(" or: topogram
|
|
12
|
-
console.log(" or: topogram
|
|
13
|
-
console.log(" or: topogram
|
|
14
|
-
console.log(" or: topogram import status [path] [--json]");
|
|
15
|
-
console.log(" or: topogram import history [path] [--verify] [--json]");
|
|
6
|
+
export function printExtractHelp() {
|
|
7
|
+
console.log("Usage: topogram extract <app-path> --out <target> [--from <track[,track]>] [--extractor <id-or-package-or-path>] [--json]");
|
|
8
|
+
console.log(" or: topogram extract refresh [path] [--from <app-path>] [--dry-run] [--json]");
|
|
9
|
+
console.log(" or: topogram extract diff [path] [--json]");
|
|
10
|
+
console.log(" or: topogram extract check [path] [--json]");
|
|
11
|
+
console.log(" or: topogram extract plan [path] [--json]");
|
|
12
|
+
console.log(" or: topogram extract status [path] [--json]");
|
|
13
|
+
console.log(" or: topogram extract history [path] [--verify] [--json]");
|
|
16
14
|
console.log("");
|
|
17
|
-
console.log("
|
|
15
|
+
console.log("Extracts reviewable Topogram candidates from a brownfield app without modifying the app.");
|
|
18
16
|
console.log("");
|
|
19
17
|
console.log("Behavior:");
|
|
20
|
-
console.log(" - writes raw
|
|
18
|
+
console.log(" - writes raw extraction candidates under topo/candidates/app");
|
|
21
19
|
console.log(" - writes reconcile proposal bundles under topo/candidates/reconcile");
|
|
22
20
|
console.log(" - writes topogram.project.json with maintained ownership and no generated stack binding");
|
|
23
|
-
console.log(` - writes ${TOPOGRAM_IMPORT_FILE} with source file hashes from
|
|
24
|
-
console.log(" -
|
|
21
|
+
console.log(` - writes ${TOPOGRAM_IMPORT_FILE} with source file hashes from extraction time`);
|
|
22
|
+
console.log(" - extracted Topogram artifacts are project-owned after creation");
|
|
25
23
|
console.log(" - refresh rewrites only candidate/reconcile artifacts and source provenance");
|
|
26
|
-
console.log(" - adoption previews never write canonical Topogram files unless --write is passed");
|
|
27
|
-
console.log(" - adoption writes refuse dirty brownfield source provenance unless --force is passed");
|
|
28
|
-
console.log(` - adoption writes append audit receipts to ${TOPOGRAM_IMPORT_ADOPTIONS_FILE}`);
|
|
29
|
-
console.log(" - forced adoption writes require --reason <text>");
|
|
30
24
|
console.log("");
|
|
31
25
|
console.log("Examples:");
|
|
32
|
-
console.log(" topogram
|
|
33
|
-
console.log(" topogram
|
|
34
|
-
console.log(" topogram
|
|
35
|
-
console.log(" topogram
|
|
36
|
-
console.log(" topogram
|
|
37
|
-
console.log(" topogram
|
|
38
|
-
console.log(" topogram
|
|
39
|
-
console.log(" topogram
|
|
40
|
-
console.log(" topogram
|
|
41
|
-
console.log(" topogram
|
|
42
|
-
console.log(" topogram
|
|
43
|
-
console.log(" topogram
|
|
44
|
-
console.log(" topogram
|
|
45
|
-
console.log(" topogram
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
console.log(" topogram extract ./existing-app --out ./extracted-topogram");
|
|
27
|
+
console.log(" topogram extract ./existing-app --out ./extracted-topogram --from db,api,ui");
|
|
28
|
+
console.log(" topogram extract ./existing-cli --out ./extracted-topogram --from cli");
|
|
29
|
+
console.log(" topogram extract ./existing-cli --out ./extracted-topogram --from cli --extractor @topogram/extractor-node-cli");
|
|
30
|
+
console.log(" topogram extract ./existing-app --out ./extracted-topogram --extractor-policy ./topogram.extractor-policy.json");
|
|
31
|
+
console.log(" topogram extract diff ./extracted-topogram");
|
|
32
|
+
console.log(" topogram extract refresh ./extracted-topogram --from ./existing-app --dry-run");
|
|
33
|
+
console.log(" topogram extract refresh ./extracted-topogram --from ./existing-app");
|
|
34
|
+
console.log(" topogram extract check ./extracted-topogram");
|
|
35
|
+
console.log(" topogram extract plan ./extracted-topogram");
|
|
36
|
+
console.log(" topogram extract status ./extracted-topogram");
|
|
37
|
+
console.log(" topogram extract history ./extracted-topogram");
|
|
38
|
+
console.log(" topogram extract history ./extracted-topogram --verify");
|
|
39
|
+
console.log(" topogram extract check --json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function printAdoptHelp() {
|
|
43
|
+
console.log("Usage: topogram adopt --list [path] [--json]");
|
|
44
|
+
console.log(" or: topogram adopt <selector> [path] [--dry-run|--write] [--force --reason <text>] [--json]");
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log("Promotes reviewed extraction candidates into canonical topo/ files.");
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log("Behavior:");
|
|
49
|
+
console.log(" - previews never write canonical Topogram files unless --write is passed");
|
|
50
|
+
console.log(" - writes refuse dirty brownfield source provenance unless --force is passed");
|
|
51
|
+
console.log(` - writes append audit receipts to ${TOPOGRAM_IMPORT_ADOPTIONS_FILE}`);
|
|
52
|
+
console.log(" - forced writes require --reason <text>");
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log("Examples:");
|
|
55
|
+
console.log(" topogram adopt --list ./extracted-topogram");
|
|
56
|
+
console.log(" topogram adopt bundle:task ./extracted-topogram --dry-run");
|
|
57
|
+
console.log(" topogram adopt bundle:task ./extracted-topogram --write");
|
|
58
|
+
console.log(" topogram adopt bundle:task ./extracted-topogram --write --force --reason \"Reviewed source drift\"");
|
|
48
59
|
}
|
|
@@ -9,7 +9,7 @@ import { TOPOGRAM_IMPORT_FILE } from "../../../import/provenance.js";
|
|
|
9
9
|
import { shellCommandArg } from "../catalog.js";
|
|
10
10
|
import { resolveTopoRoot, resolveWorkspaceContext } from "../../../workspace-paths.js";
|
|
11
11
|
|
|
12
|
-
export const TOPOGRAM_IMPORT_ADOPTIONS_FILE = ".topogram-
|
|
12
|
+
export const TOPOGRAM_IMPORT_ADOPTIONS_FILE = ".topogram-adoptions.jsonl";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @typedef {Record<string, any>} AnyRecord
|
|
@@ -219,7 +219,7 @@ export function readImportAdoptionReceipts(projectRoot) {
|
|
|
219
219
|
try {
|
|
220
220
|
return JSON.parse(line);
|
|
221
221
|
} catch (error) {
|
|
222
|
-
throw new Error(`Invalid
|
|
222
|
+
throw new Error(`Invalid adoption receipt JSON at ${historyPath}:${index + 1}.`);
|
|
223
223
|
}
|
|
224
224
|
});
|
|
225
225
|
}
|
|
@@ -265,5 +265,5 @@ export function importProjectCommandPath(projectRoot) {
|
|
|
265
265
|
* @returns {string}
|
|
266
266
|
*/
|
|
267
267
|
export function importAdoptCommand(projectRoot, selector, write = false) {
|
|
268
|
-
return `topogram
|
|
268
|
+
return `topogram adopt ${selector} ${importProjectCommandPath(projectRoot)} ${write ? "--write" : "--dry-run"}`;
|
|
269
269
|
}
|
|
@@ -76,7 +76,7 @@ export function readImportAdoptionArtifacts(inputPath) {
|
|
|
76
76
|
reconcileReport: path.join(reconcileRoot, "report.json")
|
|
77
77
|
};
|
|
78
78
|
if (!fs.existsSync(paths.adoptionPlanAgent)) {
|
|
79
|
-
throw new Error(`No
|
|
79
|
+
throw new Error(`No extraction adoption plan found under '${reconcileRoot}'. Run 'topogram extract <app-path> --out <target>' first.`);
|
|
80
80
|
}
|
|
81
81
|
return {
|
|
82
82
|
projectRoot,
|
|
@@ -182,10 +182,10 @@ export function summarizeImportAdoption(adoptionPlan, adoptionStatus, projectRoo
|
|
|
182
182
|
risks: [
|
|
183
183
|
...(blockedCount > 0 ? [`${blockedCount} adoption item(s) are blocked.`] : []),
|
|
184
184
|
...(((adoptionPlan.requires_human_review || []).length || surfaces.some((/** @type {AnyRecord} */ item) => item.human_review_required))
|
|
185
|
-
? ["
|
|
185
|
+
? ["Extracted proposal items require human review before adoption."]
|
|
186
186
|
: [])
|
|
187
187
|
],
|
|
188
|
-
nextCommand: nextBundle ? nextBundle.nextCommand : `topogram
|
|
188
|
+
nextCommand: nextBundle ? nextBundle.nextCommand : `topogram extract status ${importProjectCommandPath(projectRoot)}`
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -209,8 +209,8 @@ export function buildBrownfieldImportPlanPayload(inputPath) {
|
|
|
209
209
|
},
|
|
210
210
|
...adoption,
|
|
211
211
|
commands: {
|
|
212
|
-
check: `topogram
|
|
213
|
-
status: `topogram
|
|
212
|
+
check: `topogram extract check ${importProjectCommandPath(artifacts.projectRoot)}`,
|
|
213
|
+
status: `topogram extract status ${importProjectCommandPath(artifacts.projectRoot)}`,
|
|
214
214
|
next: adoption.nextCommand
|
|
215
215
|
}
|
|
216
216
|
};
|
|
@@ -221,7 +221,7 @@ export function buildBrownfieldImportPlanPayload(inputPath) {
|
|
|
221
221
|
* @returns {void}
|
|
222
222
|
*/
|
|
223
223
|
export function printBrownfieldImportPlan(payload) {
|
|
224
|
-
console.log(`
|
|
224
|
+
console.log(`Extraction adoption plan for ${payload.projectRoot}`);
|
|
225
225
|
console.log(`Proposal items: ${payload.summary.proposalItemCount}`);
|
|
226
226
|
console.log(`Bundles: ${payload.summary.bundleCount}`);
|
|
227
227
|
for (const bundle of payload.bundles) {
|
|
@@ -279,9 +279,9 @@ export function buildBrownfieldImportAdoptListPayload(inputPath) {
|
|
|
279
279
|
* @returns {void}
|
|
280
280
|
*/
|
|
281
281
|
export function printBrownfieldImportAdoptList(payload) {
|
|
282
|
-
console.log(`
|
|
282
|
+
console.log(`Adoption selectors for ${payload.projectRoot}`);
|
|
283
283
|
if (payload.selectors.length === 0) {
|
|
284
|
-
console.log("No adoption selectors are available. Run `topogram
|
|
284
|
+
console.log("No adoption selectors are available. Run `topogram extract plan` to inspect reconcile artifacts.");
|
|
285
285
|
return;
|
|
286
286
|
}
|
|
287
287
|
for (const selector of payload.selectors) {
|
|
@@ -34,12 +34,12 @@ import { verifyImportAdoptionReceipts } from "./status-history.js";
|
|
|
34
34
|
export function readTopogramImportRecord(projectRoot) {
|
|
35
35
|
const importPath = path.join(normalizeProjectRoot(projectRoot), TOPOGRAM_IMPORT_FILE);
|
|
36
36
|
if (!fs.existsSync(importPath)) {
|
|
37
|
-
throw new Error(`No brownfield
|
|
37
|
+
throw new Error(`No brownfield extraction provenance found at '${importPath}'. Run 'topogram extract <app-path> --out <target>' first.`);
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
40
|
return { path: importPath, record: JSON.parse(fs.readFileSync(importPath, "utf8")) };
|
|
41
41
|
} catch (error) {
|
|
42
|
-
throw new Error(`Invalid brownfield
|
|
42
|
+
throw new Error(`Invalid brownfield extraction provenance JSON at '${importPath}'.`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -48,8 +48,9 @@ export function readTopogramImportRecord(projectRoot) {
|
|
|
48
48
|
* @returns {string|null}
|
|
49
49
|
*/
|
|
50
50
|
export function importTrackValueFromRecord(importRecord) {
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
const source = importRecord.extract || importRecord.import || {};
|
|
52
|
+
const tracks = Array.isArray(source.tracks)
|
|
53
|
+
? source.tracks.map((/** @type {any} */ track) => String(track).trim()).filter(Boolean)
|
|
53
54
|
: [];
|
|
54
55
|
return tracks.length ? [...new Set(tracks)].join(",") : null;
|
|
55
56
|
}
|
|
@@ -283,7 +284,7 @@ export function buildBrownfieldImportRefreshAnalysis(inputPath, options = {}) {
|
|
|
283
284
|
const projectRoot = normalizeProjectRoot(inputPath);
|
|
284
285
|
const topogramRoot = normalizeTopogramPath(projectRoot);
|
|
285
286
|
if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
|
|
286
|
-
throw new Error(`No workspace folder found for
|
|
287
|
+
throw new Error(`No workspace folder found for extracted workspace '${inputPath}'.`);
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
const { record: importRecord } = readTopogramImportRecord(projectRoot);
|
|
@@ -291,21 +292,21 @@ export function buildBrownfieldImportRefreshAnalysis(inputPath, options = {}) {
|
|
|
291
292
|
? options.sourcePath
|
|
292
293
|
: importRecord.source?.path;
|
|
293
294
|
if (!sourcePath) {
|
|
294
|
-
throw new Error("No brownfield source path was provided or recorded. Use 'topogram
|
|
295
|
+
throw new Error("No brownfield source path was provided or recorded. Use 'topogram extract refresh <workspace> --from <app-path>'.");
|
|
295
296
|
}
|
|
296
297
|
const sourceRoot = path.resolve(sourcePath);
|
|
297
298
|
if (!fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
|
|
298
299
|
throw new Error(`Cannot refresh from missing app directory '${sourcePath}'.`);
|
|
299
300
|
}
|
|
300
301
|
if (sourceRoot === projectRoot) {
|
|
301
|
-
throw new Error("Refusing to refresh
|
|
302
|
+
throw new Error("Refusing to refresh extraction from the extracted Topogram workspace itself.");
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
const sourceComparison = compareImportRecordToSource(projectRoot, importRecord, sourceRoot);
|
|
305
306
|
const trackValue = importTrackValueFromRecord(importRecord);
|
|
306
307
|
const importResult = runWorkflow("import-app", sourceRoot, { from: trackValue });
|
|
307
308
|
const candidateCounts = importCandidateCounts(importResult.summary);
|
|
308
|
-
const candidateCountDeltas = buildCountDeltas(importRecord.import?.candidateCounts || {}, candidateCounts);
|
|
309
|
+
const candidateCountDeltas = buildCountDeltas((importRecord.extract || importRecord.import)?.candidateCounts || {}, candidateCounts);
|
|
309
310
|
const removedCandidateFiles = {
|
|
310
311
|
rawCandidateFiles: countFilesRecursive(path.join(topogramRoot, "candidates", "app")),
|
|
311
312
|
reconcileFiles: countFilesRecursive(path.join(topogramRoot, "candidates", "reconcile"))
|
|
@@ -324,7 +325,7 @@ export function buildBrownfieldImportRefreshAnalysis(inputPath, options = {}) {
|
|
|
324
325
|
topogramRoot,
|
|
325
326
|
sourcePath: sourceRoot,
|
|
326
327
|
provenancePath: path.join(projectRoot, TOPOGRAM_IMPORT_FILE),
|
|
327
|
-
importedAt: importRecord.importedAt || null,
|
|
328
|
+
importedAt: importRecord.extractedAt || importRecord.importedAt || null,
|
|
328
329
|
previousImportStatus: sourceComparison.status,
|
|
329
330
|
sourceDiff: {
|
|
330
331
|
status: sourceComparison.status,
|
|
@@ -354,13 +355,13 @@ export function buildBrownfieldImportRefreshAnalysis(inputPath, options = {}) {
|
|
|
354
355
|
/**
|
|
355
356
|
* @param {string} inputPath
|
|
356
357
|
* @param {{ sourcePath?: string|null, dryRun?: boolean }} [options]
|
|
357
|
-
* @returns {{ ok: boolean, dryRun: boolean, projectRoot: string, workspaceRoot: string, topogramRoot: string, sourcePath: string, provenancePath: string,
|
|
358
|
+
* @returns {{ ok: boolean, dryRun: boolean, projectRoot: string, workspaceRoot: string, topogramRoot: string, sourcePath: string, provenancePath: string, previousExtractStatus: string, currentExtractStatus: string, tracks: string[], sourceFiles: number, sourceDiff: Record<string, any>, removedCandidateFiles: Record<string, number>, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], plannedFiles: string[], candidateCounts: Record<string, number>, candidateCountDeltas: Record<string, any>, adoptionPlanDeltas: Record<string, any>, receiptVerification: Record<string, any>, refreshMetadata: Record<string, any>|null, nextCommands: string[] }}
|
|
358
359
|
*/
|
|
359
360
|
export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
|
|
360
361
|
const analysis = buildBrownfieldImportRefreshAnalysis(inputPath, options);
|
|
361
362
|
const dryRun = Boolean(options.dryRun);
|
|
362
363
|
let provenancePath = analysis.provenancePath;
|
|
363
|
-
let
|
|
364
|
+
let currentExtractStatus = dryRun ? analysis.previousImportStatus : "unknown";
|
|
364
365
|
/** @type {string[]} */
|
|
365
366
|
let writtenFiles = [];
|
|
366
367
|
/** @type {AnyRecord|null} */
|
|
@@ -391,7 +392,7 @@ export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
|
|
|
391
392
|
files: collectImportSourceFileRecords(analysis.sourcePath, { excludeRoots: [analysis.projectRoot] })
|
|
392
393
|
});
|
|
393
394
|
provenancePath = provenance.path;
|
|
394
|
-
|
|
395
|
+
currentExtractStatus = buildTopogramImportStatus(analysis.projectRoot).status;
|
|
395
396
|
writtenFiles = [
|
|
396
397
|
TOPOGRAM_IMPORT_FILE,
|
|
397
398
|
...rawCandidateFiles.map((filePath) => `${DEFAULT_TOPO_FOLDER_NAME}/${filePath}`),
|
|
@@ -402,15 +403,15 @@ export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
|
|
|
402
403
|
analysis.reconcileFiles = reconcileFiles.length;
|
|
403
404
|
}
|
|
404
405
|
return {
|
|
405
|
-
ok: dryRun ||
|
|
406
|
+
ok: dryRun || currentExtractStatus === "clean",
|
|
406
407
|
dryRun,
|
|
407
408
|
projectRoot: analysis.projectRoot,
|
|
408
409
|
workspaceRoot: analysis.topogramRoot,
|
|
409
410
|
topogramRoot: analysis.topogramRoot,
|
|
410
411
|
sourcePath: analysis.sourcePath,
|
|
411
412
|
provenancePath,
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
previousExtractStatus: analysis.previousImportStatus,
|
|
414
|
+
currentExtractStatus,
|
|
414
415
|
tracks: analysis.tracks,
|
|
415
416
|
sourceFiles: analysis.sourceFiles,
|
|
416
417
|
sourceDiff: analysis.sourceDiff,
|
|
@@ -426,11 +427,11 @@ export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
|
|
|
426
427
|
refreshMetadata,
|
|
427
428
|
nextCommands: [
|
|
428
429
|
dryRun
|
|
429
|
-
? `topogram
|
|
430
|
-
: `topogram
|
|
431
|
-
`topogram
|
|
432
|
-
`topogram
|
|
433
|
-
`topogram
|
|
430
|
+
? `topogram extract refresh ${importProjectCommandPath(analysis.projectRoot)}`
|
|
431
|
+
: `topogram extract check ${importProjectCommandPath(analysis.projectRoot)}`,
|
|
432
|
+
`topogram extract plan ${importProjectCommandPath(analysis.projectRoot)}`,
|
|
433
|
+
`topogram extract status ${importProjectCommandPath(analysis.projectRoot)}`,
|
|
434
|
+
`topogram extract history ${importProjectCommandPath(analysis.projectRoot)} --verify`
|
|
434
435
|
]
|
|
435
436
|
};
|
|
436
437
|
}
|
|
@@ -440,12 +441,12 @@ export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
|
|
|
440
441
|
* @returns {void}
|
|
441
442
|
*/
|
|
442
443
|
export function printBrownfieldImportRefresh(payload) {
|
|
443
|
-
console.log(`${payload.dryRun ? "Previewed" : "Refreshed"} brownfield
|
|
444
|
+
console.log(`${payload.dryRun ? "Previewed" : "Refreshed"} brownfield extraction candidates for ${payload.projectRoot}.`);
|
|
444
445
|
console.log(`Source: ${payload.sourcePath}`);
|
|
445
446
|
console.log(`Topogram: ${payload.topogramRoot}`);
|
|
446
|
-
console.log(`
|
|
447
|
-
console.log(`Previous source status: ${payload.
|
|
448
|
-
console.log(`Current source status: ${payload.
|
|
447
|
+
console.log(`Extraction provenance: ${payload.provenancePath}`);
|
|
448
|
+
console.log(`Previous source status: ${payload.previousExtractStatus}`);
|
|
449
|
+
console.log(`Current source status: ${payload.currentExtractStatus}`);
|
|
449
450
|
console.log(`Source diff: changed=${payload.sourceDiff.counts.changed}, added=${payload.sourceDiff.counts.added}, removed=${payload.sourceDiff.counts.removed}`);
|
|
450
451
|
console.log(`Tracked source files: ${payload.sourceFiles}`);
|
|
451
452
|
console.log(`Raw candidate files: ${payload.rawCandidateFiles}`);
|
|
@@ -33,7 +33,7 @@ export function buildBrownfieldImportStatusPayload(inputPath) {
|
|
|
33
33
|
projectRoot: artifacts.projectRoot,
|
|
34
34
|
workspaceRoot: artifacts.topogramRoot,
|
|
35
35
|
topogramRoot: artifacts.topogramRoot,
|
|
36
|
-
|
|
36
|
+
extract: importCheck.extract,
|
|
37
37
|
topogram: importCheck.topogram,
|
|
38
38
|
adoption: {
|
|
39
39
|
status: adoptionStatus,
|
|
@@ -52,7 +52,7 @@ export function buildBrownfieldImportStatusPayload(inputPath) {
|
|
|
52
52
|
* @returns {void}
|
|
53
53
|
*/
|
|
54
54
|
export function printBrownfieldImportStatus(payload) {
|
|
55
|
-
console.log(`
|
|
55
|
+
console.log(`Extraction status: ${payload.extract.status}`);
|
|
56
56
|
console.log(`Topogram check: ${payload.topogram.ok ? "passed" : "failed"}`);
|
|
57
57
|
console.log(`Adoption: ${payload.adoption.summary.appliedItemCount} applied, ${payload.adoption.summary.pendingItemCount} pending, ${payload.adoption.summary.blockedItemCount} blocked`);
|
|
58
58
|
const next = payload.adoption.nextCommand;
|
|
@@ -134,7 +134,7 @@ export function verifyImportAdoptionReceipts(projectRoot, receipts) {
|
|
|
134
134
|
summary,
|
|
135
135
|
files,
|
|
136
136
|
auditOnly: true,
|
|
137
|
-
note: "History verification is audit-only.
|
|
137
|
+
note: "History verification is audit-only. Extracted/adopted Topogram files are project-owned, and edits do not make the workspace invalid."
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -175,7 +175,7 @@ export function buildBrownfieldImportHistoryPayload(inputPath, options = {}) {
|
|
|
175
175
|
* @returns {void}
|
|
176
176
|
*/
|
|
177
177
|
export function printBrownfieldImportHistory(payload) {
|
|
178
|
-
console.log(`
|
|
178
|
+
console.log(`Adoption history for ${payload.projectRoot}`);
|
|
179
179
|
console.log(`Receipts: ${payload.summary.receiptCount}`);
|
|
180
180
|
console.log(`Forced writes: ${payload.summary.forcedWriteCount}`);
|
|
181
181
|
if (!payload.exists) {
|
|
@@ -79,23 +79,23 @@ function importedProjectConfig() {
|
|
|
79
79
|
*/
|
|
80
80
|
function importedWorkspaceReadme(sourceRoot, targetRoot, importSummary) {
|
|
81
81
|
return [
|
|
82
|
-
"#
|
|
82
|
+
"# Extracted Topogram Workspace",
|
|
83
83
|
"",
|
|
84
|
-
"This workspace was created from a brownfield app
|
|
84
|
+
"This workspace was created from a brownfield app extraction.",
|
|
85
85
|
"",
|
|
86
|
-
`-
|
|
86
|
+
`- Extracted source: \`${sourceRoot}\``,
|
|
87
87
|
`- Target workspace: \`${targetRoot}\``,
|
|
88
88
|
`- Tracks: ${(importSummary.tracks || []).join(", ") || "none"}`,
|
|
89
89
|
`- Provenance: \`${TOPOGRAM_IMPORT_FILE}\``,
|
|
90
90
|
"",
|
|
91
|
-
"
|
|
91
|
+
"Extracted Topogram artifacts are project-owned after creation. Edit them directly, promote candidates deliberately, and run `topogram check` before generation or maintained-app work.",
|
|
92
92
|
"",
|
|
93
93
|
"Useful commands:",
|
|
94
94
|
"",
|
|
95
95
|
"```sh",
|
|
96
|
-
"topogram
|
|
96
|
+
"topogram extract check",
|
|
97
97
|
"topogram check",
|
|
98
|
-
`topogram query
|
|
98
|
+
`topogram query extract-plan ./${DEFAULT_TOPO_FOLDER_NAME}`,
|
|
99
99
|
"```",
|
|
100
100
|
""
|
|
101
101
|
].join("\n");
|
|
@@ -149,24 +149,29 @@ export function countFilesRecursive(rootPath) {
|
|
|
149
149
|
/**
|
|
150
150
|
* @param {string} sourcePath
|
|
151
151
|
* @param {string} targetPath
|
|
152
|
-
* @param {{ from?: string|null }} [options]
|
|
152
|
+
* @param {{ from?: string|null, extractorSpecs?: string[], extractorPolicyPath?: string|null, cwd?: string|null }} [options]
|
|
153
153
|
* @returns {{ ok: boolean, sourcePath: string, targetPath: string, workspaceRoot: string, topogramRoot: string, projectConfigPath: string, provenancePath: string, tracks: string[], sourceFiles: number, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], candidateCounts: Record<string, number>, nextCommands: string[] }}
|
|
154
154
|
*/
|
|
155
155
|
export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, options = {}) {
|
|
156
156
|
const sourceRoot = path.resolve(sourcePath);
|
|
157
157
|
const targetRoot = path.resolve(targetPath);
|
|
158
158
|
if (!fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
|
|
159
|
-
throw new Error(`Cannot
|
|
159
|
+
throw new Error(`Cannot extract missing app directory '${sourcePath}'.`);
|
|
160
160
|
}
|
|
161
161
|
if (sourceRoot === targetRoot) {
|
|
162
|
-
throw new Error("Refusing to
|
|
162
|
+
throw new Error("Refusing to extract into the same directory as the brownfield app.");
|
|
163
163
|
}
|
|
164
164
|
ensureEmptyImportTarget(targetRoot);
|
|
165
165
|
|
|
166
166
|
const topogramRoot = path.join(targetRoot, DEFAULT_TOPO_FOLDER_NAME);
|
|
167
167
|
fs.mkdirSync(topogramRoot, { recursive: true });
|
|
168
168
|
const sourceFiles = collectImportSourceFileRecords(sourceRoot, { excludeRoots: [targetRoot] });
|
|
169
|
-
const importResult = runWorkflow("import-app", sourceRoot, {
|
|
169
|
+
const importResult = runWorkflow("import-app", sourceRoot, {
|
|
170
|
+
from: options.from || null,
|
|
171
|
+
extractorSpecs: options.extractorSpecs || [],
|
|
172
|
+
extractorPolicyPath: options.extractorPolicyPath || null,
|
|
173
|
+
cwd: options.cwd || process.cwd()
|
|
174
|
+
});
|
|
170
175
|
const rawCandidateFiles = writeRelativeFiles(topogramRoot, importResult.files || {});
|
|
171
176
|
|
|
172
177
|
const projectConfigPath = path.join(targetRoot, "topogram.project.json");
|
|
@@ -182,6 +187,7 @@ export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, op
|
|
|
182
187
|
tracks: importResult.summary.tracks || [],
|
|
183
188
|
findingsCount: importResult.summary.findings_count || 0,
|
|
184
189
|
candidateCounts,
|
|
190
|
+
extractorPackages: importResult.summary.package_extractors || [],
|
|
185
191
|
files: sourceFiles
|
|
186
192
|
});
|
|
187
193
|
const writtenFiles = [
|
|
@@ -206,12 +212,12 @@ export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, op
|
|
|
206
212
|
writtenFiles,
|
|
207
213
|
candidateCounts,
|
|
208
214
|
nextCommands: [
|
|
209
|
-
"topogram
|
|
210
|
-
"topogram
|
|
211
|
-
"topogram
|
|
212
|
-
"topogram
|
|
215
|
+
"topogram extract check",
|
|
216
|
+
"topogram extract plan",
|
|
217
|
+
"topogram adopt bundle:task --dry-run",
|
|
218
|
+
"topogram extract status",
|
|
213
219
|
"topogram check",
|
|
214
|
-
`topogram query
|
|
220
|
+
`topogram query extract-plan ./${DEFAULT_TOPO_FOLDER_NAME}`
|
|
215
221
|
]
|
|
216
222
|
};
|
|
217
223
|
}
|
|
@@ -221,15 +227,15 @@ export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, op
|
|
|
221
227
|
* @returns {void}
|
|
222
228
|
*/
|
|
223
229
|
export function printBrownfieldImportWorkspace(payload) {
|
|
224
|
-
console.log(`
|
|
230
|
+
console.log(`Extracted brownfield app to ${payload.targetPath}.`);
|
|
225
231
|
console.log(`Source: ${payload.sourcePath}`);
|
|
226
232
|
console.log(`Topogram: ${payload.topogramRoot}`);
|
|
227
233
|
console.log(`Project config: ${payload.projectConfigPath}`);
|
|
228
|
-
console.log(`
|
|
234
|
+
console.log(`Extraction provenance: ${payload.provenancePath}`);
|
|
229
235
|
console.log(`Tracked source files: ${payload.sourceFiles}`);
|
|
230
236
|
console.log(`Raw candidate files: ${payload.rawCandidateFiles}`);
|
|
231
237
|
console.log(`Reconcile proposal files: ${payload.reconcileFiles}`);
|
|
232
|
-
console.log("
|
|
238
|
+
console.log("Extracted Topogram artifacts are project-owned after creation; source hashes record the app evidence trusted at extraction time.");
|
|
233
239
|
console.log("");
|
|
234
240
|
console.log("Next steps:");
|
|
235
241
|
console.log(` cd ${shellCommandArg(path.relative(process.cwd(), payload.targetPath) || ".")}`);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
buildBrownfieldImportRefreshPayload,
|
|
12
12
|
buildBrownfieldImportStatusPayload,
|
|
13
13
|
buildBrownfieldImportWorkspacePayload,
|
|
14
|
+
printAdoptHelp,
|
|
14
15
|
printBrownfieldImportAdopt,
|
|
15
16
|
printBrownfieldImportAdoptList,
|
|
16
17
|
printBrownfieldImportCheck,
|
|
@@ -20,11 +21,11 @@ import {
|
|
|
20
21
|
printBrownfieldImportRefresh,
|
|
21
22
|
printBrownfieldImportStatus,
|
|
22
23
|
printBrownfieldImportWorkspace,
|
|
23
|
-
|
|
24
|
+
printExtractHelp
|
|
24
25
|
} from "./import.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* @param {{ commandArgs: Record<string, any>, inputPath: string|null|undefined, outPath?: string|null, fromValue?: string|null, reasonValue?: string|null, refreshAdopted?: boolean, dryRun?: boolean, write?: boolean, force?: boolean, json?: boolean }} context
|
|
28
|
+
* @param {{ commandArgs: Record<string, any>, inputPath: string|null|undefined, outPath?: string|null, fromValue?: string|null, extractorSpecs?: string[], extractorPolicyPath?: string|null, reasonValue?: string|null, refreshAdopted?: boolean, dryRun?: boolean, write?: boolean, force?: boolean, json?: boolean }} context
|
|
28
29
|
* @returns {number}
|
|
29
30
|
*/
|
|
30
31
|
export function runImportCommand(context) {
|
|
@@ -33,6 +34,8 @@ export function runImportCommand(context) {
|
|
|
33
34
|
inputPath,
|
|
34
35
|
outPath = null,
|
|
35
36
|
fromValue = null,
|
|
37
|
+
extractorSpecs = [],
|
|
38
|
+
extractorPolicyPath = null,
|
|
36
39
|
reasonValue = null,
|
|
37
40
|
refreshAdopted = false,
|
|
38
41
|
dryRun = false,
|
|
@@ -45,10 +48,10 @@ export function runImportCommand(context) {
|
|
|
45
48
|
if (command === "workspace") {
|
|
46
49
|
if (!outPath) {
|
|
47
50
|
console.error("Missing required --out <target>.");
|
|
48
|
-
|
|
51
|
+
printExtractHelp();
|
|
49
52
|
return 1;
|
|
50
53
|
}
|
|
51
|
-
const payload = buildBrownfieldImportWorkspacePayload(inputPath || "", outPath, { from: fromValue });
|
|
54
|
+
const payload = buildBrownfieldImportWorkspacePayload(inputPath || "", outPath, { from: fromValue, extractorSpecs, extractorPolicyPath, cwd: process.cwd() });
|
|
52
55
|
if (json) {
|
|
53
56
|
console.log(stableStringify(payload));
|
|
54
57
|
} else {
|
|
@@ -110,12 +113,12 @@ export function runImportCommand(context) {
|
|
|
110
113
|
if (command === "adopt") {
|
|
111
114
|
if (!commandArgs.importAdoptSelector || commandArgs.importAdoptSelector.startsWith("-")) {
|
|
112
115
|
console.error("Missing required <selector>.");
|
|
113
|
-
|
|
116
|
+
printAdoptHelp();
|
|
114
117
|
return 1;
|
|
115
118
|
}
|
|
116
119
|
if (write && dryRun) {
|
|
117
120
|
console.error("Use either --dry-run or --write, not both.");
|
|
118
|
-
|
|
121
|
+
printAdoptHelp();
|
|
119
122
|
return 1;
|
|
120
123
|
}
|
|
121
124
|
const payload = buildBrownfieldImportAdoptPayload(commandArgs.importAdoptSelector, inputPath || ".", {
|
|
@@ -153,5 +156,5 @@ export function runImportCommand(context) {
|
|
|
153
156
|
return payload.ok ? 0 : 1;
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
throw new Error(`Unknown
|
|
159
|
+
throw new Error(`Unknown extract/adopt command '${command}'`);
|
|
157
160
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { stableStringify } from "../../format.js";
|
|
6
|
+
import { initTopogramProject } from "../../init-project.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} projectRoot
|
|
10
|
+
* @param {string} cwd
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function displayProjectRoot(projectRoot, cwd) {
|
|
14
|
+
const relative = path.relative(cwd, projectRoot);
|
|
15
|
+
return !relative || relative.startsWith("..")
|
|
16
|
+
? projectRoot
|
|
17
|
+
: relative.split(path.sep).join("/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {ReturnType<typeof initTopogramProject>} result
|
|
22
|
+
* @param {string} cwd
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
export function printInitProjectResult(result, cwd) {
|
|
26
|
+
console.log(`Initialized Topogram workspace at ${result.projectRoot}.`);
|
|
27
|
+
console.log("Workspace: topo/");
|
|
28
|
+
console.log("Project config: topogram.project.json");
|
|
29
|
+
console.log("Output ownership: maintained (.)");
|
|
30
|
+
console.log("Template: none");
|
|
31
|
+
console.log("Generated app output: none");
|
|
32
|
+
console.log(`SDLC: ${result.sdlc.enabled ? "adopted/enforced" : "not adopted"}`);
|
|
33
|
+
if (result.created.length > 0) {
|
|
34
|
+
console.log(`Created: ${result.created.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
if (result.skipped.length > 0) {
|
|
37
|
+
console.log(`Skipped existing files: ${result.skipped.join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("Next steps:");
|
|
41
|
+
console.log(` cd ${displayProjectRoot(result.projectRoot, cwd)}`);
|
|
42
|
+
console.log(" topogram agent brief --json");
|
|
43
|
+
console.log(" topogram check --json");
|
|
44
|
+
console.log(" topogram query list --json");
|
|
45
|
+
if (result.sdlc.enabled) {
|
|
46
|
+
console.log(" topogram sdlc policy explain --json");
|
|
47
|
+
console.log(" topogram sdlc prep commit . --json");
|
|
48
|
+
} else {
|
|
49
|
+
console.log(" topogram sdlc policy init .");
|
|
50
|
+
}
|
|
51
|
+
console.log(" topogram emit <target> ./topo --json");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} inputPath
|
|
56
|
+
* @param {{ json?: boolean, cwd?: string, withSdlc?: boolean }} [options]
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export function runInitProjectCommand(inputPath, options = {}) {
|
|
60
|
+
const result = initTopogramProject({ targetPath: inputPath || ".", withSdlc: Boolean(options.withSdlc) });
|
|
61
|
+
if (options.json) {
|
|
62
|
+
console.log(stableStringify(result));
|
|
63
|
+
} else {
|
|
64
|
+
printInitProjectResult(result, options.cwd || process.cwd());
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
@@ -38,7 +38,7 @@ export function buildImportPlanForContext(context, queryFamily) {
|
|
|
38
38
|
}
|
|
39
39
|
const adoptionPlan = readJson(planPath);
|
|
40
40
|
const ast = parsePath(context.inputPath);
|
|
41
|
-
const taskModeResult = buildTaskMode(ast, {}, "
|
|
41
|
+
const taskModeResult = buildTaskMode(ast, {}, "extract-adopt");
|
|
42
42
|
if (!resultOk(taskModeResult)) {
|
|
43
43
|
return taskModeResult;
|
|
44
44
|
}
|
|
@@ -74,7 +74,7 @@ export function buildImportAdoptAgentContext(context, queryFamily) {
|
|
|
74
74
|
const topogramRoot = normalizeTopogramPath(context.inputPath);
|
|
75
75
|
const artifacts = requireReconcileArtifacts(topogramRoot, queryFamily);
|
|
76
76
|
const ast = parsePath(context.inputPath);
|
|
77
|
-
const taskModeResult = buildTaskMode(ast, {}, "
|
|
77
|
+
const taskModeResult = buildTaskMode(ast, {}, "extract-adopt");
|
|
78
78
|
if (!resultOk(taskModeResult)) {
|
|
79
79
|
return taskModeResult;
|
|
80
80
|
}
|