verify-canister-migration.mjs
191 lines 6.3 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 #!/usr/bin/env node
2 /**
3 * Static checks that hub/icp stable-memory migration contracts are still present.
4 * Fails fast if Migration.mo or main.mo drift in ways that risk an incompatible upgrade.
5 *
6 * Run: node scripts/verify-canister-migration.mjs
7 * (Also invoked from scripts/canister-predeploy.sh)
8 */
9 import fs from 'fs';
10 import path from 'path';
11 import { fileURLToPath } from 'url';
12
13 const __dirname = path.dirname(fileURLToPath(import.meta.url));
14 const REPO_ROOT = path.join(__dirname, '..');
15 const MIGRATION = path.join(REPO_ROOT, 'hub/icp/src/hub/Migration.mo');
16 const MAIN = path.join(REPO_ROOT, 'hub/icp/src/hub/main.mo');
17 const JSON_VALIDATE = path.join(REPO_ROOT, 'hub/icp/src/hub/JsonValidate.mo');
18
19 function readUtf8(p) {
20 return fs.readFileSync(p, 'utf8');
21 }
22
23 const migrationChecks = [
24 {
25 name: 'StableStorageV0: vaultEntries is (userId, pathMap) — pre–Phase-15.1',
26 ok: (s) => s.includes('vaultEntries : [(Text, [(Text, (Text, Text))])];'),
27 },
28 {
29 name: 'StableStorage V1: vaultEntries is (userId, vaultId, pathMap)',
30 ok: (s) => s.includes('vaultEntries : [(Text, Text, [(Text, (Text, Text))])];'),
31 },
32 {
33 name: 'StableStorage V1: billingByUser reserved (hosted billing roadmap)',
34 ok: (s) => s.includes('billingByUser : [(Text, BillingRecord)];'),
35 },
36 {
37 name: 'ProposalRecordV1 includes vault_id field (pre–evaluation)',
38 ok: (s) => s.includes('external_ref : Text;\n vault_id : Text;\n created_at : Text;'),
39 },
40 {
41 name: 'ProposalRecord (V2) includes evaluation_status',
42 ok: (s) => s.includes('evaluation_status : Text;') && s.includes('evaluation_waiver_json : Text;'),
43 },
44 {
45 name: 'migrateFromV0ToV1(old : { var storage : StableStorageV0 }) — historical V0→V1',
46 ok: (s) => s.includes('migrateFromV0ToV1(old : { var storage : StableStorageV0 })'),
47 },
48 {
49 name: 'migration(old : { var storage : StableStorage }) — identity after SEC-KN-4c / T4',
50 ok: (s) => s.includes('migration(old : { var storage : StableStorage })'),
51 },
52 {
53 name: 'StableStorageV5 — pre-V6 on-chain layout',
54 ok: (s) => s.includes('public type StableStorageV5'),
55 },
56 {
57 name: 'StableStorageV6 — pre-V7 on-chain layout (has operator_export_secret)',
58 ok: (s) => s.includes('public type StableStorageV6'),
59 },
60 {
61 name: 'StableStorageV7 — pre-V8 on-chain layout (has gateway_auth_secret)',
62 ok: (s) => s.includes('public type StableStorageV7'),
63 },
64 {
65 name: 'StableStorage (V8) includes operator_export_secret, gateway_auth_secret, cors_allowed_origin',
66 ok: (s) =>
67 s.includes('operator_export_secret : Text') &&
68 s.includes('gateway_auth_secret : Text') &&
69 s.includes('cors_allowed_origin : Text'),
70 },
71 {
72 name: 'StableStorageV4 type (pre-V5 proposals)',
73 ok: (s) => s.includes('public type StableStorageV4') && s.includes('[ProposalRecordV4]'),
74 },
75 {
76 name: 'ProposalRecord includes enrich + suggested frontmatter JSON (V5)',
77 ok: (s) =>
78 s.includes('assistant_notes : Text;') &&
79 s.includes('suggested_labels_json : Text;') &&
80 s.includes('assistant_suggested_frontmatter_json : Text;'),
81 },
82 {
83 name: 'ProposalRecord includes created_by (SEC-KN-4)',
84 ok: (s) => s.includes('created_by : Text;'),
85 },
86 {
87 name: 'StableStorageV5/V6/V7 pin proposal rows to ProposalRecordV7',
88 ok: (s) =>
89 s.includes('proposalEntries : [(Text, [ProposalRecordV7])];') &&
90 s.includes('public type StableStorageV5') &&
91 s.includes('public type StableStorageV6') &&
92 s.includes('public type StableStorageV7'),
93 },
94 {
95 name: 'Historical row maps return ProposalRecordV7',
96 ok: (s) =>
97 s.includes('func _proposalBeforeEnrichToCurrent(p : ProposalRecordBeforeEnrich) : ProposalRecordV7') &&
98 s.includes('func _proposalV4ToV5(p : ProposalRecordV4) : ProposalRecordV7'),
99 },
100 {
101 name: 'SEC-KN-4c identity hook; historical _proposalV7ToCurrent retained',
102 ok: (s) =>
103 s.includes('func _proposalV7ToCurrent(p : ProposalRecordV7) : ProposalRecord') &&
104 s.includes('migration(old : { var storage : StableStorage })') &&
105 s.includes('SEC-KN-4c') &&
106 !s.includes('TODO(SEC-KN-4c)'),
107 },
108 {
109 name: 'V0 → V1 maps notes into vault "default"',
110 ok: (s) => s.includes('(entry.0, "default", entry.1)'),
111 },
112 {
113 name: 'V0 proposals gain vault_id "default"',
114 ok: (s) => s.includes('vault_id = "default"') && s.includes('v0ToProposalV1'),
115 },
116 ];
117
118 const mainChecks = [
119 {
120 name: 'Actor uses Migration.migration hook',
121 ok: (s) => s.includes('(with migration = Migration.migration)'),
122 },
123 {
124 name: 'persistent actor Hub',
125 ok: (s) => s.includes('persistent actor Hub'),
126 },
127 {
128 name: 'Imports Migration module',
129 ok: (s) => s.includes('import Migration "Migration"'),
130 },
131 {
132 name: 'Imports JsonValidate + normalizes enrich fragments on GET proposal',
133 ok: (s) =>
134 s.includes('import JsonValidate "JsonValidate"') &&
135 s.includes('JsonValidate.normalizeJsonArrayFragment') &&
136 s.includes('JsonValidate.prepareEnrichJsonArray'),
137 },
138 {
139 name: 'Stable storage type matches Migration.StableStorage',
140 ok: (s) => s.includes('type StableStorage = Migration.StableStorage'),
141 },
142 ];
143
144 let failed = 0;
145
146 for (const { name, ok } of migrationChecks) {
147 const text = readUtf8(MIGRATION);
148 if (!ok(text)) {
149 console.error(`FAIL: ${name}\n file: ${MIGRATION}`);
150 failed++;
151 }
152 }
153
154 for (const { name, ok } of mainChecks) {
155 const text = readUtf8(MAIN);
156 if (!ok(text)) {
157 console.error(`FAIL: ${name}\n file: ${MAIN}`);
158 failed++;
159 }
160 }
161
162 const jsonValidateChecks = [
163 {
164 name: 'JsonValidate.mo: enrich prepare + GET normalize helpers',
165 ok: (s) =>
166 s.includes('prepareEnrichJsonArray') &&
167 s.includes('prepareEnrichJsonObject') &&
168 s.includes('normalizeJsonArrayFragment') &&
169 s.includes('normalizeJsonObjectFragment'),
170 },
171 ];
172
173 if (!fs.existsSync(JSON_VALIDATE)) {
174 console.error(`FAIL: JsonValidate.mo missing\n file: ${JSON_VALIDATE}`);
175 failed++;
176 } else {
177 const jv = readUtf8(JSON_VALIDATE);
178 for (const { name, ok } of jsonValidateChecks) {
179 if (!ok(jv)) {
180 console.error(`FAIL: ${name}\n file: ${JSON_VALIDATE}`);
181 failed++;
182 }
183 }
184 }
185
186 if (failed > 0) {
187 console.error(`\nverify-canister-migration: ${failed} check(s) failed.`);
188 process.exit(1);
189 }
190
191 console.log('verify-canister-migration: OK (Migration.mo + main.mo contracts).');
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago