@ductape/cli 0.3.0 → 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.
- package/dist/commands/db-migrate.js +47 -8
- package/dist/commands/migrate-codebase.d.ts +14 -0
- package/dist/commands/migrate-codebase.js +23 -0
- package/dist/commands/migration-artifact.d.ts +11 -15
- package/dist/commands/migration-artifact.js +80 -27
- package/dist/commands/migration-capabilities.d.ts +53 -0
- package/dist/commands/migration-capabilities.js +66 -0
- package/dist/commands/migration-database.d.ts +5 -0
- package/dist/commands/migration-database.js +2 -2
- package/dist/index.js +22 -2
- package/dist/lib/migration-artifact.d.ts +6 -0
- package/dist/lib/migration-artifact.js +13 -2
- package/package.json +1 -1
|
@@ -11,15 +11,41 @@ async function getAppliedTags(proxy, dbContext) {
|
|
|
11
11
|
const result = await proxy.execute('migration.history', [], dbContext);
|
|
12
12
|
const list = Array.isArray(result)
|
|
13
13
|
? result
|
|
14
|
-
:
|
|
14
|
+
: result !== null
|
|
15
|
+
&& typeof result === 'object'
|
|
16
|
+
&& Array.isArray(result.data)
|
|
15
17
|
? result.data
|
|
16
|
-
:
|
|
17
|
-
|
|
18
|
+
: null;
|
|
19
|
+
if (!Array.isArray(list)) {
|
|
20
|
+
throw new Error('migration.history returned an invalid response instead of a history array');
|
|
21
|
+
}
|
|
22
|
+
return list.map((entry, index) => {
|
|
23
|
+
if (entry === null || typeof entry !== 'object' || typeof entry.tag !== 'string') {
|
|
24
|
+
throw new Error(`migration.history returned an invalid entry at index ${index}`);
|
|
25
|
+
}
|
|
26
|
+
return entry.tag;
|
|
27
|
+
});
|
|
18
28
|
}
|
|
19
|
-
catch {
|
|
20
|
-
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Could not read migration history for ${dbContext.product}/${dbContext.database}:${dbContext.env}. ` +
|
|
31
|
+
'Migration state is unknown; no migrations were classified as pending.', { cause: error });
|
|
21
32
|
}
|
|
22
33
|
}
|
|
34
|
+
function getMigrationResult(response, migrationTag) {
|
|
35
|
+
if (response === null || typeof response !== 'object')
|
|
36
|
+
return null;
|
|
37
|
+
const result = response[migrationTag];
|
|
38
|
+
if (result === null || typeof result !== 'object')
|
|
39
|
+
return null;
|
|
40
|
+
const success = result.success;
|
|
41
|
+
if (typeof success !== 'boolean')
|
|
42
|
+
return null;
|
|
43
|
+
const error = result.error;
|
|
44
|
+
return {
|
|
45
|
+
success,
|
|
46
|
+
...(typeof error === 'string' ? { error } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
23
49
|
export async function runDbMigrate(opts) {
|
|
24
50
|
const found = findProjectConfig();
|
|
25
51
|
if (!found)
|
|
@@ -59,13 +85,26 @@ export async function runDbMigrate(opts) {
|
|
|
59
85
|
}
|
|
60
86
|
process.stdout.write(` Applying ${migration.tag}... `);
|
|
61
87
|
try {
|
|
62
|
-
await proxy.execute('migration.run', [[migration]], dbContext);
|
|
88
|
+
const response = await proxy.execute('migration.run', [[migration]], dbContext);
|
|
89
|
+
const migrationResult = getMigrationResult(response, migration.tag);
|
|
90
|
+
if (migrationResult && !migrationResult.success) {
|
|
91
|
+
throw new Error(migrationResult.error ?? 'SDK reported an unsuccessful migration');
|
|
92
|
+
}
|
|
93
|
+
// A successful HTTP response is not proof that the migration was
|
|
94
|
+
// recorded. Verify history before claiming success, including when
|
|
95
|
+
// talking to older proxies that serialize Map results as `{}`.
|
|
96
|
+
const appliedAfterRun = await getAppliedTags(proxy, dbContext);
|
|
97
|
+
if (!appliedAfterRun.includes(migration.tag)) {
|
|
98
|
+
throw new Error('the operation returned without error, but the migration tag is absent from migration history');
|
|
99
|
+
}
|
|
63
100
|
console.log('done');
|
|
64
101
|
totalApplied++;
|
|
65
102
|
}
|
|
66
103
|
catch (err) {
|
|
67
|
-
console.log('
|
|
68
|
-
fail(`Migration "${migration.tag}"
|
|
104
|
+
console.log('unverified');
|
|
105
|
+
fail(`Migration "${migration.tag}" could not be verified: ` +
|
|
106
|
+
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
107
|
+
'Do not rerun it until the live schema and migration history have been inspected.');
|
|
69
108
|
}
|
|
70
109
|
}
|
|
71
110
|
}
|
|
@@ -4,6 +4,18 @@ interface MigrationPlan {
|
|
|
4
4
|
version: 2;
|
|
5
5
|
artifact_kind: 'ai_review_bootstrap';
|
|
6
6
|
authority: 'advisory_only';
|
|
7
|
+
execution_provenance?: {
|
|
8
|
+
mcp_client?: {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
source: 'mcp_initialize_handshake';
|
|
12
|
+
trust: 'protocol_asserted_unverified';
|
|
13
|
+
};
|
|
14
|
+
ai_model: {
|
|
15
|
+
status: 'unavailable';
|
|
16
|
+
reason: 'MCP does not provide a server-verifiable model identity';
|
|
17
|
+
};
|
|
18
|
+
};
|
|
7
19
|
source: string;
|
|
8
20
|
mode: MigrationMode;
|
|
9
21
|
destination: string;
|
|
@@ -172,5 +184,7 @@ export declare function runMigrateCodebase(opts: {
|
|
|
172
184
|
ensureProduct?: boolean;
|
|
173
185
|
write?: boolean;
|
|
174
186
|
json?: boolean;
|
|
187
|
+
mcpClientName?: string;
|
|
188
|
+
mcpClientVersion?: string;
|
|
175
189
|
}): Promise<void>;
|
|
176
190
|
export {};
|
|
@@ -288,17 +288,26 @@ function detectSecretReferences(texts) {
|
|
|
288
288
|
const found = new Map();
|
|
289
289
|
const rules = [
|
|
290
290
|
['github', /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g],
|
|
291
|
+
['github', /\$\{\{\s*secrets\[['"]([A-Za-z_][A-Za-z0-9_]*)['"]\]\s*\}\}/g],
|
|
291
292
|
['gitlab', /\$\{?([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL)[A-Z0-9_]*)\}?/g],
|
|
293
|
+
['gitlab', /\bvault\s*:\s*['"]?([A-Za-z0-9_./@-]+)/g],
|
|
292
294
|
['jenkins', /\bcredentials\(\s*['"]([^'"]+)['"]\s*\)/g],
|
|
295
|
+
['jenkins', /\bcredentialsId\s*:\s*['"]([^'"]+)['"]/g],
|
|
293
296
|
['kubernetes', /\bsecretKeyRef\s*:\s*(?:\r?\n[ \t]+[^\n]*)*?\r?\n[ \t]+key\s*:\s*['"]?([A-Za-z0-9_.-]+)/g],
|
|
294
297
|
['kubernetes', /\bremoteRef\s*:\s*(?:\r?\n[ \t]+[^\n]*)*?\r?\n[ \t]+key\s*:\s*['"]?([A-Za-z0-9_./-]+)/g],
|
|
298
|
+
['kubernetes', /\b(?:secretName|secretProviderClass)\s*:\s*['"]?([A-Za-z0-9_.-]+)/g],
|
|
299
|
+
['kubernetes', /\bsecretRef\s*:\s*(?:\r?\n[ \t]+[^\n]*)*?\r?\n[ \t]+name\s*:\s*['"]?([A-Za-z0-9_.-]+)/g],
|
|
295
300
|
['spring', /\$\{([A-Za-z_][A-Za-z0-9_.-]*)(?::[^}]*)?\}/g],
|
|
296
301
|
['dotnet', /(?:GetConnectionString\(\s*|Configuration\s*\[\s*)['"]([^'"]+)['"]/g],
|
|
297
302
|
['dotnet', /<UserSecretsId>\s*([^<\r\n]+)\s*<\/UserSecretsId>/g],
|
|
298
303
|
['terraform', /\bvar\.([A-Za-z_][A-Za-z0-9_]*)/g],
|
|
299
304
|
['aws', /(?:secretsmanager|secret-id|secretId)[/:="'\s]+([A-Za-z0-9_./-]+)/gi],
|
|
305
|
+
['aws', /\{\{resolve:(?:secretsmanager|ssm-secure):([^}:]+)/gi],
|
|
306
|
+
['aws', /arn:aws:secretsmanager:[^:\s]+:[^:\s]+:secret:([A-Za-z0-9_./+=@-]+)/gi],
|
|
300
307
|
['gcp', /(?:secretmanager|secretVersion|secret-version)[/:="'\s]+([A-Za-z0-9_./-]+)/gi],
|
|
308
|
+
['gcp', /projects\/[^/\s]+\/secrets\/([A-Za-z0-9_-]+)/gi],
|
|
301
309
|
['azure', /(?:vault\.azure\.net\/secrets\/|secretName\s*[:=]\s*['"])([A-Za-z0-9_.-]+)/gi],
|
|
310
|
+
['azure', /@Microsoft\.KeyVault\(\s*SecretUri=https:\/\/[^/\s]+\.vault\.azure\.net\/secrets\/([A-Za-z0-9_.-]+)/gi],
|
|
302
311
|
['docker', /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::?-[^}]*)?\}/g],
|
|
303
312
|
['circleci', /\$\{?([A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL)[A-Z0-9_]*)\}?/g],
|
|
304
313
|
['azure_devops', /\$\(([A-Za-z_][A-Za-z0-9_.-]*(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL)[A-Za-z0-9_.-]*)\)/gi],
|
|
@@ -727,6 +736,20 @@ export async function runMigrateCodebase(opts) {
|
|
|
727
736
|
});
|
|
728
737
|
const productTag = plan.product.tag;
|
|
729
738
|
const productName = plan.product.name;
|
|
739
|
+
if (opts.mcpClientName && opts.mcpClientVersion) {
|
|
740
|
+
plan.execution_provenance = {
|
|
741
|
+
mcp_client: {
|
|
742
|
+
name: opts.mcpClientName,
|
|
743
|
+
version: opts.mcpClientVersion,
|
|
744
|
+
source: 'mcp_initialize_handshake',
|
|
745
|
+
trust: 'protocol_asserted_unverified',
|
|
746
|
+
},
|
|
747
|
+
ai_model: {
|
|
748
|
+
status: 'unavailable',
|
|
749
|
+
reason: 'MCP does not provide a server-verifiable model identity',
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
}
|
|
730
753
|
if (opts.ensureProduct) {
|
|
731
754
|
const context = requireWorkspaceContext({});
|
|
732
755
|
try {
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
+
type JsonSchema = {
|
|
2
|
+
$schema: string;
|
|
3
|
+
title: string;
|
|
4
|
+
type: 'object';
|
|
5
|
+
required: string[];
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
additionalProperties: boolean;
|
|
8
|
+
};
|
|
1
9
|
export declare const MIGRATION_ARTIFACT_SCHEMAS: {
|
|
2
10
|
version: number;
|
|
3
|
-
artifacts:
|
|
4
|
-
|
|
5
|
-
title: string;
|
|
6
|
-
type: string;
|
|
7
|
-
required: string[];
|
|
8
|
-
properties: {
|
|
9
|
-
version: {
|
|
10
|
-
const: number;
|
|
11
|
-
};
|
|
12
|
-
artifact_kind: {
|
|
13
|
-
const: string;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
additionalProperties: boolean;
|
|
17
|
-
}[];
|
|
11
|
+
artifacts: JsonSchema[];
|
|
12
|
+
definitions: JsonSchema[];
|
|
18
13
|
};
|
|
19
14
|
export declare function runMigrationArtifactValidate(opts: {
|
|
20
15
|
file: string;
|
|
@@ -30,3 +25,4 @@ export declare function runMigrationArtifactMigrate(opts: {
|
|
|
30
25
|
json?: boolean;
|
|
31
26
|
}): void;
|
|
32
27
|
export declare function runMigrationArtifactSchemas(json?: boolean): void;
|
|
28
|
+
export {};
|
|
@@ -1,39 +1,92 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { printJson } from '../lib/output.js';
|
|
3
3
|
import { migrateMigrationJson, readMigrationJson, recoverMigrationJson, } from '../lib/migration-artifact.js';
|
|
4
|
+
const objectSchema = (title, required, properties = {}) => ({
|
|
5
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
6
|
+
title,
|
|
7
|
+
type: 'object',
|
|
8
|
+
required,
|
|
9
|
+
properties,
|
|
10
|
+
additionalProperties: true,
|
|
11
|
+
});
|
|
12
|
+
const artifact = (kind, required) => objectSchema(kind, ['version', 'artifact_kind', ...required], { version: { const: 1 }, artifact_kind: { const: kind } });
|
|
4
13
|
export const MIGRATION_ARTIFACT_SCHEMAS = {
|
|
5
|
-
version:
|
|
14
|
+
version: 2,
|
|
6
15
|
artifacts: [
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
artifact('ai_review_bootstrap', ['authority', 'source', 'mode', 'destination', 'review_queue']),
|
|
17
|
+
artifact('ai_migration_review_ledger', ['source', 'entries']),
|
|
18
|
+
artifact('ductape_migration_slice', ['tag', 'name', 'ledger', 'files', 'surfaces', 'product_boundary']),
|
|
19
|
+
artifact('ductape_migration_portfolio', ['ledger', 'partitions', 'slices', 'cross_slice_contracts']),
|
|
20
|
+
artifact('ductape_migration_partition_proposal', ['authority', 'ledger', 'limits', 'partitions']),
|
|
21
|
+
artifact('ductape_migration_e2e_baseline', ['source', 'created_at', 'command', 'suite', 'baseline', 'final']),
|
|
22
|
+
artifact('ductape_migration_generated_vendor_evidence', ['analysis', 'generated', 'vendor']),
|
|
23
|
+
artifact('ductape_migration_assurance', [
|
|
24
|
+
'session_flows', 'feature_resilience', 'third_party_calls', 'components',
|
|
25
|
+
'provider_migrations', 'runtime', 'sdk_matrix',
|
|
26
|
+
]),
|
|
27
|
+
artifact('ductape_migration_verification_matrix', ['categories']),
|
|
28
|
+
artifact('ductape_database_baseline', [
|
|
29
|
+
'migration_id', 'database_tag', 'owners', 'providers', 'schema_snapshots',
|
|
30
|
+
'structural_comparison',
|
|
31
|
+
]),
|
|
32
|
+
artifact('ductape_database_data', [
|
|
33
|
+
'migration_id', 'transformations', 'batching', 'checkpoints', 'idempotency',
|
|
34
|
+
'resume_strategy', 'validation', 'reconciliation',
|
|
35
|
+
]),
|
|
36
|
+
artifact('ductape_database_cutover', [
|
|
37
|
+
'migration_id', 'phases', 'expand', 'backfill', 'dual_compatibility', 'contract',
|
|
38
|
+
'deployment_order', 'migration_locking', 'backup', 'restore_test', 'rollback',
|
|
39
|
+
]),
|
|
40
|
+
artifact('ductape_review_ledger_signature', [
|
|
41
|
+
'ledger', 'ledger_sha256', 'algorithm', 'key_id', 'signature', 'signed_at',
|
|
42
|
+
]),
|
|
43
|
+
artifact('ductape_service_product_map', ['analysis', 'mappings', 'assets', 'promotions']),
|
|
44
|
+
artifact('ductape_secret_migration_map', ['analysis', 'classifications']),
|
|
45
|
+
artifact('ductape_sdk_capability_matrix', ['authority', 'unsupported_policy', 'sdks']),
|
|
46
|
+
],
|
|
47
|
+
definitions: [
|
|
48
|
+
objectSchema('migration_e2e_definition', ['command', 'suite_files', 'baseline']),
|
|
49
|
+
objectSchema('migration_e2e_final_evidence', ['command', 'status', 'evidence', 'environment', 'suite_root']),
|
|
50
|
+
objectSchema('migration_slice_definition', [
|
|
51
|
+
'surfaces', 'product_boundary', 'sdk_capabilities', 'interface_contracts',
|
|
52
|
+
'functional_requirements', 'operational_requirements', 'parity_evidence',
|
|
53
|
+
'frontend_parity', 'required_assets', 'tests', 'failure_tests', 'runtime_evidence',
|
|
54
|
+
'smoke_checks', 'cutover_conditions', 'rollback_conditions', 'deployment_cutover',
|
|
55
|
+
]),
|
|
56
|
+
objectSchema('migration_portfolio_definition', ['partitions', 'slices', 'cross_slice_contracts']),
|
|
57
|
+
objectSchema('migration_database_definition', ['baseline', 'data', 'cutover']),
|
|
58
|
+
objectSchema('migration_product_map_definition', ['mappings', 'assets', 'promotions']),
|
|
59
|
+
objectSchema('migration_secret_map_definition', ['classifications']),
|
|
60
|
+
objectSchema('migration_generated_vendor_definition', ['generated', 'vendor']),
|
|
61
|
+
objectSchema('migration_review_record_definition', [
|
|
62
|
+
'purpose', 'dependencies', 'interfaces', 'functional_parity_requirements',
|
|
63
|
+
'operational_parity_requirements', 'findings',
|
|
64
|
+
]),
|
|
65
|
+
objectSchema('migration_assurance_definition', [
|
|
66
|
+
'version', 'artifact_kind', 'session_flows', 'feature_resilience', 'third_party_calls',
|
|
67
|
+
'components', 'provider_migrations', 'runtime', 'sdk_matrix',
|
|
68
|
+
]),
|
|
69
|
+
objectSchema('migration_verification_definition', ['version', 'artifact_kind', 'categories']),
|
|
70
|
+
objectSchema('migration_environment_inventory', ['environments']),
|
|
71
|
+
],
|
|
30
72
|
};
|
|
31
73
|
export function runMigrationArtifactValidate(opts) {
|
|
32
74
|
const value = readMigrationJson(opts.file);
|
|
33
|
-
const schema = MIGRATION_ARTIFACT_SCHEMAS.artifacts.find((candidate) => candidate.properties.artifact_kind
|
|
34
|
-
if (!schema || value.version !== 1)
|
|
75
|
+
const schema = MIGRATION_ARTIFACT_SCHEMAS.artifacts.find((candidate) => candidate.properties.artifact_kind?.const === value.artifact_kind);
|
|
76
|
+
if (!schema || value.version !== 1) {
|
|
35
77
|
throw new Error('[ARTIFACT_VERSION_UNSUPPORTED] Unknown artifact kind or version.');
|
|
36
|
-
|
|
78
|
+
}
|
|
79
|
+
const missing = schema.required.filter((field) => !(field in value) || value[field] == null);
|
|
80
|
+
if (missing.length) {
|
|
81
|
+
throw new Error(`[ARTIFACT_SCHEMA_INVALID] Missing required fields: ${missing.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
printJson({
|
|
84
|
+
file: path.resolve(opts.file),
|
|
85
|
+
valid: true,
|
|
86
|
+
artifact_kind: value.artifact_kind,
|
|
87
|
+
version: value.version,
|
|
88
|
+
schema_catalog_version: MIGRATION_ARTIFACT_SCHEMAS.version,
|
|
89
|
+
}, Boolean(opts.json));
|
|
37
90
|
}
|
|
38
91
|
export function runMigrationArtifactRecover(opts) {
|
|
39
92
|
recoverMigrationJson(opts.file);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type SupportedLanguage = 'typescript' | 'go' | 'java' | 'dotnet';
|
|
2
|
+
export declare const SDK_CAPABILITY_MATRIX: {
|
|
3
|
+
readonly version: 1;
|
|
4
|
+
readonly artifact_kind: "ductape_sdk_capability_matrix";
|
|
5
|
+
readonly authority: "source_verified_catalog";
|
|
6
|
+
readonly generated_from: "repository source; update and test this catalog with every supported SDK release";
|
|
7
|
+
readonly unsupported_policy: {
|
|
8
|
+
readonly rule: "Never infer cross-language parity or synthesize a missing API.";
|
|
9
|
+
readonly action: "Inspect the installed package exports, record the capability as unsupported, and stop for an approved fallback.";
|
|
10
|
+
};
|
|
11
|
+
readonly sdks: readonly [{
|
|
12
|
+
readonly language: "typescript";
|
|
13
|
+
readonly package: "@ductape/sdk";
|
|
14
|
+
readonly version: "0.1.101";
|
|
15
|
+
readonly status: "supported";
|
|
16
|
+
readonly source_version: "sdk/ts/package.json";
|
|
17
|
+
readonly public_surface_evidence: "sdk/ts/src/index.ts";
|
|
18
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
19
|
+
readonly lifecycle: readonly ["await asynchronous operations", "close/disconnect owned clients during application shutdown"];
|
|
20
|
+
}, {
|
|
21
|
+
readonly language: "go";
|
|
22
|
+
readonly package: "github.com/ductape/ductape/sdk/go";
|
|
23
|
+
readonly version: "v0.0.1";
|
|
24
|
+
readonly status: "supported";
|
|
25
|
+
readonly source_version: "sdk/go git tag v0.0.1";
|
|
26
|
+
readonly public_surface_evidence: "sdk/go/ductape";
|
|
27
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
28
|
+
readonly lifecycle: readonly ["propagate context.Context cancellation", "close owned clients during application shutdown"];
|
|
29
|
+
}, {
|
|
30
|
+
readonly language: "java";
|
|
31
|
+
readonly package: "app.ductape:ductape-sdk";
|
|
32
|
+
readonly version: "0.1.9-SNAPSHOT";
|
|
33
|
+
readonly status: "supported-source-snapshot";
|
|
34
|
+
readonly source_version: "sdk/java/build.gradle";
|
|
35
|
+
readonly public_surface_evidence: "sdk/java/src/main/java/app/ductape/sdk/Ductape.java";
|
|
36
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
37
|
+
readonly lifecycle: readonly ["propagate interruption/cancellation", "close owned clients during application shutdown"];
|
|
38
|
+
}, {
|
|
39
|
+
readonly language: "dotnet";
|
|
40
|
+
readonly package: "Ductape.Sdk";
|
|
41
|
+
readonly version: "0.1.12";
|
|
42
|
+
readonly status: "supported";
|
|
43
|
+
readonly source_version: "sdk/dotnet/src/Ductape.Sdk/Ductape.Sdk.csproj";
|
|
44
|
+
readonly public_surface_evidence: "sdk/dotnet/src/Ductape.Sdk/Ductape.cs";
|
|
45
|
+
readonly capabilities: readonly ["actions", "agents", "cache", "cloud", "databases", "events", "features", "graphs", "imports", "jobs", "logs", "models", "notifications", "products", "resilience", "secrets", "sessions", "storage", "vectors", "warehouse"];
|
|
46
|
+
readonly lifecycle: readonly ["propagate CancellationToken", "dispose owned clients during application shutdown"];
|
|
47
|
+
}];
|
|
48
|
+
};
|
|
49
|
+
export declare function runMigrationCapabilities(opts: {
|
|
50
|
+
language?: SupportedLanguage;
|
|
51
|
+
version?: string;
|
|
52
|
+
json?: boolean;
|
|
53
|
+
}): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { printJson } from '../lib/output.js';
|
|
2
|
+
const COMMON_RUNTIME_CAPABILITIES = [
|
|
3
|
+
'actions', 'agents', 'cache', 'cloud', 'databases', 'events', 'features',
|
|
4
|
+
'graphs', 'imports', 'jobs', 'logs', 'models', 'notifications', 'products',
|
|
5
|
+
'resilience', 'secrets', 'sessions', 'storage', 'vectors', 'warehouse',
|
|
6
|
+
];
|
|
7
|
+
export const SDK_CAPABILITY_MATRIX = {
|
|
8
|
+
version: 1,
|
|
9
|
+
artifact_kind: 'ductape_sdk_capability_matrix',
|
|
10
|
+
authority: 'source_verified_catalog',
|
|
11
|
+
generated_from: 'repository source; update and test this catalog with every supported SDK release',
|
|
12
|
+
unsupported_policy: {
|
|
13
|
+
rule: 'Never infer cross-language parity or synthesize a missing API.',
|
|
14
|
+
action: 'Inspect the installed package exports, record the capability as unsupported, and stop for an approved fallback.',
|
|
15
|
+
},
|
|
16
|
+
sdks: [
|
|
17
|
+
{
|
|
18
|
+
language: 'typescript',
|
|
19
|
+
package: '@ductape/sdk',
|
|
20
|
+
version: '0.1.101',
|
|
21
|
+
status: 'supported',
|
|
22
|
+
source_version: 'sdk/ts/package.json',
|
|
23
|
+
public_surface_evidence: 'sdk/ts/src/index.ts',
|
|
24
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
25
|
+
lifecycle: ['await asynchronous operations', 'close/disconnect owned clients during application shutdown'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
language: 'go',
|
|
29
|
+
package: 'github.com/ductape/ductape/sdk/go',
|
|
30
|
+
version: 'v0.0.1',
|
|
31
|
+
status: 'supported',
|
|
32
|
+
source_version: 'sdk/go git tag v0.0.1',
|
|
33
|
+
public_surface_evidence: 'sdk/go/ductape',
|
|
34
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
35
|
+
lifecycle: ['propagate context.Context cancellation', 'close owned clients during application shutdown'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
language: 'java',
|
|
39
|
+
package: 'app.ductape:ductape-sdk',
|
|
40
|
+
version: '0.1.9-SNAPSHOT',
|
|
41
|
+
status: 'supported-source-snapshot',
|
|
42
|
+
source_version: 'sdk/java/build.gradle',
|
|
43
|
+
public_surface_evidence: 'sdk/java/src/main/java/app/ductape/sdk/Ductape.java',
|
|
44
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
45
|
+
lifecycle: ['propagate interruption/cancellation', 'close owned clients during application shutdown'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
language: 'dotnet',
|
|
49
|
+
package: 'Ductape.Sdk',
|
|
50
|
+
version: '0.1.12',
|
|
51
|
+
status: 'supported',
|
|
52
|
+
source_version: 'sdk/dotnet/src/Ductape.Sdk/Ductape.Sdk.csproj',
|
|
53
|
+
public_surface_evidence: 'sdk/dotnet/src/Ductape.Sdk/Ductape.cs',
|
|
54
|
+
capabilities: [...COMMON_RUNTIME_CAPABILITIES],
|
|
55
|
+
lifecycle: ['propagate CancellationToken', 'dispose owned clients during application shutdown'],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
export function runMigrationCapabilities(opts) {
|
|
60
|
+
const sdks = SDK_CAPABILITY_MATRIX.sdks.filter((sdk) => (!opts.language || sdk.language === opts.language) &&
|
|
61
|
+
(!opts.version || sdk.version === opts.version));
|
|
62
|
+
if (!sdks.length) {
|
|
63
|
+
throw new Error(`No supported SDK capability matrix matches ${opts.language ?? '*'}@${opts.version ?? '*'}.`);
|
|
64
|
+
}
|
|
65
|
+
printJson({ ...SDK_CAPABILITY_MATRIX, sdks }, Boolean(opts.json));
|
|
66
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare const REQUIRED_DATABASE_FIELDS: {
|
|
2
|
+
readonly baseline: readonly ["database_tag", "owners", "providers", "code_schema_evidence", "migration_history_evidence", "applied_history_evidence", "live_snd_schema_evidence", "proposed_schema_evidence", "drift", "objects", "security", "topology", "compatibility", "performance_baseline"];
|
|
3
|
+
readonly data: readonly ["transformations", "batching", "checkpoints", "idempotency", "resume_strategy", "validation", "reconciliation", "failure_recovery", "pii_controls", "retention", "seed_data"];
|
|
4
|
+
readonly cutover: readonly ["phases", "expand", "backfill", "dual_compatibility", "contract", "deployment_order", "migration_locking", "backup", "restore_test", "rollback", "irreversible_changes", "monitoring", "reconciliation", "approval"];
|
|
5
|
+
};
|
|
1
6
|
export declare function initDatabaseMigration(opts: {
|
|
2
7
|
definition: string;
|
|
3
8
|
output: string;
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { printJson } from '../lib/output.js';
|
|
5
5
|
import { readMigrationJson, writeMigrationJson } from '../lib/migration-artifact.js';
|
|
6
|
-
const
|
|
6
|
+
export const REQUIRED_DATABASE_FIELDS = {
|
|
7
7
|
baseline: ['database_tag', 'owners', 'providers', 'code_schema_evidence', 'migration_history_evidence', 'applied_history_evidence', 'live_snd_schema_evidence', 'proposed_schema_evidence', 'drift', 'objects', 'security', 'topology', 'compatibility', 'performance_baseline'],
|
|
8
8
|
data: ['transformations', 'batching', 'checkpoints', 'idempotency', 'resume_strategy', 'validation', 'reconciliation', 'failure_recovery', 'pii_controls', 'retention', 'seed_data'],
|
|
9
9
|
cutover: ['phases', 'expand', 'backfill', 'dual_compatibility', 'contract', 'deployment_order', 'migration_locking', 'backup', 'restore_test', 'rollback', 'irreversible_changes', 'monitoring', 'reconciliation', 'approval'],
|
|
@@ -47,7 +47,7 @@ export function validateDatabaseMigration(directory) {
|
|
|
47
47
|
if (artifact.version !== 1 || artifact.artifact_kind !== `ductape_database_${name}`) {
|
|
48
48
|
blockers.push(`${name}: unsupported artifact version or kind`);
|
|
49
49
|
}
|
|
50
|
-
for (const field of missing(artifact,
|
|
50
|
+
for (const field of missing(artifact, REQUIRED_DATABASE_FIELDS[name]))
|
|
51
51
|
blockers.push(`${name}.${field}: evidence required`);
|
|
52
52
|
if (!text(artifact.migration_id))
|
|
53
53
|
blockers.push(`${name}.migration_id: required for cross-artifact consistency`);
|
package/dist/index.js
CHANGED
|
@@ -39,13 +39,18 @@ import { runMigrationGeneratedInit, runMigrationGeneratedValidate, } from './com
|
|
|
39
39
|
import { runMigrationAssuranceValidate } from './commands/migration-assurance.js';
|
|
40
40
|
import { runMigrationVerificationValidate } from './commands/migration-verification.js';
|
|
41
41
|
import { runMigrationArtifactMigrate, runMigrationArtifactRecover, runMigrationArtifactSchemas, runMigrationArtifactValidate, } from './commands/migration-artifact.js';
|
|
42
|
+
import { runMigrationCapabilities } from './commands/migration-capabilities.js';
|
|
43
|
+
import { structuredMigrationError } from './lib/migration-artifact.js';
|
|
42
44
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
45
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
44
46
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
47
|
function wrap(fn) {
|
|
46
48
|
return (...args) => {
|
|
47
|
-
Promise.resolve(fn(...args)).catch((err) => {
|
|
48
|
-
|
|
49
|
+
Promise.resolve().then(() => fn(...args)).catch((err) => {
|
|
50
|
+
const command = process.argv[2] ?? '';
|
|
51
|
+
console.error(command === 'migrate-codebase' || command.startsWith('migration-')
|
|
52
|
+
? structuredMigrationError(err)
|
|
53
|
+
: err instanceof Error ? err.message : err);
|
|
49
54
|
process.exit(1);
|
|
50
55
|
});
|
|
51
56
|
};
|
|
@@ -171,6 +176,8 @@ program
|
|
|
171
176
|
.option('--exclude <patterns>', 'Comma-separated repository-relative glob patterns')
|
|
172
177
|
.option('--ensure-product', 'Create the product when it does not exist')
|
|
173
178
|
.option('--write', 'Write redacted advisory artifacts only; never writes application code or executable assets')
|
|
179
|
+
.option('--mcp-client-name <name>', 'Protocol-asserted MCP client name (unverified)', undefined)
|
|
180
|
+
.option('--mcp-client-version <version>', 'Protocol-asserted MCP client version (unverified)', undefined)
|
|
174
181
|
.option('--json', 'JSON output')
|
|
175
182
|
.action(wrap((opts) => runMigrateCodebase({
|
|
176
183
|
source: opts.source,
|
|
@@ -186,6 +193,8 @@ program
|
|
|
186
193
|
ensureProduct: Boolean(opts.ensureProduct),
|
|
187
194
|
write: Boolean(opts.write),
|
|
188
195
|
json: Boolean(opts.json),
|
|
196
|
+
mcpClientName: opts.mcpClientName,
|
|
197
|
+
mcpClientVersion: opts.mcpClientVersion,
|
|
189
198
|
})));
|
|
190
199
|
const migrationE2E = program
|
|
191
200
|
.command('migration-e2e')
|
|
@@ -240,6 +249,17 @@ program
|
|
|
240
249
|
strict: Boolean(opts.strict),
|
|
241
250
|
json: Boolean(opts.json),
|
|
242
251
|
})));
|
|
252
|
+
program
|
|
253
|
+
.command('migration-capabilities')
|
|
254
|
+
.description('Print the source-verified capability matrix for supported SDK language/version pairs')
|
|
255
|
+
.option('--language <language>', 'typescript | go | java | dotnet')
|
|
256
|
+
.option('--version <version>', 'Exact supported SDK version')
|
|
257
|
+
.option('--json', 'JSON output')
|
|
258
|
+
.action(wrap((opts) => runMigrationCapabilities({
|
|
259
|
+
language: opts.language,
|
|
260
|
+
version: opts.version,
|
|
261
|
+
json: Boolean(opts.json),
|
|
262
|
+
})));
|
|
243
263
|
program
|
|
244
264
|
.command('migration-verification')
|
|
245
265
|
.description('Validate cited automation evidence for large-repo, parity, frontend, database, and asset migration')
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type MigrationArtifactErrorCode = 'ARTIFACT_TOO_LARGE' | 'ARTIFACT_INVALID_JSON' | 'ARTIFACT_SECRET_MATERIAL' | 'ARTIFACT_LOCKED' | 'ARTIFACT_EXISTS' | 'ARTIFACT_VERSION_UNSUPPORTED' | 'ARTIFACT_RECOVERY_FAILED';
|
|
2
|
+
export declare const MIGRATION_ERROR_CODES: {
|
|
3
|
+
readonly validation: "MIGRATION_VALIDATION_FAILED";
|
|
4
|
+
readonly configuration: "MIGRATION_CONFIGURATION_INVALID";
|
|
5
|
+
readonly external: "MIGRATION_EXTERNAL_OPERATION_FAILED";
|
|
6
|
+
};
|
|
7
|
+
export declare function structuredMigrationError(error: unknown): string;
|
|
2
8
|
export declare class MigrationArtifactError extends Error {
|
|
3
9
|
readonly code: MigrationArtifactErrorCode;
|
|
4
10
|
constructor(code: MigrationArtifactErrorCode, message: string);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
export const MIGRATION_ERROR_CODES = {
|
|
4
|
+
validation: 'MIGRATION_VALIDATION_FAILED',
|
|
5
|
+
configuration: 'MIGRATION_CONFIGURATION_INVALID',
|
|
6
|
+
external: 'MIGRATION_EXTERNAL_OPERATION_FAILED',
|
|
7
|
+
};
|
|
8
|
+
export function structuredMigrationError(error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
if (/^\[[A-Z0-9_]+\]/.test(message))
|
|
11
|
+
return message;
|
|
12
|
+
return `[${MIGRATION_ERROR_CODES.validation}] ${message}`;
|
|
13
|
+
}
|
|
3
14
|
export class MigrationArtifactError extends Error {
|
|
4
15
|
code;
|
|
5
16
|
constructor(code, message) {
|
|
@@ -37,8 +48,8 @@ export function readMigrationJson(file, maxBytes = 5_000_000) {
|
|
|
37
48
|
try {
|
|
38
49
|
value = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
39
50
|
}
|
|
40
|
-
catch
|
|
41
|
-
throw new MigrationArtifactError('ARTIFACT_INVALID_JSON', `${resolved}:
|
|
51
|
+
catch {
|
|
52
|
+
throw new MigrationArtifactError('ARTIFACT_INVALID_JSON', `${resolved}: JSON parsing failed; parser context is intentionally suppressed to prevent evidence leakage.`);
|
|
42
53
|
}
|
|
43
54
|
inspect(value);
|
|
44
55
|
return value;
|
package/package.json
CHANGED