@ductape/cli 0.2.23 → 0.3.1

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 (104) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +7 -0
  3. package/dist/commands/db-migrate.js +47 -8
  4. package/dist/commands/migrate-codebase.d.ts +190 -0
  5. package/dist/commands/migrate-codebase.js +776 -0
  6. package/dist/commands/migration-artifact.d.ts +28 -0
  7. package/dist/commands/migration-artifact.js +101 -0
  8. package/dist/commands/migration-assurance.d.ts +10 -0
  9. package/dist/commands/migration-assurance.js +120 -0
  10. package/dist/commands/migration-capabilities.d.ts +53 -0
  11. package/dist/commands/migration-capabilities.js +66 -0
  12. package/dist/commands/migration-database.d.ts +26 -0
  13. package/dist/commands/migration-database.js +162 -0
  14. package/dist/commands/migration-e2e.d.ts +68 -0
  15. package/dist/commands/migration-e2e.js +189 -0
  16. package/dist/commands/migration-environments.d.ts +20 -0
  17. package/dist/commands/migration-environments.js +58 -0
  18. package/dist/commands/migration-generated.d.ts +68 -0
  19. package/dist/commands/migration-generated.js +217 -0
  20. package/dist/commands/migration-portfolio.d.ts +88 -0
  21. package/dist/commands/migration-portfolio.js +223 -0
  22. package/dist/commands/migration-products.d.ts +23 -0
  23. package/dist/commands/migration-products.js +88 -0
  24. package/dist/commands/migration-review.d.ts +248 -0
  25. package/dist/commands/migration-review.js +624 -0
  26. package/dist/commands/migration-secrets.d.ts +24 -0
  27. package/dist/commands/migration-secrets.js +93 -0
  28. package/dist/commands/migration-slice.d.ts +132 -0
  29. package/dist/commands/migration-slice.js +266 -0
  30. package/dist/commands/migration-verification.d.ts +12 -0
  31. package/dist/commands/migration-verification.js +194 -0
  32. package/dist/commands/products.d.ts +1 -0
  33. package/dist/commands/products.js +33 -1
  34. package/dist/commands/secrets.d.ts +8 -0
  35. package/dist/commands/secrets.js +44 -0
  36. package/dist/index.js +376 -4
  37. package/dist/lib/migration-artifact.d.ts +19 -0
  38. package/dist/lib/migration-artifact.js +114 -0
  39. package/package.json +8 -1
  40. package/API_PARITY.md +0 -58
  41. package/DB_MIGRATE_PLAN.md +0 -223
  42. package/ductape.example.ts +0 -18
  43. package/src/commands/apply.ts +0 -153
  44. package/src/commands/apps-import.ts +0 -43
  45. package/src/commands/apps.ts +0 -92
  46. package/src/commands/cloud.ts +0 -68
  47. package/src/commands/completion.ts +0 -119
  48. package/src/commands/db-migrate.ts +0 -209
  49. package/src/commands/db-schema.ts +0 -429
  50. package/src/commands/db.ts +0 -63
  51. package/src/commands/generate.ts +0 -74
  52. package/src/commands/graph.ts +0 -51
  53. package/src/commands/init.ts +0 -39
  54. package/src/commands/install.ts +0 -76
  55. package/src/commands/link.ts +0 -84
  56. package/src/commands/login.ts +0 -145
  57. package/src/commands/logout.ts +0 -7
  58. package/src/commands/platform.ts +0 -123
  59. package/src/commands/products.ts +0 -228
  60. package/src/commands/profiles.ts +0 -23
  61. package/src/commands/resources.ts +0 -213
  62. package/src/commands/secrets.ts +0 -46
  63. package/src/commands/unlink.ts +0 -15
  64. package/src/commands/whoami.ts +0 -27
  65. package/src/commands/workspaces.ts +0 -98
  66. package/src/index.ts +0 -523
  67. package/src/lib/api-client.ts +0 -88
  68. package/src/lib/app-import.ts +0 -82
  69. package/src/lib/apply-loaders.ts +0 -30
  70. package/src/lib/config.ts +0 -230
  71. package/src/lib/context-store.ts +0 -77
  72. package/src/lib/db-methods.ts +0 -56
  73. package/src/lib/db-types.ts +0 -104
  74. package/src/lib/encryption.ts +0 -12
  75. package/src/lib/forms/enums.ts +0 -52
  76. package/src/lib/forms/index.ts +0 -8
  77. package/src/lib/forms/prompt.ts +0 -209
  78. package/src/lib/forms/schemas.ts +0 -288
  79. package/src/lib/forms/types.ts +0 -27
  80. package/src/lib/http.ts +0 -76
  81. package/src/lib/integrations.ts +0 -110
  82. package/src/lib/interactive-opts.ts +0 -13
  83. package/src/lib/migration-files.ts +0 -58
  84. package/src/lib/output.ts +0 -39
  85. package/src/lib/platform-api.ts +0 -209
  86. package/src/lib/proxy/context.ts +0 -94
  87. package/src/lib/proxy/db-proxy.ts +0 -40
  88. package/src/lib/proxy/graph-proxy.ts +0 -64
  89. package/src/lib/proxy/sdk-proxy.ts +0 -58
  90. package/src/lib/read-body.ts +0 -57
  91. package/src/lib/resources.ts +0 -160
  92. package/src/lib/schema-loader.ts +0 -152
  93. package/src/lib/snippets.ts +0 -165
  94. package/src/lib/templates.ts +0 -157
  95. package/src/lib/workspace-context.ts +0 -43
  96. package/src/lib/workspaces.ts +0 -283
  97. package/test/apply-loaders.test.ts +0 -40
  98. package/test/encryption.test.ts +0 -20
  99. package/test/forms.test.ts +0 -25
  100. package/test/http.test.ts +0 -15
  101. package/test/platform-api.test.ts +0 -42
  102. package/test/resources.test.ts +0 -23
  103. package/test/workspace-resolve.test.ts +0 -29
  104. package/tsconfig.json +0 -15
@@ -0,0 +1,189 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { printJson } from '../lib/output.js';
5
+ import { readMigrationJson, writeMigrationJson } from '../lib/migration-artifact.js';
6
+ function readJson(file) {
7
+ return readMigrationJson(file);
8
+ }
9
+ function checksum(file) {
10
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
11
+ }
12
+ function relativeFile(source, file) {
13
+ if (!file || path.isAbsolute(file))
14
+ throw new Error(`E2E suite file must be repository-relative: ${file}`);
15
+ const normalized = path.normalize(file).replaceAll('\\', '/');
16
+ if (normalized === '..' || normalized.startsWith('../')) {
17
+ throw new Error(`E2E suite file escapes the source repository: ${file}`);
18
+ }
19
+ const resolved = path.resolve(source, normalized);
20
+ if (!fs.statSync(resolved).isFile())
21
+ throw new Error(`E2E suite file does not exist: ${normalized}`);
22
+ return normalized;
23
+ }
24
+ function requireEvidence(value, label) {
25
+ if (!value.command?.trim())
26
+ throw new Error(`${label} command is required.`);
27
+ if (value.status !== 'passed')
28
+ throw new Error(`${label} must have passed before it can be recorded.`);
29
+ if (!value.environment?.trim())
30
+ throw new Error(`${label} environment evidence is required.`);
31
+ if (!Array.isArray(value.evidence) || value.evidence.length === 0 ||
32
+ value.evidence.some((item) => !item?.trim())) {
33
+ throw new Error(`${label} requires at least one non-empty evidence entry.`);
34
+ }
35
+ }
36
+ export function initializeMigrationE2E(opts) {
37
+ const source = path.resolve(opts.source);
38
+ if (!fs.statSync(source).isDirectory())
39
+ throw new Error(`Migration source is not a directory: ${source}`);
40
+ const definition = readJson(path.resolve(opts.definition));
41
+ requireEvidence({ command: definition.command, ...definition.baseline }, 'Baseline E2E run');
42
+ if (!Array.isArray(definition.suite_files) || definition.suite_files.length === 0) {
43
+ throw new Error('Baseline E2E suite_files must contain at least one test or fixture.');
44
+ }
45
+ const files = [...new Set(definition.suite_files.map((file) => relativeFile(source, file)))].sort();
46
+ if (files.length !== definition.suite_files.length)
47
+ throw new Error('Baseline E2E suite_files contains duplicates.');
48
+ const output = path.resolve(opts.output ?? path.join(source, 'ductape', 'migration-guidance', 'e2e-baseline.json'));
49
+ if (fs.existsSync(output))
50
+ throw new Error(`Migration E2E baseline already exists: ${output}`);
51
+ const manifest = {
52
+ version: 1,
53
+ artifact_kind: 'ductape_migration_e2e_baseline',
54
+ source,
55
+ created_at: new Date().toISOString(),
56
+ command: definition.command,
57
+ suite: files.map((file) => ({ file, checksum: checksum(path.join(source, file)) })),
58
+ baseline: definition.baseline,
59
+ final: null,
60
+ };
61
+ fs.mkdirSync(path.dirname(output), { recursive: true });
62
+ writeMigrationJson(output, manifest, { createOnly: true });
63
+ return { path: output, manifest };
64
+ }
65
+ export function recordMigrationE2EFinal(manifestFile, evidenceFile) {
66
+ const resolved = path.resolve(manifestFile);
67
+ const manifest = readJson(resolved);
68
+ if (manifest.artifact_kind !== 'ductape_migration_e2e_baseline') {
69
+ throw new Error('Invalid migration E2E baseline manifest.');
70
+ }
71
+ if (manifest.final)
72
+ throw new Error('Final E2E evidence is already recorded.');
73
+ const evidence = readJson(path.resolve(evidenceFile));
74
+ requireEvidence(evidence, 'Final E2E run');
75
+ if (evidence.command !== manifest.command) {
76
+ throw new Error('Final E2E command must exactly match the original baseline command.');
77
+ }
78
+ if (!evidence.suite_root?.trim() || !path.isAbsolute(evidence.suite_root) ||
79
+ !fs.existsSync(evidence.suite_root) || !fs.statSync(evidence.suite_root).isDirectory()) {
80
+ throw new Error('Final E2E suite_root must be an existing absolute migrated-codebase directory.');
81
+ }
82
+ const changed = manifest.suite.filter(({ file, checksum: expected }) => {
83
+ try {
84
+ return checksum(path.join(manifest.source, file)) !== expected;
85
+ }
86
+ catch {
87
+ return true;
88
+ }
89
+ });
90
+ if (changed.length) {
91
+ throw new Error(`Original E2E baseline changed or is missing: ${changed.map((item) => item.file).join(', ')}`);
92
+ }
93
+ const migratedMismatch = manifest.suite.filter(({ file, checksum: expected }) => {
94
+ try {
95
+ return checksum(path.join(evidence.suite_root, file)) !== expected;
96
+ }
97
+ catch {
98
+ return true;
99
+ }
100
+ });
101
+ if (migratedMismatch.length) {
102
+ throw new Error(`Migrated codebase is not using the original E2E suite: ${migratedMismatch.map((item) => item.file).join(', ')}`);
103
+ }
104
+ manifest.final = { ...evidence, recorded_at: new Date().toISOString() };
105
+ writeMigrationJson(resolved, manifest, { backup: true });
106
+ return manifest;
107
+ }
108
+ export function validateMigrationE2E(manifestFile) {
109
+ const resolved = path.resolve(manifestFile);
110
+ const manifest = readJson(resolved);
111
+ if (manifest.artifact_kind !== 'ductape_migration_e2e_baseline') {
112
+ throw new Error('Invalid migration E2E baseline manifest.');
113
+ }
114
+ const blockers = [];
115
+ if (!manifest.suite?.length)
116
+ blockers.push('original E2E suite is empty');
117
+ for (const item of manifest.suite ?? []) {
118
+ try {
119
+ if (checksum(path.join(manifest.source, item.file)) !== item.checksum) {
120
+ blockers.push(`${item.file}: original baseline checksum changed`);
121
+ }
122
+ }
123
+ catch {
124
+ blockers.push(`${item.file}: original baseline file is missing`);
125
+ }
126
+ }
127
+ if (manifest.baseline?.status !== 'passed' || !manifest.baseline.evidence?.length) {
128
+ blockers.push('passing baseline E2E evidence is missing');
129
+ }
130
+ if (!manifest.final)
131
+ blockers.push('final run against the original E2E suite is missing');
132
+ else {
133
+ if (manifest.final.command !== manifest.command)
134
+ blockers.push('final command differs from baseline command');
135
+ if (manifest.final.status !== 'passed' || !manifest.final.evidence?.length) {
136
+ blockers.push('passing final E2E evidence is missing');
137
+ }
138
+ for (const item of manifest.suite ?? []) {
139
+ try {
140
+ if (checksum(path.join(manifest.final.suite_root, item.file)) !== item.checksum) {
141
+ blockers.push(`${item.file}: migrated suite differs from original baseline`);
142
+ }
143
+ }
144
+ catch {
145
+ blockers.push(`${item.file}: migrated suite file is missing`);
146
+ }
147
+ }
148
+ }
149
+ return { manifest: resolved, ready: blockers.length === 0, suite_files: manifest.suite?.length ?? 0, blockers };
150
+ }
151
+ export function validateMigrationE2EBaseline(manifestFile) {
152
+ const resolved = path.resolve(manifestFile);
153
+ const manifest = readJson(resolved);
154
+ if (manifest.artifact_kind !== 'ductape_migration_e2e_baseline') {
155
+ throw new Error('Invalid migration E2E baseline manifest.');
156
+ }
157
+ const blockers = [];
158
+ if (!manifest.suite?.length)
159
+ blockers.push('original E2E suite is empty');
160
+ for (const item of manifest.suite ?? []) {
161
+ try {
162
+ if (checksum(path.join(manifest.source, item.file)) !== item.checksum) {
163
+ blockers.push(`${item.file}: original baseline checksum changed`);
164
+ }
165
+ }
166
+ catch {
167
+ blockers.push(`${item.file}: original baseline file is missing`);
168
+ }
169
+ }
170
+ if (manifest.baseline?.status !== 'passed' || !manifest.baseline.evidence?.length) {
171
+ blockers.push('passing baseline E2E evidence is missing');
172
+ }
173
+ return { manifest: resolved, ready: blockers.length === 0, blockers };
174
+ }
175
+ export function runMigrationE2EInit(opts) {
176
+ const result = initializeMigrationE2E(opts);
177
+ printJson({ path: result.path, suite_files: result.manifest.suite.length }, Boolean(opts.json));
178
+ }
179
+ export function runMigrationE2EFinal(opts) {
180
+ const manifest = recordMigrationE2EFinal(opts.manifest, opts.evidence);
181
+ printJson({ manifest: path.resolve(opts.manifest), final_status: manifest.final?.status }, Boolean(opts.json));
182
+ }
183
+ export function runMigrationE2EValidate(opts) {
184
+ const result = validateMigrationE2E(opts.manifest);
185
+ printJson(result, Boolean(opts.json));
186
+ if (opts.strict && !result.ready) {
187
+ throw new Error('Migration E2E gate is not ready: resolve every reported blocker.');
188
+ }
189
+ }
@@ -0,0 +1,20 @@
1
+ export declare function reconcileEnvironments(analysisPath: string, inventoryPath: string): {
2
+ ready: boolean;
3
+ detected: string[];
4
+ live: string[];
5
+ matched: string[];
6
+ missing: string[];
7
+ extra: string[];
8
+ ambiguous: {
9
+ normalized: string;
10
+ candidates: string[];
11
+ }[];
12
+ creation_supported: boolean;
13
+ creation_blocker: string;
14
+ };
15
+ export declare function runMigrationEnvironmentsReconcile(opts: {
16
+ analysis: string;
17
+ inventory: string;
18
+ strict?: boolean;
19
+ json?: boolean;
20
+ }): void;
@@ -0,0 +1,58 @@
1
+ import { printJson } from '../lib/output.js';
2
+ import { readMigrationJson } from '../lib/migration-artifact.js';
3
+ function read(file) {
4
+ return readMigrationJson(file);
5
+ }
6
+ function normalize(value) {
7
+ const v = value.toLowerCase();
8
+ if (['production', 'prod', 'prd'].includes(v))
9
+ return 'prd';
10
+ if (['staging', 'stage', 'stg'].includes(v))
11
+ return 'stg';
12
+ if (['sandbox', 'snd'].includes(v))
13
+ return 'snd';
14
+ if (['development', 'develop', 'dev'].includes(v))
15
+ return 'dev';
16
+ if (['testing', 'test'].includes(v))
17
+ return 'test';
18
+ return v;
19
+ }
20
+ function liveSlugs(inventory) {
21
+ const source = Array.isArray(inventory) ? inventory
22
+ : Array.isArray(inventory?.environments) ? inventory.environments
23
+ : Array.isArray(inventory?.data) ? inventory.data : [];
24
+ return source.map((item) => typeof item === 'string' ? item : item?.slug)
25
+ .filter((value) => typeof value === 'string');
26
+ }
27
+ export function reconcileEnvironments(analysisPath, inventoryPath) {
28
+ const analysis = read(analysisPath);
29
+ const detected = [...new Set((analysis.environments ?? []).map((value) => normalize(value)))];
30
+ const live = liveSlugs(read(inventoryPath));
31
+ const groups = new Map();
32
+ for (const slug of live)
33
+ groups.set(normalize(slug), [...(groups.get(normalize(slug)) ?? []), slug]);
34
+ const ambiguous = [...groups.entries()]
35
+ .filter(([, values]) => values.length > 1)
36
+ .map(([normalized, candidates]) => ({ normalized, candidates }));
37
+ const ambiguousNames = new Set(ambiguous.map((item) => item.normalized));
38
+ const matched = detected.filter((env) => groups.has(env) && !ambiguousNames.has(env));
39
+ const missing = detected.filter((env) => !groups.has(env));
40
+ const extra = [...groups.keys()].filter((env) => !detected.includes(env));
41
+ return {
42
+ ready: missing.length === 0 && ambiguous.length === 0,
43
+ detected,
44
+ live,
45
+ matched,
46
+ missing,
47
+ extra,
48
+ ambiguous,
49
+ creation_supported: false,
50
+ creation_blocker: 'The current administrative CLI exposes environment list/fetch only; create requires a platform route.',
51
+ };
52
+ }
53
+ export function runMigrationEnvironmentsReconcile(opts) {
54
+ const result = reconcileEnvironments(opts.analysis, opts.inventory);
55
+ printJson(result, Boolean(opts.json));
56
+ if (opts.strict && !result.ready)
57
+ throw new Error('Environment reconciliation is not ready.');
58
+ }
@@ -0,0 +1,68 @@
1
+ type ContractKind = 'public_api' | 'generated_client' | 'schema' | 'ui_snapshot' | 'internal';
2
+ interface GeneratedManifest {
3
+ version: 1;
4
+ artifact_kind: 'ductape_migration_generated_vendor_evidence';
5
+ source: string;
6
+ analysis: string;
7
+ generated: Array<{
8
+ file: string;
9
+ checked_in_checksum: string;
10
+ contract_kind: ContractKind;
11
+ generator: {
12
+ name: string;
13
+ version: string;
14
+ command: string;
15
+ inputs: Array<{
16
+ file: string;
17
+ checksum: string;
18
+ }>;
19
+ };
20
+ regeneration: {
21
+ clean_workspace: true;
22
+ status: 'passed';
23
+ evidence: string[];
24
+ regenerated_checksum: string;
25
+ matches_checked_in: boolean;
26
+ };
27
+ compatibility_tests: string[];
28
+ owner: string;
29
+ }>;
30
+ vendor: Array<{
31
+ file: string;
32
+ current_checksum: string;
33
+ upstream_source: string;
34
+ upstream_version: string;
35
+ upstream_checksum: string;
36
+ owner: string;
37
+ locally_modified: boolean;
38
+ diff_evidence: string[];
39
+ compatibility_tests: string[];
40
+ }>;
41
+ }
42
+ export declare function initializeMigrationGenerated(opts: {
43
+ analysis: string;
44
+ definition: string;
45
+ output?: string;
46
+ }): {
47
+ path: string;
48
+ manifest: GeneratedManifest;
49
+ };
50
+ export declare function validateMigrationGenerated(file: string): {
51
+ evidence: string;
52
+ ready: boolean;
53
+ generated: number;
54
+ vendor: number;
55
+ blockers: string[];
56
+ };
57
+ export declare function runMigrationGeneratedInit(opts: {
58
+ analysis: string;
59
+ definition: string;
60
+ output?: string;
61
+ json?: boolean;
62
+ }): void;
63
+ export declare function runMigrationGeneratedValidate(opts: {
64
+ file: string;
65
+ strict?: boolean;
66
+ json?: boolean;
67
+ }): void;
68
+ export {};
@@ -0,0 +1,217 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { printJson } from '../lib/output.js';
5
+ import { readMigrationJson, writeMigrationJson } from '../lib/migration-artifact.js';
6
+ function readJson(file) {
7
+ return readMigrationJson(file);
8
+ }
9
+ function checksum(file) {
10
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
11
+ }
12
+ function nonEmpty(values) {
13
+ return Array.isArray(values) && values.length > 0 &&
14
+ values.every((value) => typeof value === 'string' && value.trim().length > 0);
15
+ }
16
+ function sourceFile(source, file) {
17
+ if (!file || path.isAbsolute(file))
18
+ throw new Error(`Repository file must be relative: ${file}`);
19
+ const normalized = path.normalize(file).replaceAll('\\', '/');
20
+ if (normalized === '..' || normalized.startsWith('../')) {
21
+ throw new Error(`Repository file escapes the source root: ${file}`);
22
+ }
23
+ const absolute = path.resolve(source, normalized);
24
+ if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
25
+ throw new Error(`Repository file does not exist: ${normalized}`);
26
+ }
27
+ return { file: normalized, absolute };
28
+ }
29
+ function exactKeys(entries, label) {
30
+ const keys = entries.map((entry) => entry.file);
31
+ if (new Set(keys).size !== keys.length)
32
+ throw new Error(`${label} contains duplicate file classifications.`);
33
+ }
34
+ export function initializeMigrationGenerated(opts) {
35
+ const analysisPath = path.resolve(opts.analysis);
36
+ const analysis = readJson(analysisPath);
37
+ const source = path.resolve(analysis.source);
38
+ const definition = readJson(path.resolve(opts.definition));
39
+ if (!Array.isArray(definition.generated) || !Array.isArray(definition.vendor)) {
40
+ throw new Error('Definition requires generated and vendor arrays.');
41
+ }
42
+ exactKeys(definition.generated, 'generated');
43
+ exactKeys(definition.vendor, 'vendor');
44
+ const hintFiles = new Set((analysis.generated_source_hints ?? []).map((hint) => hint.file));
45
+ const classified = new Set(definition.generated.map((entry) => entry.file));
46
+ const missingHints = [...hintFiles].filter((file) => !classified.has(file));
47
+ if (missingHints.length) {
48
+ throw new Error(`Generated hints require contextual classification: ${missingHints.join(', ')}`);
49
+ }
50
+ const manifest = {
51
+ version: 1,
52
+ artifact_kind: 'ductape_migration_generated_vendor_evidence',
53
+ source,
54
+ analysis: analysisPath,
55
+ generated: definition.generated.map((entry) => {
56
+ const checkedIn = sourceFile(source, entry.file);
57
+ if (!['public_api', 'generated_client', 'schema', 'ui_snapshot', 'internal'].includes(entry.contract_kind)) {
58
+ throw new Error(`${entry.file}: invalid contract_kind.`);
59
+ }
60
+ if (!entry.generator?.name?.trim() || !entry.generator.version?.trim() ||
61
+ !entry.generator.command?.trim() || !nonEmpty(entry.generator.inputs)) {
62
+ throw new Error(`${entry.file}: generator name, version, command, and inputs are required.`);
63
+ }
64
+ if (!entry.owner?.trim())
65
+ throw new Error(`${entry.file}: owner is required.`);
66
+ if (entry.regeneration?.clean_workspace !== true || entry.regeneration.status !== 'passed' ||
67
+ !nonEmpty(entry.regeneration.evidence) || !entry.regeneration.regenerated_output?.trim()) {
68
+ throw new Error(`${entry.file}: passing clean-workspace regeneration evidence is required.`);
69
+ }
70
+ const regenerated = path.resolve(entry.regeneration.regenerated_output);
71
+ if (!fs.existsSync(regenerated) || !fs.statSync(regenerated).isFile()) {
72
+ throw new Error(`${entry.file}: regenerated_output does not exist.`);
73
+ }
74
+ const checkedInChecksum = checksum(checkedIn.absolute);
75
+ const regeneratedChecksum = checksum(regenerated);
76
+ const requiresContract = entry.contract_kind !== 'internal';
77
+ if (requiresContract && !nonEmpty(entry.compatibility_tests)) {
78
+ throw new Error(`${entry.file}: ${entry.contract_kind} requires compatibility tests.`);
79
+ }
80
+ return {
81
+ file: checkedIn.file,
82
+ checked_in_checksum: checkedInChecksum,
83
+ contract_kind: entry.contract_kind,
84
+ generator: {
85
+ name: entry.generator.name,
86
+ version: entry.generator.version,
87
+ command: entry.generator.command,
88
+ inputs: entry.generator.inputs.map((input) => {
89
+ const resolved = sourceFile(source, input);
90
+ return { file: resolved.file, checksum: checksum(resolved.absolute) };
91
+ }),
92
+ },
93
+ regeneration: {
94
+ clean_workspace: true,
95
+ status: 'passed',
96
+ evidence: entry.regeneration.evidence,
97
+ regenerated_checksum: regeneratedChecksum,
98
+ matches_checked_in: regeneratedChecksum === checkedInChecksum,
99
+ },
100
+ compatibility_tests: entry.compatibility_tests ?? [],
101
+ owner: entry.owner,
102
+ };
103
+ }),
104
+ vendor: definition.vendor.map((entry) => {
105
+ const current = sourceFile(source, entry.file);
106
+ if (!entry.upstream_source?.trim() || !entry.upstream_version?.trim() ||
107
+ !/^[a-f0-9]{64}$/.test(entry.upstream_checksum) || !entry.owner?.trim()) {
108
+ throw new Error(`${entry.file}: upstream source, version, SHA-256 checksum, and owner are required.`);
109
+ }
110
+ const currentChecksum = checksum(current.absolute);
111
+ const actuallyModified = currentChecksum !== entry.upstream_checksum;
112
+ if (entry.locally_modified !== actuallyModified) {
113
+ throw new Error(`${entry.file}: locally_modified does not match the upstream checksum comparison.`);
114
+ }
115
+ if (actuallyModified && !nonEmpty(entry.diff_evidence)) {
116
+ throw new Error(`${entry.file}: locally modified vendor source requires diff evidence.`);
117
+ }
118
+ return {
119
+ ...entry,
120
+ file: current.file,
121
+ current_checksum: currentChecksum,
122
+ diff_evidence: entry.diff_evidence ?? [],
123
+ compatibility_tests: entry.compatibility_tests ?? [],
124
+ };
125
+ }),
126
+ };
127
+ const output = path.resolve(opts.output ??
128
+ path.join(path.dirname(analysisPath), 'generated-vendor-evidence.json'));
129
+ if (fs.existsSync(output))
130
+ throw new Error(`Generated/vendor evidence already exists: ${output}`);
131
+ fs.mkdirSync(path.dirname(output), { recursive: true });
132
+ writeMigrationJson(output, manifest, { createOnly: true });
133
+ return { path: output, manifest };
134
+ }
135
+ export function validateMigrationGenerated(file) {
136
+ const resolved = path.resolve(file);
137
+ const manifest = readJson(resolved);
138
+ if (manifest.artifact_kind !== 'ductape_migration_generated_vendor_evidence') {
139
+ throw new Error('Invalid generated/vendor migration evidence.');
140
+ }
141
+ const blockers = [];
142
+ for (const entry of manifest.generated ?? []) {
143
+ try {
144
+ const current = sourceFile(manifest.source, entry.file);
145
+ if (checksum(current.absolute) !== entry.checked_in_checksum) {
146
+ blockers.push(`${entry.file}: checked-in generated output changed`);
147
+ }
148
+ }
149
+ catch {
150
+ blockers.push(`${entry.file}: checked-in generated output is missing`);
151
+ }
152
+ for (const input of entry.generator?.inputs ?? []) {
153
+ try {
154
+ if (checksum(sourceFile(manifest.source, input.file).absolute) !== input.checksum) {
155
+ blockers.push(`${entry.file}: generator input changed: ${input.file}`);
156
+ }
157
+ }
158
+ catch {
159
+ blockers.push(`${entry.file}: generator input is missing: ${input.file}`);
160
+ }
161
+ }
162
+ if (!entry.generator?.version?.trim())
163
+ blockers.push(`${entry.file}: generator version is missing`);
164
+ if (!entry.regeneration?.clean_workspace || entry.regeneration.status !== 'passed' ||
165
+ !entry.regeneration.evidence?.length) {
166
+ blockers.push(`${entry.file}: clean regeneration evidence is incomplete`);
167
+ }
168
+ if (!entry.regeneration?.matches_checked_in ||
169
+ entry.regeneration.regenerated_checksum !== entry.checked_in_checksum) {
170
+ blockers.push(`${entry.file}: regenerated output differs from checked-in output`);
171
+ }
172
+ if (entry.contract_kind !== 'internal' && !entry.compatibility_tests?.length) {
173
+ blockers.push(`${entry.file}: contract compatibility tests are missing`);
174
+ }
175
+ }
176
+ for (const entry of manifest.vendor ?? []) {
177
+ let currentChecksum;
178
+ try {
179
+ const current = sourceFile(manifest.source, entry.file);
180
+ currentChecksum = checksum(current.absolute);
181
+ }
182
+ catch {
183
+ blockers.push(`${entry.file}: vendored source is missing`);
184
+ continue;
185
+ }
186
+ if (currentChecksum !== entry.current_checksum)
187
+ blockers.push(`${entry.file}: vendored source changed after review`);
188
+ const actuallyModified = currentChecksum !== entry.upstream_checksum;
189
+ if (actuallyModified !== entry.locally_modified)
190
+ blockers.push(`${entry.file}: vendor modification classification is stale`);
191
+ if (actuallyModified && (!entry.owner?.trim() || !entry.diff_evidence?.length)) {
192
+ blockers.push(`${entry.file}: local vendor modification lacks owner or diff evidence`);
193
+ }
194
+ }
195
+ return {
196
+ evidence: resolved,
197
+ ready: blockers.length === 0,
198
+ generated: manifest.generated?.length ?? 0,
199
+ vendor: manifest.vendor?.length ?? 0,
200
+ blockers,
201
+ };
202
+ }
203
+ export function runMigrationGeneratedInit(opts) {
204
+ const result = initializeMigrationGenerated(opts);
205
+ printJson({
206
+ path: result.path,
207
+ generated: result.manifest.generated.length,
208
+ vendor: result.manifest.vendor.length,
209
+ }, Boolean(opts.json));
210
+ }
211
+ export function runMigrationGeneratedValidate(opts) {
212
+ const result = validateMigrationGenerated(opts.file);
213
+ printJson(result, Boolean(opts.json));
214
+ if (opts.strict && !result.ready) {
215
+ throw new Error('Generated/vendor evidence is not ready: resolve every reported blocker.');
216
+ }
217
+ }
@@ -0,0 +1,88 @@
1
+ interface PartitionProposal {
2
+ version: 1;
3
+ artifact_kind: 'ductape_migration_partition_proposal';
4
+ authority: 'advisory_only';
5
+ ledger: string;
6
+ limits: {
7
+ max_bytes: number;
8
+ max_tokens: number;
9
+ token_estimate: 'ceil(bytes/4)';
10
+ };
11
+ partitions: Array<{
12
+ tag: string;
13
+ files: string[];
14
+ dependencies: string[];
15
+ byte_estimate: number;
16
+ token_estimate: number;
17
+ oversized_scc: boolean;
18
+ }>;
19
+ }
20
+ interface Portfolio {
21
+ version: 1;
22
+ artifact_kind: 'ductape_migration_portfolio';
23
+ ledger: string;
24
+ partitions: Array<{
25
+ tag: string;
26
+ files: string[];
27
+ context_budget: number;
28
+ summary: string;
29
+ provenance: Array<{
30
+ file: string;
31
+ checksum: string;
32
+ }>;
33
+ }>;
34
+ slices: string[];
35
+ cross_slice_contracts: Array<{
36
+ provider: string;
37
+ consumer: string;
38
+ contract: string;
39
+ evidence: string;
40
+ }>;
41
+ }
42
+ export declare function proposeMigrationPartitions(opts: {
43
+ ledger: string;
44
+ maxBytes?: number;
45
+ maxTokens?: number;
46
+ output?: string;
47
+ }): {
48
+ path: string;
49
+ proposal: PartitionProposal;
50
+ };
51
+ export declare function initializeMigrationPortfolio(opts: {
52
+ ledger: string;
53
+ definition: string;
54
+ output?: string;
55
+ }): {
56
+ path: string;
57
+ portfolio: Portfolio;
58
+ };
59
+ export declare function validateMigrationPortfolio(portfolioPath: string): {
60
+ portfolio: string;
61
+ ready: boolean;
62
+ partitions: number;
63
+ slices: number;
64
+ coverage: {
65
+ reviewed_files: number;
66
+ partitioned_files: number;
67
+ };
68
+ blockers: string[];
69
+ };
70
+ export declare function runMigrationPortfolioInit(opts: {
71
+ ledger: string;
72
+ definition: string;
73
+ output?: string;
74
+ json?: boolean;
75
+ }): void;
76
+ export declare function runMigrationPortfolioValidate(opts: {
77
+ portfolio: string;
78
+ strict?: boolean;
79
+ json?: boolean;
80
+ }): void;
81
+ export declare function runMigrationPortfolioPropose(opts: {
82
+ ledger: string;
83
+ maxBytes?: number;
84
+ maxTokens?: number;
85
+ output?: string;
86
+ json?: boolean;
87
+ }): void;
88
+ export {};