@njeybe/nativ 2.0.0

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 (140) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/bin/cli.js +31 -0
  5. package/dist/commands/bench.d.ts +8 -0
  6. package/dist/commands/bench.js +52 -0
  7. package/dist/commands/db.d.ts +22 -0
  8. package/dist/commands/db.js +415 -0
  9. package/dist/commands/doctor.d.ts +38 -0
  10. package/dist/commands/doctor.js +169 -0
  11. package/dist/commands/hook.d.ts +31 -0
  12. package/dist/commands/hook.js +105 -0
  13. package/dist/commands/init.d.ts +3 -0
  14. package/dist/commands/init.js +144 -0
  15. package/dist/commands/mcp.d.ts +5 -0
  16. package/dist/commands/mcp.js +16 -0
  17. package/dist/commands/setup.d.ts +17 -0
  18. package/dist/commands/setup.js +51 -0
  19. package/dist/commands/status.d.ts +5 -0
  20. package/dist/commands/status.js +75 -0
  21. package/dist/commands/task.d.ts +83 -0
  22. package/dist/commands/task.js +807 -0
  23. package/dist/commands/test-gen.d.ts +14 -0
  24. package/dist/commands/test-gen.js +52 -0
  25. package/dist/commands/triage.d.ts +70 -0
  26. package/dist/commands/triage.js +343 -0
  27. package/dist/commands/update.d.ts +14 -0
  28. package/dist/commands/update.js +112 -0
  29. package/dist/commands/validate.d.ts +1 -0
  30. package/dist/commands/validate.js +89 -0
  31. package/dist/commands/verify.d.ts +8 -0
  32. package/dist/commands/verify.js +65 -0
  33. package/dist/commands/worktree.d.ts +47 -0
  34. package/dist/commands/worktree.js +396 -0
  35. package/dist/core/benchmark.d.ts +57 -0
  36. package/dist/core/benchmark.js +772 -0
  37. package/dist/core/enforcement.d.ts +80 -0
  38. package/dist/core/enforcement.js +271 -0
  39. package/dist/core/legacy-templates.d.ts +24 -0
  40. package/dist/core/legacy-templates.js +60 -0
  41. package/dist/core/lock-manager.d.ts +54 -0
  42. package/dist/core/lock-manager.js +260 -0
  43. package/dist/core/root-resolver.d.ts +32 -0
  44. package/dist/core/root-resolver.js +149 -0
  45. package/dist/core/setup-assets.d.ts +123 -0
  46. package/dist/core/setup-assets.js +438 -0
  47. package/dist/core/telemetry.d.ts +156 -0
  48. package/dist/core/telemetry.js +416 -0
  49. package/dist/core/test-generator-types.d.ts +227 -0
  50. package/dist/core/test-generator-types.js +179 -0
  51. package/dist/core/test-generator.d.ts +34 -0
  52. package/dist/core/test-generator.js +1690 -0
  53. package/dist/core/tier1-liaison.d.ts +139 -0
  54. package/dist/core/tier1-liaison.js +320 -0
  55. package/dist/core/verifier.d.ts +52 -0
  56. package/dist/core/verifier.js +249 -0
  57. package/dist/core/version.d.ts +5 -0
  58. package/dist/core/version.js +14 -0
  59. package/dist/core/worktree-merge.d.ts +30 -0
  60. package/dist/core/worktree-merge.js +98 -0
  61. package/dist/db/data-engine.d.ts +46 -0
  62. package/dist/db/data-engine.js +802 -0
  63. package/dist/db/diff.d.ts +39 -0
  64. package/dist/db/diff.js +420 -0
  65. package/dist/db/env-parser.d.ts +86 -0
  66. package/dist/db/env-parser.js +549 -0
  67. package/dist/db/introspector.d.ts +21 -0
  68. package/dist/db/introspector.js +835 -0
  69. package/dist/db/types.d.ts +217 -0
  70. package/dist/db/types.js +9 -0
  71. package/dist/governor/circuit-breaker.d.ts +100 -0
  72. package/dist/governor/circuit-breaker.js +231 -0
  73. package/dist/governor/evaluator.d.ts +67 -0
  74. package/dist/governor/evaluator.js +851 -0
  75. package/dist/governor/index.d.ts +6 -0
  76. package/dist/governor/index.js +6 -0
  77. package/dist/governor/rules/api-rules.d.ts +2 -0
  78. package/dist/governor/rules/api-rules.js +140 -0
  79. package/dist/governor/rules/db-rules.d.ts +2 -0
  80. package/dist/governor/rules/db-rules.js +180 -0
  81. package/dist/governor/store.d.ts +33 -0
  82. package/dist/governor/store.js +84 -0
  83. package/dist/governor/types.d.ts +64 -0
  84. package/dist/governor/types.js +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.js +358 -0
  87. package/dist/mcp/server.d.ts +3 -0
  88. package/dist/mcp/server.js +309 -0
  89. package/dist/providers/claude-api.d.ts +5 -0
  90. package/dist/providers/claude-api.js +75 -0
  91. package/dist/providers/claude-cli.d.ts +13 -0
  92. package/dist/providers/claude-cli.js +134 -0
  93. package/dist/providers/gemini.d.ts +8 -0
  94. package/dist/providers/gemini.js +78 -0
  95. package/dist/providers/index.d.ts +5 -0
  96. package/dist/providers/index.js +5 -0
  97. package/dist/providers/registry.d.ts +38 -0
  98. package/dist/providers/registry.js +121 -0
  99. package/dist/providers/types.d.ts +74 -0
  100. package/dist/providers/types.js +25 -0
  101. package/dist/runner/agent-supervisor.d.ts +363 -0
  102. package/dist/runner/agent-supervisor.js +1672 -0
  103. package/dist/scanner/context-builder.d.ts +2 -0
  104. package/dist/scanner/context-builder.js +29 -0
  105. package/dist/scanner/detector.d.ts +2 -0
  106. package/dist/scanner/detector.js +760 -0
  107. package/dist/scanner/types.d.ts +80 -0
  108. package/dist/scanner/types.js +50 -0
  109. package/dist/server/studio-server.d.ts +39 -0
  110. package/dist/server/studio-server.js +1931 -0
  111. package/dist/server/studio-ui.d.ts +15 -0
  112. package/dist/server/studio-ui.js +5321 -0
  113. package/package.json +70 -0
  114. package/plugin/.claude-plugin/plugin.json +18 -0
  115. package/plugin/.mcp.json +10 -0
  116. package/plugin/agents/architect.md +28 -0
  117. package/plugin/agents/verifier.md +28 -0
  118. package/plugin/agents/worker.md +26 -0
  119. package/plugin/hooks/hooks.json +28 -0
  120. package/plugin/skills/nativ/SKILL.md +75 -0
  121. package/templates/.claudeignore +45 -0
  122. package/templates/AGENTS.md +67 -0
  123. package/templates/CLAUDE.md +32 -0
  124. package/templates/GEMINI.md +66 -0
  125. package/templates/claude-agents/architect.md +28 -0
  126. package/templates/claude-agents/verifier.md +28 -0
  127. package/templates/claude-agents/worker.md +26 -0
  128. package/templates/dot-ai/api_contracts.json +66 -0
  129. package/templates/dot-ai/context.md +38 -0
  130. package/templates/dot-ai/db_schema.json +39 -0
  131. package/templates/dot-ai/master_plan.json +28 -0
  132. package/templates/dot-ai/subagents/backend.md +11 -0
  133. package/templates/dot-ai/subagents/database.md +10 -0
  134. package/templates/dot-ai/subagents/db-migration.md +11 -0
  135. package/templates/dot-ai/subagents/devops-agent.md +11 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +11 -0
  137. package/templates/dot-ai/subagents/frontend.md +126 -0
  138. package/templates/dot-ai/subagents/qa-tester.md +13 -0
  139. package/templates/dot-ai/subagents/security-auditor.md +11 -0
  140. package/templates/dot-ai/ui_specs.md +78 -0
@@ -0,0 +1,39 @@
1
+ import type { DatabaseEngine, SchemaDiff, SchemaDiffSummary, TableDiff, TableSchema } from './types.js';
2
+ export declare function normalizeType(type: string): string;
3
+ export declare function normalizeDefault(value: string | null | undefined): string | null;
4
+ export declare function diffTable(source: TableSchema, target: TableSchema): TableDiff;
5
+ /** Computes the GET /api/diff payload for `source` applied onto `target`. */
6
+ export declare function diffSchemas(source: TableSchema[], target: TableSchema[]): SchemaDiff;
7
+ export type MigrationLanguage = 'sql' | 'mongodb-js' | 'firestore-json';
8
+ export interface ContractDiffSummary extends SchemaDiffSummary {
9
+ addedCount: number;
10
+ alteredCount: number;
11
+ droppedCount: number;
12
+ unchangedCount: number;
13
+ /** Engine the migration script targets. */
14
+ engine: DatabaseEngine;
15
+ }
16
+ /**
17
+ * GET /api/diff response per .ai/api_contracts.json. The legacy *Tables keys are kept
18
+ * alongside the contract keys so existing studio/CLI consumers keep working.
19
+ */
20
+ export interface ContractSchemaDiff extends SchemaDiff {
21
+ summary: ContractDiffSummary;
22
+ added: TableSchema[];
23
+ dropped: TableSchema[];
24
+ altered: TableDiff[];
25
+ unchanged: string[];
26
+ migrationScript: string;
27
+ migrationLanguage: MigrationLanguage;
28
+ }
29
+ export declare function toContractDiff(diff: SchemaDiff, engine: DatabaseEngine, targetLabel: string): ContractSchemaDiff;
30
+ /** Builds the migration script for the engine being migrated (the diff target's engine). */
31
+ export declare function generateMigrationScript(diff: SchemaDiff, engine: DatabaseEngine, targetLabel: string): {
32
+ script: string;
33
+ language: MigrationLanguage;
34
+ };
35
+ export declare const CONTRACT_SCHEMA_PATH: string;
36
+ /** Engine declared in .ai/db_schema.json, if valid. */
37
+ export declare function loadContractEngine(cwd?: string): DatabaseEngine | null;
38
+ /** Reads table/collection definitions from .ai/db_schema.json for Live-vs-Contract drift checks. */
39
+ export declare function loadContractTables(cwd?: string): TableSchema[];
@@ -0,0 +1,420 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Schema diff & drift engine.
5
+ *
6
+ * Direction: `source` is the desired schema (Dev, or a live DB when comparing to the contract),
7
+ * `target` is the schema it would be applied to (Prod, or .ai/db_schema.json).
8
+ * - addedTables: exist in source only
9
+ * - droppedTables: exist in target only (applying source would drop them → destructive)
10
+ * - alteredTables: exist in both but differ
11
+ * For altered columns, `before` is the target definition and `after` the source definition.
12
+ */
13
+ // ─── Normalization ─────────────────────────────────────────────────────────────
14
+ const TYPE_ALIASES = {
15
+ INT: 'INTEGER',
16
+ INT4: 'INTEGER',
17
+ INT2: 'SMALLINT',
18
+ INT8: 'BIGINT',
19
+ SERIAL: 'INTEGER',
20
+ SERIAL4: 'INTEGER',
21
+ BIGSERIAL: 'BIGINT',
22
+ SERIAL8: 'BIGINT',
23
+ BOOL: 'BOOLEAN',
24
+ FLOAT4: 'REAL',
25
+ FLOAT8: 'DOUBLE PRECISION',
26
+ DECIMAL: 'NUMERIC',
27
+ TIMESTAMPTZ: 'TIMESTAMP WITH TIME ZONE',
28
+ 'TIMESTAMP WITHOUT TIME ZONE': 'TIMESTAMP',
29
+ TIMETZ: 'TIME WITH TIME ZONE',
30
+ 'TIME WITHOUT TIME ZONE': 'TIME',
31
+ 'CHARACTER VARYING': 'VARCHAR',
32
+ CHARACTER: 'CHAR',
33
+ };
34
+ export function normalizeType(type) {
35
+ const upper = type.trim().toUpperCase().replace(/\s+/g, ' ').replace(/\s*\(\s*/g, '(').replace(/\s*,\s*/g, ',').replace(/\s*\)/g, ')');
36
+ const match = upper.match(/^([A-Z0-9_ ]+?)(\(.*\))?(\[\])?$/);
37
+ if (!match)
38
+ return upper;
39
+ const [, base, args = '', array = ''] = match;
40
+ return `${TYPE_ALIASES[base] ?? base}${args}${array}`;
41
+ }
42
+ export function normalizeDefault(value) {
43
+ if (value == null)
44
+ return null;
45
+ let v = value.trim();
46
+ if (!v || v.toUpperCase() === 'NULL')
47
+ return null;
48
+ // Strip Postgres casts like 'user'::character varying or '{}'::jsonb.
49
+ v = v.replace(/::[a-z_][a-z0-9_ ]*(\[\])?(\([^)]*\))?/gi, '');
50
+ while (v.startsWith('(') && v.endsWith(')'))
51
+ v = v.slice(1, -1).trim();
52
+ const lower = v.toLowerCase();
53
+ if (lower === 'current_timestamp' || lower === 'current_timestamp()' || lower === 'now()')
54
+ return 'now()';
55
+ return lower;
56
+ }
57
+ const key = (name) => name.toLowerCase();
58
+ function indexBy(items, keyOf) {
59
+ const map = new Map();
60
+ for (const item of items)
61
+ map.set(keyOf(item), item);
62
+ return map;
63
+ }
64
+ // ─── Comparison ────────────────────────────────────────────────────────────────
65
+ /** A field counts as an image field when flagged by introspection or declared as IMAGE_URL. */
66
+ const isMediaColumn = (c) => c.isMedia === true || normalizeType(c.type) === 'IMAGE_URL';
67
+ function compareColumns(before, after) {
68
+ const changedFields = [];
69
+ if (normalizeType(before.type) !== normalizeType(after.type))
70
+ changedFields.push('type');
71
+ if (before.nullable !== after.nullable)
72
+ changedFields.push('nullable');
73
+ if (before.primaryKey !== after.primaryKey)
74
+ changedFields.push('primaryKey');
75
+ if (normalizeDefault(before.default) !== normalizeDefault(after.default))
76
+ changedFields.push('default');
77
+ if (isMediaColumn(before) !== isMediaColumn(after))
78
+ changedFields.push('isMedia');
79
+ return changedFields.length ? { name: after.name, before, after, changedFields } : null;
80
+ }
81
+ /**
82
+ * Changes that can lose data or fail against existing rows. Document stores have no
83
+ * NOT NULL constraint, so nullability changes are only destructive for SQL tables.
84
+ */
85
+ function isDestructiveColumnChange(change, isCollection) {
86
+ return (change.changedFields.includes('type') ||
87
+ change.changedFields.includes('primaryKey') ||
88
+ (!isCollection && change.changedFields.includes('nullable') && change.before.nullable && !change.after.nullable));
89
+ }
90
+ const indexSignature = (idx) => `${idx.unique ? 'U' : 'N'}:${idx.columns.map(key).join(',')}`;
91
+ // FK names are engine-specific (SQLite synthesizes them), so match on structure instead.
92
+ const fkSignature = (fk) => `${key(fk.column)}->${key(fk.referencedTable)}.${key(fk.referencedColumn)}:${(fk.onDelete ?? 'NO ACTION').toUpperCase()}`;
93
+ export function diffTable(source, target) {
94
+ const sourceCols = indexBy(source.columns, (c) => key(c.name));
95
+ const targetCols = indexBy(target.columns, (c) => key(c.name));
96
+ const addedColumns = source.columns.filter((c) => !targetCols.has(key(c.name)));
97
+ const droppedColumns = target.columns.filter((c) => !sourceCols.has(key(c.name)));
98
+ const alteredColumns = [];
99
+ for (const col of source.columns) {
100
+ const before = targetCols.get(key(col.name));
101
+ const change = before && compareColumns(before, col);
102
+ if (change)
103
+ alteredColumns.push(change);
104
+ }
105
+ // Indexes match by name; a same-named index with a different definition is dropped and re-added.
106
+ const sourceIdx = indexBy(source.indexes ?? [], (i) => key(i.name));
107
+ const targetIdx = indexBy(target.indexes ?? [], (i) => key(i.name));
108
+ const addedIndexes = (source.indexes ?? []).filter((i) => {
109
+ const t = targetIdx.get(key(i.name));
110
+ return !t || indexSignature(t) !== indexSignature(i);
111
+ });
112
+ const droppedIndexes = (target.indexes ?? []).filter((i) => {
113
+ const s = sourceIdx.get(key(i.name));
114
+ return !s || indexSignature(s) !== indexSignature(i);
115
+ });
116
+ const sourceFks = new Set((source.foreignKeys ?? []).map(fkSignature));
117
+ const targetFks = new Set((target.foreignKeys ?? []).map(fkSignature));
118
+ const addedForeignKeys = (source.foreignKeys ?? []).filter((fk) => !targetFks.has(fkSignature(fk)));
119
+ const droppedForeignKeys = (target.foreignKeys ?? []).filter((fk) => !sourceFks.has(fkSignature(fk)));
120
+ const isCollection = source.entityType === 'collection' || target.entityType === 'collection';
121
+ // Adding a NOT NULL column without a default fails on populated SQL tables; schemaless stores don't care.
122
+ const addsRequiredColumn = !isCollection && addedColumns.some((c) => !c.nullable && normalizeDefault(c.default) === null && !c.primaryKey);
123
+ return {
124
+ name: source.name,
125
+ addedColumns,
126
+ droppedColumns,
127
+ alteredColumns,
128
+ addedIndexes,
129
+ droppedIndexes,
130
+ addedForeignKeys,
131
+ droppedForeignKeys,
132
+ isDestructive: droppedColumns.length > 0 || alteredColumns.some((c) => isDestructiveColumnChange(c, isCollection)) || addsRequiredColumn,
133
+ };
134
+ }
135
+ function hasChanges(diff) {
136
+ return (diff.addedColumns.length +
137
+ diff.droppedColumns.length +
138
+ diff.alteredColumns.length +
139
+ diff.addedIndexes.length +
140
+ diff.droppedIndexes.length +
141
+ diff.addedForeignKeys.length +
142
+ diff.droppedForeignKeys.length >
143
+ 0);
144
+ }
145
+ /** Computes the GET /api/diff payload for `source` applied onto `target`. */
146
+ export function diffSchemas(source, target) {
147
+ const sourceMap = indexBy(source, (t) => key(t.name));
148
+ const targetMap = indexBy(target, (t) => key(t.name));
149
+ const addedTables = source.filter((t) => !targetMap.has(key(t.name)));
150
+ const droppedTables = target.filter((t) => !sourceMap.has(key(t.name)));
151
+ const alteredTables = [];
152
+ const unchangedTables = [];
153
+ for (const table of source) {
154
+ const counterpart = targetMap.get(key(table.name));
155
+ if (!counterpart)
156
+ continue;
157
+ const diff = diffTable(table, counterpart);
158
+ if (hasChanges(diff))
159
+ alteredTables.push(diff);
160
+ else
161
+ unchangedTables.push(table.name);
162
+ }
163
+ const byName = (a, b) => a.name.localeCompare(b.name);
164
+ addedTables.sort(byName);
165
+ droppedTables.sort(byName);
166
+ alteredTables.sort(byName);
167
+ unchangedTables.sort((a, b) => a.localeCompare(b));
168
+ return {
169
+ summary: {
170
+ addedTablesCount: addedTables.length,
171
+ alteredTablesCount: alteredTables.length,
172
+ droppedTablesCount: droppedTables.length,
173
+ unchangedTablesCount: unchangedTables.length,
174
+ hasDestructiveChanges: droppedTables.length > 0 || alteredTables.some((t) => t.isDestructive),
175
+ },
176
+ addedTables,
177
+ droppedTables,
178
+ alteredTables,
179
+ unchangedTables,
180
+ };
181
+ }
182
+ export function toContractDiff(diff, engine, targetLabel) {
183
+ const { script, language } = generateMigrationScript(diff, engine, targetLabel);
184
+ return {
185
+ ...diff,
186
+ summary: {
187
+ ...diff.summary,
188
+ addedCount: diff.summary.addedTablesCount,
189
+ alteredCount: diff.summary.alteredTablesCount,
190
+ droppedCount: diff.summary.droppedTablesCount,
191
+ unchangedCount: diff.summary.unchangedTablesCount,
192
+ engine,
193
+ },
194
+ added: diff.addedTables,
195
+ dropped: diff.droppedTables,
196
+ altered: diff.alteredTables,
197
+ unchanged: diff.unchangedTables,
198
+ migrationScript: script,
199
+ migrationLanguage: language,
200
+ };
201
+ }
202
+ // ─── Migration script generation (SQL DDL / MongoDB shell / Firestore index JSON) ──
203
+ function hasDrift(d) {
204
+ return d.addedTables.length + d.alteredTables.length + d.droppedTables.length > 0;
205
+ }
206
+ function generateSql(d, dialect, targetLabel) {
207
+ const q = (name) => (dialect === 'mysql' ? `\`${name.split('`').join('``')}\`` : `"${name.split('"').join('""')}"`);
208
+ const colDef = (c) => `${q(c.name)} ${c.type}${c.nullable ? '' : ' NOT NULL'}${c.default != null ? ` DEFAULT ${c.default}` : ''}`;
209
+ const fkDef = (f) => `CONSTRAINT ${q(f.name)} FOREIGN KEY (${q(f.column)}) REFERENCES ${q(f.referencedTable)} (${q(f.referencedColumn)})${f.onDelete ? ` ON DELETE ${f.onDelete}` : ''}`;
210
+ const idxDef = (t, i) => `CREATE ${i.unique ? 'UNIQUE ' : ''}INDEX ${q(i.name)} ON ${q(t)} (${i.columns.map(q).join(', ')});`;
211
+ const dropIdx = (t, i) => (dialect === 'mysql' ? `DROP INDEX ${q(i.name)} ON ${q(t)};` : `DROP INDEX ${q(i.name)};`);
212
+ const label = dialect === 'postgresql' ? 'PostgreSQL' : dialect === 'mysql' ? 'MySQL' : 'SQLite';
213
+ const out = [
214
+ `-- nativ migration preview: Dev → ${targetLabel} (${label} dialect)`,
215
+ '-- Generated from structural metadata only. Review carefully; nothing is executed automatically.',
216
+ '',
217
+ ];
218
+ for (const t of d.addedTables) {
219
+ const lines = t.columns.map((c) => ` ${colDef(c)}`);
220
+ const pks = t.columns.filter((c) => c.primaryKey).map((c) => q(c.name));
221
+ if (pks.length)
222
+ lines.push(` PRIMARY KEY (${pks.join(', ')})`);
223
+ for (const f of t.foreignKeys ?? [])
224
+ lines.push(` ${fkDef(f)}`);
225
+ out.push(`-- [NEW TABLE] ${t.name}`, `CREATE TABLE ${q(t.name)} (\n${lines.join(',\n')}\n);`);
226
+ for (const i of t.indexes ?? [])
227
+ out.push(idxDef(t.name, i));
228
+ out.push('');
229
+ }
230
+ for (const t of d.alteredTables) {
231
+ const T = `ALTER TABLE ${q(t.name)} `;
232
+ out.push(`-- [ALTERED] ${t.name}${t.isDestructive ? ' ⚠ DESTRUCTIVE' : ''}`);
233
+ for (const f of t.droppedForeignKeys) {
234
+ out.push(dialect === 'sqlite' ? `-- SQLite cannot drop constraints in place; rebuild ${t.name} to remove FK ${f.name}` : `${T}${dialect === 'mysql' ? 'DROP FOREIGN KEY' : 'DROP CONSTRAINT'} ${q(f.name)};`);
235
+ }
236
+ for (const i of t.droppedIndexes)
237
+ out.push(dropIdx(t.name, i));
238
+ for (const c of t.addedColumns)
239
+ out.push(`${T}ADD COLUMN ${colDef(c)};`);
240
+ for (const ch of t.alteredColumns) {
241
+ const c = ch.after;
242
+ const f = ch.changedFields.filter((x) => x !== 'isMedia');
243
+ if (!f.length)
244
+ continue;
245
+ if (dialect === 'mysql') {
246
+ out.push(`${T}MODIFY COLUMN ${colDef(c)};`);
247
+ continue;
248
+ }
249
+ if (dialect === 'sqlite') {
250
+ out.push(`-- SQLite cannot ALTER COLUMN ${t.name}.${c.name} (${f.join(', ')}); a table rebuild is required.`);
251
+ continue;
252
+ }
253
+ if (f.includes('type'))
254
+ out.push(`${T}ALTER COLUMN ${q(c.name)} TYPE ${c.type} USING ${q(c.name)}::${c.type};`);
255
+ if (f.includes('nullable'))
256
+ out.push(`${T}ALTER COLUMN ${q(c.name)} ${c.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'};`);
257
+ if (f.includes('default'))
258
+ out.push(`${T}ALTER COLUMN ${q(c.name)} ${c.default == null ? 'DROP DEFAULT' : `SET DEFAULT ${c.default}`};`);
259
+ if (f.includes('primaryKey'))
260
+ out.push(`-- Primary key membership changed for ${t.name}.${c.name}; recreate the PRIMARY KEY constraint manually.`);
261
+ }
262
+ for (const c of t.droppedColumns)
263
+ out.push('-- ⚠ DESTRUCTIVE: drops column data', `${T}DROP COLUMN ${q(c.name)};`);
264
+ for (const i of t.addedIndexes)
265
+ out.push(idxDef(t.name, i));
266
+ for (const f of t.addedForeignKeys) {
267
+ out.push(dialect === 'sqlite' ? `-- SQLite cannot add constraints in place; rebuild ${t.name} to add FK ${f.column} → ${f.referencedTable}` : `${T}ADD ${fkDef(f)};`);
268
+ }
269
+ out.push('');
270
+ }
271
+ for (const t of d.droppedTables) {
272
+ out.push(`-- ⚠ DESTRUCTIVE [DROPPED] ${t.name}: permanently deletes the table and all rows`, `DROP TABLE ${q(t.name)};`, '');
273
+ }
274
+ if (!hasDrift(d))
275
+ out.push('-- No schema drift. Nothing to migrate.');
276
+ return `${out.join('\n').replace(/\n+$/, '')}\n`;
277
+ }
278
+ function generateMongo(d, targetLabel) {
279
+ const coll = (name) => `db.getCollection(${JSON.stringify(name)})`;
280
+ const keySpec = (i) => `{ ${i.columns.map((c) => `${JSON.stringify(c)}: 1`).join(', ')} }`;
281
+ const createIdx = (t, i) => `${coll(t)}.createIndex(${keySpec(i)}, { name: ${JSON.stringify(i.name)}${i.unique ? ', unique: true' : ''} });`;
282
+ const out = [
283
+ `// nativ migration preview: Dev → ${targetLabel} (MongoDB shell)`,
284
+ '// Generated from structural metadata only. Review carefully; nothing is executed automatically.',
285
+ '',
286
+ ];
287
+ for (const t of d.addedTables) {
288
+ out.push(`// [NEW COLLECTION] ${t.name}`, `db.createCollection(${JSON.stringify(t.name)});`);
289
+ for (const i of t.indexes ?? [])
290
+ out.push(createIdx(t.name, i));
291
+ out.push('');
292
+ }
293
+ for (const t of d.alteredTables) {
294
+ out.push(`// [ALTERED] ${t.name}${t.isDestructive ? ' ⚠ DESTRUCTIVE' : ''}`);
295
+ for (const i of t.droppedIndexes)
296
+ out.push(`${coll(t.name)}.dropIndex(${JSON.stringify(i.name)});`);
297
+ for (const c of t.addedColumns) {
298
+ if (c.isSubcollection) {
299
+ out.push(`// + subcollection ${JSON.stringify(c.name)} (Firestore-only concept; model as a separate collection or embedded array)`);
300
+ continue;
301
+ }
302
+ const f = JSON.stringify(c.name);
303
+ out.push(`// + field ${f} (${c.type}): schemaless, no DDL needed. Optional backfill:`, `// ${coll(t.name)}.updateMany({ ${f}: { $exists: false } }, { $set: { ${f}: null } });`);
304
+ }
305
+ for (const ch of t.alteredColumns) {
306
+ out.push(`// ~ field ${JSON.stringify(ch.name)}: ${ch.before.type} → ${ch.after.type} (${ch.changedFields.join(', ')}); convert existing values with a data migration.`);
307
+ }
308
+ for (const c of t.droppedColumns) {
309
+ out.push('// ⚠ DESTRUCTIVE: removes field data from every document', `${coll(t.name)}.updateMany({}, { $unset: { ${JSON.stringify(c.name)}: "" } });`);
310
+ }
311
+ for (const i of t.addedIndexes)
312
+ out.push(createIdx(t.name, i));
313
+ out.push('');
314
+ }
315
+ for (const t of d.droppedTables) {
316
+ out.push(`// ⚠ DESTRUCTIVE [DROPPED] ${t.name}: permanently deletes the collection and all documents`, `${coll(t.name)}.drop();`, '');
317
+ }
318
+ if (!hasDrift(d))
319
+ out.push('// No schema drift. Nothing to migrate.');
320
+ return `${out.join('\n').replace(/\n+$/, '')}\n`;
321
+ }
322
+ /** Firestore: deployable index definitions (firestore.indexes.json shape) plus data-migration steps. */
323
+ function generateFirestore(d, targetLabel) {
324
+ const indexes = [];
325
+ const fieldOverrides = [];
326
+ const steps = [];
327
+ const warnings = [];
328
+ const addIndex = (collection, i) => {
329
+ if (i.unique) {
330
+ warnings.push(`Firestore has no unique indexes: enforce uniqueness of ${collection}.${i.columns.join('+')} (${i.name}) in application code or security rules.`);
331
+ }
332
+ if (i.columns.length > 1) {
333
+ indexes.push({ collectionGroup: collection, queryScope: 'COLLECTION', fields: i.columns.map((f) => ({ fieldPath: f, order: 'ASCENDING' })) });
334
+ }
335
+ else if (i.columns.length === 1) {
336
+ fieldOverrides.push({ collectionGroup: collection, fieldPath: i.columns[0], indexes: [{ order: 'ASCENDING', queryScope: 'COLLECTION' }] });
337
+ }
338
+ };
339
+ for (const t of d.addedTables) {
340
+ steps.push({ op: 'createCollection', collection: t.name, note: 'Collections are created implicitly by the first document write.' });
341
+ for (const i of t.indexes ?? [])
342
+ addIndex(t.name, i);
343
+ }
344
+ for (const t of d.alteredTables) {
345
+ for (const c of t.addedColumns) {
346
+ steps.push(c.isSubcollection ? { op: 'addSubcollection', collection: t.name, subcollection: c.name } : { op: 'addField', collection: t.name, field: c.name, type: c.type });
347
+ }
348
+ for (const ch of t.alteredColumns) {
349
+ steps.push({ op: 'changeField', collection: t.name, field: ch.name, from: ch.before.type, to: ch.after.type, changed: ch.changedFields });
350
+ }
351
+ for (const c of t.droppedColumns)
352
+ steps.push({ op: 'deleteField', collection: t.name, field: c.name, destructive: true });
353
+ for (const i of t.addedIndexes)
354
+ addIndex(t.name, i);
355
+ for (const i of t.droppedIndexes)
356
+ steps.push({ op: 'removeIndex', collection: t.name, index: i.name, fields: i.columns });
357
+ }
358
+ for (const t of d.droppedTables) {
359
+ steps.push({ op: 'deleteCollection', collection: t.name, destructive: true, note: 'Permanently deletes every document (e.g. firebase firestore:delete --recursive).' });
360
+ }
361
+ const doc = {
362
+ _nativ: `Preview Dev → ${targetLabel} (Firestore). Nothing runs automatically.`,
363
+ _deploy: 'indexes + fieldOverrides: firebase deploy --only firestore:indexes; dataMigrations: apply with a script',
364
+ indexes,
365
+ fieldOverrides,
366
+ dataMigrations: steps,
367
+ };
368
+ if (warnings.length)
369
+ doc.warnings = warnings;
370
+ return `${JSON.stringify(doc, null, 2)}\n`;
371
+ }
372
+ /** Builds the migration script for the engine being migrated (the diff target's engine). */
373
+ export function generateMigrationScript(diff, engine, targetLabel) {
374
+ switch (engine) {
375
+ case 'mongodb':
376
+ return { script: generateMongo(diff, targetLabel), language: 'mongodb-js' };
377
+ case 'firestore':
378
+ return { script: generateFirestore(diff, targetLabel), language: 'firestore-json' };
379
+ default:
380
+ return { script: generateSql(diff, engine, targetLabel), language: 'sql' };
381
+ }
382
+ }
383
+ // ─── Contract source ──────────────────────────────────────────────────────────
384
+ export const CONTRACT_SCHEMA_PATH = path.join('.ai', 'db_schema.json');
385
+ /** Engine declared in .ai/db_schema.json, if valid. */
386
+ export function loadContractEngine(cwd = process.cwd()) {
387
+ try {
388
+ const parsed = JSON.parse(fs.readFileSync(path.join(cwd, CONTRACT_SCHEMA_PATH), 'utf8'));
389
+ const engines = ['postgresql', 'mysql', 'sqlite', 'mongodb', 'firestore'];
390
+ return engines.includes(parsed.engine) ? parsed.engine : null;
391
+ }
392
+ catch {
393
+ return null;
394
+ }
395
+ }
396
+ /** Reads table/collection definitions from .ai/db_schema.json for Live-vs-Contract drift checks. */
397
+ export function loadContractTables(cwd = process.cwd()) {
398
+ const filePath = path.join(cwd, CONTRACT_SCHEMA_PATH);
399
+ if (!fs.existsSync(filePath))
400
+ return [];
401
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
402
+ return (parsed.tables ?? [])
403
+ .filter((t) => typeof t?.name === 'string')
404
+ .map((t) => ({
405
+ name: t.name,
406
+ ...(t.entityType === 'collection' || t.entityType === 'table' ? { entityType: t.entityType } : {}),
407
+ ...(t.description ? { description: t.description } : {}),
408
+ columns: (t.columns ?? []).map((c) => ({
409
+ name: c.name,
410
+ type: c.type,
411
+ primaryKey: Boolean(c.primaryKey),
412
+ nullable: c.nullable !== false,
413
+ default: c.default ?? null,
414
+ ...(c.isMedia ? { isMedia: true } : {}),
415
+ ...(c.isSubcollection ? { isSubcollection: true } : {}),
416
+ })),
417
+ indexes: t.indexes ?? [],
418
+ foreignKeys: t.foreignKeys ?? [],
419
+ }));
420
+ }
@@ -0,0 +1,86 @@
1
+ import type { ComponentEngine, ConnectionComponents, DatabaseEngine, DatabaseEnv, FirestoreConfig, FragmentedConfig, TemplateDetectionResult } from './types.js';
2
+ /**
3
+ * Safe environment parser.
4
+ * Reads .env files locally so connection strings stay in-memory inside the nativ
5
+ * process. Nothing returned here for display purposes contains credentials —
6
+ * always pass URLs through maskConnectionString() before rendering them.
7
+ */
8
+ export declare const MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
9
+ /** Template files scanned to detect project-specific connection variables. */
10
+ export declare const EXAMPLE_ENV_FILES: string[];
11
+ export interface ResolvedConnection {
12
+ env: DatabaseEnv;
13
+ /** Raw URL. Never log, print, or serialize this value. */
14
+ url: string;
15
+ engine: DatabaseEngine;
16
+ maskedUrl: string;
17
+ sourceKey: string;
18
+ detectedFromExample?: boolean;
19
+ exampleFile?: string | null;
20
+ suggestion?: string | null;
21
+ /** True when assembled from fragmented DB_HOST/DB_DATABASE-style keys. */
22
+ synthesized?: boolean;
23
+ }
24
+ /** Parses dotenv-formatted text into key/value pairs without touching process.env. */
25
+ export declare function parseEnvContent(content: string): Record<string, string>;
26
+ /** Loads and merges all known .env files in `cwd`. Missing/unreadable files are skipped. */
27
+ export declare function loadEnvFiles(cwd?: string): Record<string, string>;
28
+ /** Infers the database engine from a connection URL or file path. */
29
+ export declare function detectEngine(url: string): DatabaseEngine | null;
30
+ /** Resolves a SQLite URL (`sqlite:./x.db`, `file:./x.db`, or plain path) to a filesystem path. */
31
+ export declare function sqlitePathFromUrl(url: string, cwd?: string): string;
32
+ /**
33
+ * Firestore connection URL: `firestore://<projectId>[/<databaseId>][?emulator=<host:port>]`.
34
+ * Credentials are never part of it: real projects use Application Default Credentials.
35
+ */
36
+ export declare function parseFirestoreUrl(url: string): FirestoreConfig;
37
+ export declare function firestoreUrlFromConfig(config: FirestoreConfig): string;
38
+ /**
39
+ * Redacts credentials from a connection string.
40
+ * `postgres://user:secret@host:5432/db?password=x` -> `postgres://user:••••••••@host:5432/db?password=••••••••`
41
+ */
42
+ export declare function maskConnectionString(url: string | null | undefined): string;
43
+ /** Redacts signed-URL tokens from a media URL (e.g. Firebase Storage `?token=`, GCS `X-Goog-Signature`). */
44
+ export declare function redactMediaUrl(url: string): string;
45
+ /** Extracts the database name (or SQLite file name / Firestore project) from a connection URL for display. */
46
+ export declare function databaseNameFromUrl(url: string): string;
47
+ /**
48
+ * Scans template environment files (.env.example, .env.sample, .env.template, .env.defaults, .env.dist)
49
+ * to detect project-specific connection string variables, implied database engines, and whether
50
+ * the variables are configured in the current environment.
51
+ */
52
+ export declare function detectExampleDbKeys(cwd?: string): TemplateDetectionResult;
53
+ export declare const DEFAULT_PORTS: Record<ComponentEngine, number>;
54
+ /** Maps a DB_CONNECTION-style driver name (mysql, mariadb, pgsql, postgres…) to an engine. */
55
+ export declare function componentEngineFromDriver(driver: string | null | undefined): ComponentEngine | null;
56
+ /**
57
+ * Assembles a connection URL from discrete parameters, percent-encoding credentials and database name.
58
+ * The result contains the raw password: pass it through maskConnectionString() before display.
59
+ */
60
+ export declare function connectionUrlFromComponents(components: ConnectionComponents): string;
61
+ export interface SynthesizedConnection {
62
+ /** Raw URL. Never log, print, or serialize this value. */
63
+ url: string;
64
+ engine: ComponentEngine;
65
+ /** Primary key shown as the source (the host or database key). */
66
+ sourceKey: string;
67
+ config: FragmentedConfig;
68
+ }
69
+ /**
70
+ * Detects fragmented DB_HOST / DB_PORT / DB_DATABASE / DB_USERNAME / DB_PASSWORD / DB_CONNECTION keys
71
+ * (plus MYSQL_* and POSTGRES_* / PG* families) and assembles a MySQL or PostgreSQL URL in-memory.
72
+ * Returns null when no family has a host or database configured, or the driver is not MySQL/PostgreSQL
73
+ * (e.g. Laravel's DB_CONNECTION=sqlite).
74
+ */
75
+ export declare function synthesizeConnectionFromComponents(vars: Record<string, string>, env?: DatabaseEnv): SynthesizedConnection | null;
76
+ export interface ResolvedConnectionsResult {
77
+ dev: ResolvedConnection | null;
78
+ prod: ResolvedConnection | null;
79
+ templateInfo: TemplateDetectionResult;
80
+ }
81
+ /**
82
+ * Resolves Dev and Prod connection URLs from .env files, with process.env taking precedence
83
+ * and template files (.env.example) dynamically prioritizing custom keys.
84
+ * Returns null for an environment with no configured or recognizable URL.
85
+ */
86
+ export declare function resolveConnections(cwd?: string): ResolvedConnectionsResult;