@feltdb/core 0.4.4 → 0.4.6
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/README.md +22 -0
- package/bin/create-feltdb.js +3 -0
- package/bin/feltdb.js +3 -0
- package/dist/analytics-backend.js +1 -1
- package/dist/cli/api-client.js +236 -0
- package/dist/cli/cli.js +18 -0
- package/dist/cli/commands.js +795 -0
- package/dist/cli/config.js +66 -0
- package/dist/cli/index.js +399 -0
- package/dist/create/application-identity.js +132 -0
- package/dist/create/cli-scripts-generator.js +211 -0
- package/dist/create/cli.js +202 -0
- package/dist/create/create.js +638 -0
- package/dist/create/docker-compose-generator.js +258 -0
- package/dist/create/index.js +4 -0
- package/dist/create/package-versions.js +4 -0
- package/dist/create/runtime-templates.js +272 -0
- package/dist/index-backend.js +1 -1
- package/dist/migrate/analyzer/index.d.ts +2 -0
- package/dist/migrate/analyzer/index.js +34 -0
- package/dist/migrate/benchmark/index.d.ts +13 -0
- package/dist/migrate/benchmark/index.js +8 -0
- package/dist/migrate/capability-gate/index.d.ts +3 -0
- package/dist/migrate/capability-gate/index.js +3 -0
- package/dist/migrate/classifier/index.d.ts +3 -0
- package/dist/migrate/classifier/index.js +53 -0
- package/dist/migrate/cli/index.d.ts +2 -0
- package/dist/migrate/cli/index.js +201 -0
- package/dist/migrate/domain-model/index.d.ts +2 -0
- package/dist/migrate/domain-model/index.js +4 -0
- package/dist/migrate/generator/index.d.ts +2 -0
- package/dist/migrate/generator/index.js +21 -0
- package/dist/migrate/index.d.ts +16 -0
- package/dist/migrate/index.js +16 -0
- package/dist/migrate/measurement/index.d.ts +7 -0
- package/dist/migrate/measurement/index.js +1 -0
- package/dist/migrate/migrator/index.d.ts +2 -0
- package/dist/migrate/migrator/index.js +35 -0
- package/dist/migrate/planner/index.d.ts +2 -0
- package/dist/migrate/planner/index.js +12 -0
- package/dist/migrate/prover/index.d.ts +9 -0
- package/dist/migrate/prover/index.js +3 -0
- package/dist/migrate/reporter/index.d.ts +4 -0
- package/dist/migrate/reporter/index.js +1 -0
- package/dist/migrate/runtime-dependencies/index.d.ts +13 -0
- package/dist/migrate/runtime-dependencies/index.js +112 -0
- package/dist/migrate/state-contract/index.d.ts +13 -0
- package/dist/migrate/state-contract/index.js +49 -0
- package/dist/migrate/types/index.d.ts +228 -0
- package/dist/migrate/types/index.js +1 -0
- package/dist/migrate/ui-wiring/index.d.ts +18 -0
- package/dist/migrate/ui-wiring/index.js +2 -0
- package/dist/migrate/util.d.ts +5 -0
- package/dist/migrate/util.js +10 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/useCapability.d.ts +11 -0
- package/dist/react/useCapability.d.ts.map +1 -0
- package/dist/react/useCapability.js +30 -0
- package/dist/react/useCollection.d.ts +17 -0
- package/dist/react/useCollection.d.ts.map +1 -0
- package/dist/react/useCollection.js +69 -0
- package/dist/react/useFeltDB.d.ts +136 -0
- package/dist/react/useFeltDB.d.ts.map +1 -0
- package/dist/react/useFeltDB.js +350 -0
- package/dist/studio/.gitkeep +1 -0
- package/dist/studio/KeyManagementPanel-BOvWTRyH.js +246 -0
- package/dist/studio/app.d.ts +10 -0
- package/dist/studio/app.d.ts.map +1 -0
- package/dist/studio/components/AgentExplorer.d.ts +9 -0
- package/dist/studio/components/AgentExplorer.d.ts.map +1 -0
- package/dist/studio/components/ApplicationDesigner.d.ts +11 -0
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -0
- package/dist/studio/components/CapabilityExplorer.d.ts +9 -0
- package/dist/studio/components/CapabilityExplorer.d.ts.map +1 -0
- package/dist/studio/components/ConflictExplorer.d.ts +9 -0
- package/dist/studio/components/ConflictExplorer.d.ts.map +1 -0
- package/dist/studio/components/ExecutionViewer.d.ts +9 -0
- package/dist/studio/components/ExecutionViewer.d.ts.map +1 -0
- package/dist/studio/components/GlobalSearch.d.ts +10 -0
- package/dist/studio/components/GlobalSearch.d.ts.map +1 -0
- package/dist/studio/components/HealthCenter.d.ts +9 -0
- package/dist/studio/components/HealthCenter.d.ts.map +1 -0
- package/dist/studio/components/KeyManagementPanel.d.ts +9 -0
- package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -0
- package/dist/studio/components/KeyManagementPanel.js +2 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts +4 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.js +281 -0
- package/dist/studio/components/OperationsExplorer.d.ts +10 -0
- package/dist/studio/components/OperationsExplorer.d.ts.map +1 -0
- package/dist/studio/components/OverviewDashboard.d.ts +8 -0
- package/dist/studio/components/OverviewDashboard.d.ts.map +1 -0
- package/dist/studio/components/PeerMap.d.ts +8 -0
- package/dist/studio/components/PeerMap.d.ts.map +1 -0
- package/dist/studio/components/ProvenanceViewer.d.ts +11 -0
- package/dist/studio/components/ProvenanceViewer.d.ts.map +1 -0
- package/dist/studio/components/ReferenceExplorer.d.ts +10 -0
- package/dist/studio/components/ReferenceExplorer.d.ts.map +1 -0
- package/dist/studio/components/SettingsPanel.d.ts +7 -0
- package/dist/studio/components/SettingsPanel.d.ts.map +1 -0
- package/dist/studio/components/StateExplorer.d.ts +10 -0
- package/dist/studio/components/StateExplorer.d.ts.map +1 -0
- package/dist/studio/components/WorkflowVisualizer.d.ts +9 -0
- package/dist/studio/components/WorkflowVisualizer.d.ts.map +1 -0
- package/dist/studio/components/index.d.ts +24 -0
- package/dist/studio/components/index.d.ts.map +1 -0
- package/dist/studio/components/index.js +4 -0
- package/dist/studio/components-BRYUceo9.js +1711 -0
- package/dist/studio/hooks/index.d.ts +37 -0
- package/dist/studio/hooks/index.d.ts.map +1 -0
- package/dist/studio/index.d.ts +27 -0
- package/dist/studio/index.d.ts.map +1 -0
- package/dist/studio/index.html +30 -0
- package/dist/studio/index.js +236 -0
- package/dist/studio/main.d.ts +1 -0
- package/dist/studio/main.d.ts.map +1 -0
- package/dist/studio/studio.css +2 -0
- package/dist/studio/types/index.d.ts +133 -0
- package/dist/studio/types/index.d.ts.map +1 -0
- package/dist/studio/utils/api.d.ts +27 -0
- package/dist/studio/utils/api.d.ts.map +1 -0
- package/dist/studio/utils/format.d.ts +24 -0
- package/dist/studio/utils/format.d.ts.map +1 -0
- package/dist/studio/utils/index.d.ts +7 -0
- package/dist/studio/utils/index.d.ts.map +1 -0
- package/dist/studio/utils/index.js +75 -0
- package/dist/studio/utils/managed-instance.d.ts +40 -0
- package/dist/studio/utils/managed-instance.d.ts.map +1 -0
- package/dist/studio/utils/managed-instance.js +51 -0
- package/dist/studio-app/.gitkeep +1 -0
- package/dist/studio-app/assets/feltdb_wasm-BXMn9UxO.js +1 -0
- package/dist/studio-app/assets/feltdb_wasm_bg-bYYbZeRM.wasm +0 -0
- package/dist/studio-app/assets/index-B-lZjdtI.js +28 -0
- package/dist/studio-app/assets/index-CB-Eed9V.css +1 -0
- package/dist/studio-app/index.html +3 -0
- package/dist/wasm/feltdb_wasm.d.ts +461 -0
- package/dist/wasm/feltdb_wasm.js +1690 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/feltdb_wasm_bg.wasm.d.ts +84 -0
- package/dist/wasm/package.json +16 -0
- package/package.json +40 -5
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { analyze, classify, approve, domainModel, gate, plan, discoverStateContract, generateRepositories, proveStatic, requireProof, measure, report, scanPostgresDependencies, requirePostgresRuntimeFree, configurationMigrationPlan, verifyPostgresIsolation, hash, writeJson } from '../index.js';
|
|
6
|
+
const argv = process.argv.slice(2), command = argv[0] ?? 'status';
|
|
7
|
+
function arg(name, def) { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : def; }
|
|
8
|
+
const cwd = process.cwd(), configFile = resolve(cwd, arg('config', 'felt-migrate.json'));
|
|
9
|
+
const HELP = `FeltDB Migration Sherpa
|
|
10
|
+
|
|
11
|
+
Usage: felt-migrate <command> [--config felt-migrate.json]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
analyze Discover schema and application persistence consumers
|
|
15
|
+
classify Classify every persistence entity
|
|
16
|
+
review Review classifications; add --approve to lock them
|
|
17
|
+
capabilities Run the fail-closed FeltDB capability gate
|
|
18
|
+
plan Generate the deterministic migration plan
|
|
19
|
+
generate Generate FeltDB-native repositories
|
|
20
|
+
state-contract Discover the canonical application state contract
|
|
21
|
+
ui-scan Inventory legacy and canonical UI state consumers
|
|
22
|
+
ui-verify Verify canonical UI state dependencies
|
|
23
|
+
postgres-scan Inventory and classify PostgreSQL dependencies
|
|
24
|
+
runtime-scan Scan application runtime connections and SQL
|
|
25
|
+
config-scan Scan environment and configuration dependencies
|
|
26
|
+
deployment-scan Scan containers, startup, health and deployment files
|
|
27
|
+
postgres-remove Generate an approval-required configuration removal plan
|
|
28
|
+
postgres-verify Prove startup and recovery with PostgreSQL blocked
|
|
29
|
+
prove Run static semantic proof checks
|
|
30
|
+
measure Capture measured before/after metrics
|
|
31
|
+
report Generate the auditable migration report
|
|
32
|
+
status Show migration readiness and provenance hashes
|
|
33
|
+
all Run the safe pipeline, pausing at approval gates
|
|
34
|
+
`;
|
|
35
|
+
async function loadConfig() { return JSON.parse(await readFile(configFile, 'utf8')); }
|
|
36
|
+
async function read(dir, file) { return JSON.parse(await readFile(join(dir, file), 'utf8')); }
|
|
37
|
+
async function run() {
|
|
38
|
+
const cfg = await loadConfig(), root = resolve(cwd, cfg.sourceRoot), dir = resolve(cwd, cfg.artifactDir ?? '.feltdb');
|
|
39
|
+
await mkdir(dir, { recursive: true });
|
|
40
|
+
const runtimeCommands = ['postgres-scan', 'runtime-scan', 'config-scan', 'deployment-scan', 'postgres-remove', 'postgres-verify'];
|
|
41
|
+
if (runtimeCommands.includes(command)) {
|
|
42
|
+
if (!cfg.feltDbRuntime)
|
|
43
|
+
throw new Error('feltDbRuntime must define the deployment-mode-specific runtime contract');
|
|
44
|
+
let manifest;
|
|
45
|
+
if (command === 'postgres-verify')
|
|
46
|
+
manifest = await read(dir, 'postgres-dependencies.json');
|
|
47
|
+
else {
|
|
48
|
+
manifest = await scanPostgresDependencies(root, cfg.feltDbRuntime, cfg.runtimeDependencyOverrides);
|
|
49
|
+
await writeJson(join(dir, 'postgres-dependencies.json'), manifest);
|
|
50
|
+
}
|
|
51
|
+
if (command === 'config-scan' || command === 'postgres-remove')
|
|
52
|
+
await writeJson(join(dir, 'postgres-config-migration-plan.json'), configurationMigrationPlan(manifest));
|
|
53
|
+
if (command === 'postgres-verify') {
|
|
54
|
+
let harness;
|
|
55
|
+
if (cfg.runtimeVerificationModule) {
|
|
56
|
+
const loaded = await import(pathToFileURL(resolve(cwd, cfg.runtimeVerificationModule)).href);
|
|
57
|
+
harness = await (loaded.createHarness?.() ?? loaded.default?.());
|
|
58
|
+
}
|
|
59
|
+
const proof = await verifyPostgresIsolation(manifest, harness);
|
|
60
|
+
await writeJson(join(dir, 'runtime-dependency-proof.json'), proof);
|
|
61
|
+
if (!proof.verifiedWithoutPostgres)
|
|
62
|
+
throw new Error('Static runtime gate passed, but postgres-free startup/recovery/network isolation was not executed');
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
requirePostgresRuntimeFree(manifest);
|
|
66
|
+
console.log(JSON.stringify({ runtime: manifest.runtime.length, migrationSource: manifest.migrationSource.length, rollbackOnly: manifest.rollbackOnly.length, legacyFallback: manifest.legacyFallback.length, runtimeDependencyHash: manifest.runtimeDependencyHash }, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (command === 'analyze' || command === 'all') {
|
|
70
|
+
const a = await analyze(root, resolve(cwd, cfg.schemaFile));
|
|
71
|
+
await writeJson(join(dir, 'analysis.json'), a);
|
|
72
|
+
await writeJson(join(dir, 'schema.json'), a.entities);
|
|
73
|
+
await writeJson(join(dir, 'dependency-graph.json'), a.dependencies);
|
|
74
|
+
await writeJson(join(dir, 'query-inventory.json'), a.findings.filter(x => x.kind === 'sql'));
|
|
75
|
+
await writeJson(join(dir, 'access-patterns.json'), a.findings);
|
|
76
|
+
}
|
|
77
|
+
if (command === 'classify' || command === 'all') {
|
|
78
|
+
const a = await read(dir, 'analysis.json');
|
|
79
|
+
let c = classify(a, cfg.confidenceThreshold ?? .8, cfg.classificationOverrides, cfg.blobThresholdBytes);
|
|
80
|
+
if (command === 'all') {
|
|
81
|
+
try {
|
|
82
|
+
const prior = await read(dir, 'classification.json');
|
|
83
|
+
if (prior.approved && prior.classificationHash === c.classificationHash)
|
|
84
|
+
c = { ...c, approved: true };
|
|
85
|
+
}
|
|
86
|
+
catch { /* first run */ }
|
|
87
|
+
}
|
|
88
|
+
await writeJson(join(dir, 'classification.json'), c);
|
|
89
|
+
if (c.entities.some(x => x.requiresApproval)) {
|
|
90
|
+
console.log('Classification review required. Add explicit overrides, then run review --approve.');
|
|
91
|
+
if (command === 'all')
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (command === 'review') {
|
|
96
|
+
const c = await read(dir, 'classification.json');
|
|
97
|
+
if (argv.includes('--state-contract')) {
|
|
98
|
+
const s = await read(dir, 'state-contract.json');
|
|
99
|
+
console.log(JSON.stringify({ domains: s.domains.length, legacyConsumers: s.legacyConsumers, unresolved: s.unresolved }, null, 2));
|
|
100
|
+
if (argv.includes('--approve') && s.unresolved.length)
|
|
101
|
+
throw new Error('Cannot approve an unresolved state contract');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
console.log(JSON.stringify(c.entities, null, 2));
|
|
105
|
+
if (argv.includes('--approve'))
|
|
106
|
+
await writeJson(join(dir, 'classification.json'), approve(c));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (command === 'all') {
|
|
110
|
+
const c = await read(dir, 'classification.json');
|
|
111
|
+
if (!c.approved) {
|
|
112
|
+
console.log('Pipeline paused at human classification approval gate. Run: felt-migrate review --approve');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (['capabilities', 'plan', 'generate', 'state-contract', 'ui-scan', 'ui-verify', 'prove', 'measure', 'benchmark', 'report', 'status', 'all'].includes(command)) {
|
|
117
|
+
const a = await read(dir, 'analysis.json'), c = await read(dir, 'classification.json');
|
|
118
|
+
if (!c.approved && command !== 'status')
|
|
119
|
+
throw new Error('Classification must be approved');
|
|
120
|
+
const model = domainModel(a, c);
|
|
121
|
+
if (command === 'capabilities' || command === 'all')
|
|
122
|
+
gate(cfg.capabilities ?? {}, cfg.requiredCapabilities);
|
|
123
|
+
if (command === 'plan' || command === 'all') {
|
|
124
|
+
const p = plan(a, c, cfg.tenantKeys);
|
|
125
|
+
await writeJson(join(dir, 'domain-model.json'), model);
|
|
126
|
+
await writeJson(join(dir, 'migration-plan.json'), p);
|
|
127
|
+
await writeJson(join(dir, 'migration-checkpoints.json'), p.steps.map(x => ({ stepId: x.id, status: 'pending', records: 0 })));
|
|
128
|
+
}
|
|
129
|
+
if (['state-contract', 'ui-scan', 'ui-verify', 'all'].includes(command)) {
|
|
130
|
+
const s = await discoverStateContract(a, cfg.stateDomains);
|
|
131
|
+
await writeJson(join(dir, 'state-contract.json'), s);
|
|
132
|
+
await writeJson(join(dir, 'state-consumption-graph.json'), s.consumers);
|
|
133
|
+
await writeJson(join(dir, 'ui-state-dependencies.json'), s.consumers);
|
|
134
|
+
if ((cfg.strictStateContract ?? true) && s.unresolved.length) {
|
|
135
|
+
console.log('Pipeline paused at state-contract review gate.');
|
|
136
|
+
if (command === 'all')
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let runtimeManifest;
|
|
141
|
+
if (command === 'all') {
|
|
142
|
+
if (!cfg.feltDbRuntime)
|
|
143
|
+
throw new Error('feltDbRuntime must define the deployment-mode-specific runtime contract');
|
|
144
|
+
runtimeManifest = await scanPostgresDependencies(root, cfg.feltDbRuntime, cfg.runtimeDependencyOverrides);
|
|
145
|
+
await writeJson(join(dir, 'postgres-dependencies.json'), runtimeManifest);
|
|
146
|
+
await writeJson(join(dir, 'postgres-config-migration-plan.json'), configurationMigrationPlan(runtimeManifest));
|
|
147
|
+
requirePostgresRuntimeFree(runtimeManifest);
|
|
148
|
+
}
|
|
149
|
+
if (command === 'generate' || command === 'all') {
|
|
150
|
+
const repos = generateRepositories(model);
|
|
151
|
+
for (const [name, code] of Object.entries(repos)) {
|
|
152
|
+
const file = join(dir, 'generated', 'repositories', name);
|
|
153
|
+
await mkdir(resolve(file, '..'), { recursive: true });
|
|
154
|
+
await writeFile(file, code);
|
|
155
|
+
}
|
|
156
|
+
await writeJson(join(dir, 'generated-files.json'), Object.keys(repos));
|
|
157
|
+
}
|
|
158
|
+
if (command === 'prove' || command === 'all') {
|
|
159
|
+
const s = await read(dir, 'state-contract.json'), repos = generateRepositories(model), checks = proveStatic(model, repos, s, cfg.strictStateContract ?? true);
|
|
160
|
+
await writeJson(join(dir, 'proof.json'), checks);
|
|
161
|
+
requireProof(checks);
|
|
162
|
+
}
|
|
163
|
+
if (command === 'measure' || command === 'report' || command === 'all') {
|
|
164
|
+
const s = await read(dir, 'state-contract.json'), metrics = measure(a, model, s, generateRepositories(model));
|
|
165
|
+
await writeJson(join(dir, 'measurements.json'), metrics);
|
|
166
|
+
if (command === 'report' || command === 'all') {
|
|
167
|
+
const checks = await read(dir, 'proof.json');
|
|
168
|
+
let rm = runtimeManifest, ri;
|
|
169
|
+
if (!rm)
|
|
170
|
+
try {
|
|
171
|
+
rm = await read(dir, 'postgres-dependencies.json');
|
|
172
|
+
}
|
|
173
|
+
catch { /* runtime scan not run */ }
|
|
174
|
+
try {
|
|
175
|
+
ri = await read(dir, 'runtime-dependency-proof.json');
|
|
176
|
+
}
|
|
177
|
+
catch { /* dynamic isolation not run */ }
|
|
178
|
+
await writeFile(join(dir, 'MIGRATION_REPORT.md'), report(metrics, checks, s, rm, ri));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (command === 'benchmark')
|
|
182
|
+
throw new Error('Benchmark requires application workload adapters; use the exported benchmark() API');
|
|
183
|
+
if (command === 'status') {
|
|
184
|
+
const state = c.approved ? 'APPROVED' : 'CLASSIFIED';
|
|
185
|
+
console.log(JSON.stringify({ readiness: state, schemaHash: a.schemaHash, classificationHash: c.classificationHash }, null, 2));
|
|
186
|
+
}
|
|
187
|
+
if (command === 'all') {
|
|
188
|
+
const p = await read(dir, 'migration-plan.json'), s = await read(dir, 'state-contract.json'), r = runtimeManifest;
|
|
189
|
+
await writeJson(join(dir, 'migration-manifest.json'), { engineVersion: '1.0.0', source: { type: 'postgresql', schemaHash: a.schemaHash }, target: { type: 'feltdb' }, classificationHash: c.classificationHash, planHash: p.planHash, stateContractHash: s.contractHash, runtimeDependencyHash: r.runtimeDependencyHash, generatedAt: new Date().toISOString(), entities: model.entities.length, state: model.entities.filter(x => x.classification === 'STATE').length, history: model.entities.filter(x => x.classification === 'HISTORY').length, relationships: model.entities.filter(x => x.classification === 'RELATIONSHIP').length, blobs: model.entities.filter(x => x.classification === 'BLOB_REFERENCE').length, secrets: model.entities.filter(x => x.classification === 'SECRET_REFERENCE').length, uiConsumers: s.consumers.length, legacyUiConsumers: s.legacyConsumers, postgres: { migrationSource: r.migrationSource.length > 0, rollbackAvailable: r.rollbackOnly.length > 0, runtimeDependency: false }, manifestHash: hash([a.schemaHash, c.classificationHash, p.planHash, s.contractHash, r.runtimeDependencyHash]) });
|
|
190
|
+
console.log('Safe generation pipeline complete. Data migration requires runtime source and target adapters.');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
throw new Error(`Unknown command: ${command}`);
|
|
195
|
+
}
|
|
196
|
+
if (argv.includes('--help') || argv.includes('-h') || command === 'help')
|
|
197
|
+
console.log(HELP);
|
|
198
|
+
else if (argv.includes('--version') || argv.includes('-v'))
|
|
199
|
+
console.log('0.4.3');
|
|
200
|
+
else
|
|
201
|
+
run().catch(e => { console.error(`felt-migrate: ${e instanceof Error ? e.message : String(e)}`); process.exitCode = 1; });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { hash } from '../util.js';
|
|
2
|
+
const secret = /(password|secret|token|credential|private_key|certificate|api_key)/i, blob = /(bytea|blob|binary|image|wasm|archive)/i;
|
|
3
|
+
export function domainModel(a, c) { if (!c.approved)
|
|
4
|
+
throw new Error('Classification is not approved'); const by = new Map(a.entities.map(e => [e.name, e])); const entities = c.entities.map(x => { const e = by.get(x.entity); return { ...x, authoritative: x.classification === 'STATE', target: x.entity.replace(/_([a-z])/g, (_, v) => v.toUpperCase()), secretColumns: e.columns.filter(v => secret.test(v.name)).map(v => v.name), blobColumns: e.columns.filter(v => blob.test(v.type) || blob.test(v.name)).map(v => v.name) }; }); return { version: 1, entities, hash: hash(entities) }; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const forbidden = /\b(SQL|JOIN|SELECT\s+.+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM|ORM|query\s*builder)\b/i;
|
|
2
|
+
export function generateRepositories(model) {
|
|
3
|
+
const output = {};
|
|
4
|
+
for (const e of model.entities) {
|
|
5
|
+
if (['REMOVE', 'OPERATIONAL', 'SECRET_REFERENCE', 'BLOB_REFERENCE'].includes(e.classification))
|
|
6
|
+
continue;
|
|
7
|
+
let methods = '';
|
|
8
|
+
if (e.classification === 'HISTORY')
|
|
9
|
+
methods = ` append(event) { return this.history.append('${e.target}', event); }\n list(query) { return this.history.list('${e.target}', query); }\n reconstruct(id) { return this.history.reconstruct('${e.target}', id); }`;
|
|
10
|
+
else if (e.classification === 'RELATIONSHIP')
|
|
11
|
+
methods = ` add(from, to) { return this.relationship.add('${e.target}', from, to); }\n remove(from, to) { return this.relationship.remove('${e.target}', from, to); }\n list(id) { return this.relationship.list('${e.target}', id); }`;
|
|
12
|
+
else
|
|
13
|
+
methods = ` get(id) { return this.state.get('${e.target}', id); }\n put(id, value) { return this.state.put('${e.target}', id, value); }\n update(id, patch) { return this.state.update('${e.target}', id, patch); }\n delete(id) { return this.state.delete('${e.target}', id); }\n find(query) { return this.state.find('${e.target}', query); }`;
|
|
14
|
+
const name = e.target[0].toUpperCase() + e.target.slice(1);
|
|
15
|
+
output[`${e.target}.repository.js`] = `// Generated by FeltDB Migration Sherpa\nexport class ${name}Repository {\n constructor(primitives) { Object.assign(this, primitives); }\n${methods}\n}\n`;
|
|
16
|
+
}
|
|
17
|
+
for (const [file, code] of Object.entries(output))
|
|
18
|
+
if (forbidden.test(code.replace(/^\/\/.*$/gm, '')))
|
|
19
|
+
throw new Error(`Generated repository ${file} contains a prohibited compatibility primitive`);
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './types/index.js';
|
|
2
|
+
export * from './analyzer/index.js';
|
|
3
|
+
export * from './classifier/index.js';
|
|
4
|
+
export * from './domain-model/index.js';
|
|
5
|
+
export * from './capability-gate/index.js';
|
|
6
|
+
export * from './planner/index.js';
|
|
7
|
+
export * from './state-contract/index.js';
|
|
8
|
+
export * from './ui-wiring/index.js';
|
|
9
|
+
export * from './runtime-dependencies/index.js';
|
|
10
|
+
export * from './generator/index.js';
|
|
11
|
+
export * from './migrator/index.js';
|
|
12
|
+
export * from './prover/index.js';
|
|
13
|
+
export * from './measurement/index.js';
|
|
14
|
+
export * from './benchmark/index.js';
|
|
15
|
+
export * from './reporter/index.js';
|
|
16
|
+
export * from './util.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './types/index.js';
|
|
2
|
+
export * from './analyzer/index.js';
|
|
3
|
+
export * from './classifier/index.js';
|
|
4
|
+
export * from './domain-model/index.js';
|
|
5
|
+
export * from './capability-gate/index.js';
|
|
6
|
+
export * from './planner/index.js';
|
|
7
|
+
export * from './state-contract/index.js';
|
|
8
|
+
export * from './ui-wiring/index.js';
|
|
9
|
+
export * from './runtime-dependencies/index.js';
|
|
10
|
+
export * from './generator/index.js';
|
|
11
|
+
export * from './migrator/index.js';
|
|
12
|
+
export * from './prover/index.js';
|
|
13
|
+
export * from './measurement/index.js';
|
|
14
|
+
export * from './benchmark/index.js';
|
|
15
|
+
export * from './reporter/index.js';
|
|
16
|
+
export * from './util.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Analysis, DomainModel, StateContract } from '../types/index.js';
|
|
2
|
+
export interface Measurements {
|
|
3
|
+
before: Record<string, number>;
|
|
4
|
+
after: Record<string, number>;
|
|
5
|
+
reductions: Record<string, number>;
|
|
6
|
+
}
|
|
7
|
+
export declare function measure(a: Analysis, model: DomainModel, contract: StateContract, repositories: Record<string, string>): Measurements;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function measure(a, model, contract, repositories) { const before = { postgresTables: a.entities.length, foreignKeys: a.entities.reduce((n, e) => n + (e.foreignKeys?.length ?? 0), 0), indexes: a.entities.reduce((n, e) => n + (e.indexes?.length ?? 0), 0), sqlConsumers: a.findings.filter(x => x.kind === 'sql').length, uiDatabaseConsumers: contract.consumers.length }; const after = { feltCollections: model.entities.filter(e => !['REMOVE', 'SECRET_REFERENCE', 'BLOB_REFERENCE'].includes(e.classification)).length, stateCollections: model.entities.filter(e => e.classification === 'STATE').length, historyCollections: model.entities.filter(e => e.classification === 'HISTORY').length, relationships: model.entities.filter(e => e.classification === 'RELATIONSHIP').length, blobReferences: model.entities.filter(e => e.classification === 'BLOB_REFERENCE').length, repositoryLOC: Object.values(repositories).reduce((n, x) => n + x.split('\n').length, 0), canonicalUiConsumers: contract.canonicalConsumers, legacyUiConsumers: contract.legacyConsumers }; const reductions = { postgresTables: before.postgresTables, foreignKeys: before.foreignKeys, sqlConsumers: before.sqlConsumers, legacyUiConsumers: before.uiDatabaseConsumers - after.legacyUiConsumers }; return { before, after, reductions }; }
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { Checkpoint, DomainModel, MigrationAdapter, MigrationPlan, MigrationTelemetry, SourceAdapter } from '../types/index.js';
|
|
2
|
+
export declare function migrate(plan: MigrationPlan, model: DomainModel, source: SourceAdapter, target: MigrationAdapter, previous?: Checkpoint[], tenant?: string, onTelemetry?: (x: MigrationTelemetry) => void): Promise<Checkpoint[]>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export async function migrate(plan, model, source, target, previous = [], tenant, onTelemetry) { const checkpoints = new Map(previous.map(x => [x.stepId, x])); const by = new Map(model.entities.map(x => [x.entity, x])); for (const step of plan.steps) {
|
|
2
|
+
const old = checkpoints.get(step.id);
|
|
3
|
+
if (old?.status === 'complete')
|
|
4
|
+
continue;
|
|
5
|
+
const cp = { stepId: step.id, status: 'running', cursor: old?.cursor, records: old?.records ?? 0 };
|
|
6
|
+
checkpoints.set(step.id, cp);
|
|
7
|
+
const started = Date.now();
|
|
8
|
+
try {
|
|
9
|
+
await target.transaction(async () => { for await (const record of source.read(step.entity, cp.cursor, tenant)) {
|
|
10
|
+
const d = by.get(step.entity);
|
|
11
|
+
const clean = { ...record.value };
|
|
12
|
+
for (const key of [...d.secretColumns, ...d.blobColumns])
|
|
13
|
+
delete clean[key];
|
|
14
|
+
const options = { idempotencyKey: `${plan.planHash}:${step.id}:${record.id}`, tenant };
|
|
15
|
+
if (step.classification === 'HISTORY')
|
|
16
|
+
await target.append(d.target, record.id, clean, options);
|
|
17
|
+
else if (step.classification === 'RELATIONSHIP') {
|
|
18
|
+
const values = Object.values(clean).filter(v => typeof v === 'string');
|
|
19
|
+
if (values.length >= 2)
|
|
20
|
+
await target.relate(d.target, values[0], values[1], options);
|
|
21
|
+
}
|
|
22
|
+
else if (!['REMOVE', 'SECRET_REFERENCE', 'BLOB_REFERENCE'].includes(step.classification))
|
|
23
|
+
await target.put(d.target, record.id, clean, options);
|
|
24
|
+
cp.cursor = record.cursor;
|
|
25
|
+
cp.records++;
|
|
26
|
+
} });
|
|
27
|
+
cp.status = 'complete';
|
|
28
|
+
onTelemetry?.({ timestamp: new Date().toISOString(), stepId: step.id, operation: 'migrate', records: cp.records, durationMs: Date.now() - started, tenant });
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
cp.status = 'failed';
|
|
32
|
+
cp.error = e instanceof Error ? e.message : String(e);
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
} return plan.steps.map(s => checkpoints.get(s.id) ?? { stepId: s.id, status: 'pending', records: 0 }); }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { hash } from '../util.js';
|
|
2
|
+
export function plan(a, c, tenantKeys = {}) { if (!c.approved)
|
|
3
|
+
throw new Error('Classification is not approved'); const deps = new Map(a.entities.map(e => [e.name, new Set((e.foreignKeys ?? []).map(f => f.referencedTable))])); const done = new Set(), steps = []; while (done.size < a.entities.length) {
|
|
4
|
+
const ready = [...deps].filter(([n, d]) => !done.has(n) && [...d].every(x => done.has(x) || !deps.has(x))).map(([n]) => n).sort();
|
|
5
|
+
if (!ready.length)
|
|
6
|
+
throw new Error(`Dependency cycle: ${[...deps.keys()].filter(x => !done.has(x)).join(', ')}`);
|
|
7
|
+
for (const n of ready) {
|
|
8
|
+
done.add(n);
|
|
9
|
+
const ce = c.entities.find(x => x.entity === n);
|
|
10
|
+
steps.push({ id: `${String(steps.length + 1).padStart(4, '0')}-${n}`, entity: n, classification: ce.classification, dependsOn: [...deps.get(n)].sort(), order: steps.length, tenantKey: tenantKeys[n] });
|
|
11
|
+
}
|
|
12
|
+
} const core = { version: 1, steps }; return { ...core, planHash: hash(core) }; }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DomainModel, StateContract } from '../types/index.js';
|
|
2
|
+
export interface ProofCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
status: 'pass' | 'fail';
|
|
5
|
+
measured: number;
|
|
6
|
+
details?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function proveStatic(model: DomainModel, repositories: Record<string, string>, contract: StateContract, strict?: boolean): ProofCheck[];
|
|
9
|
+
export declare function requireProof(checks: ProofCheck[]): void;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function proveStatic(model, repositories, contract, strict = true) { const code = Object.values(repositories).join('\n'); const checks = [{ name: 'generated_sql', status: /\b(SELECT|INSERT|JOIN|query builder)\b/i.test(code) ? 'fail' : 'pass', measured: (code.match(/\b(SELECT|INSERT|JOIN)\b/gi) ?? []).length }, { name: 'secret_payload_fields', status: model.entities.every(e => e.classification === 'SECRET_REFERENCE' || e.secretColumns.every(c => !code.includes(c))) ? 'pass' : 'fail', measured: model.entities.reduce((n, e) => n + e.secretColumns.length, 0) }, { name: 'blob_payload_fields', status: model.entities.every(e => e.classification === 'BLOB_REFERENCE' || e.blobColumns.every(c => !code.includes(c))) ? 'pass' : 'fail', measured: model.entities.reduce((n, e) => n + e.blobColumns.length, 0) }, { name: 'state_contract', status: (!strict || contract.unresolved.length === 0) ? 'pass' : 'fail', measured: contract.unresolved.length, details: contract.unresolved.join(', ') }]; return checks; }
|
|
2
|
+
export function requireProof(checks) { const failed = checks.filter(x => x.status === 'fail'); if (failed.length)
|
|
3
|
+
throw new Error(`Semantic proof failed: ${failed.map(x => x.name).join(', ')}`); }
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Measurements } from '../measurement/index.js';
|
|
2
|
+
import type { ProofCheck } from '../prover/index.js';
|
|
3
|
+
import type { PostgresDependencyManifest, RuntimeDependencyProof, StateContract } from '../types/index.js';
|
|
4
|
+
export declare function report(measurements: Measurements, proof: ProofCheck[], contract: StateContract, runtime?: PostgresDependencyManifest, isolation?: RuntimeDependencyProof): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function report(measurements, proof, contract, runtime, isolation) { const passed = proof.filter(x => x.status === 'pass').length, runtimeLines = runtime ? `\n## PostgreSQL runtime cutover\n\n- Runtime dependencies: ${runtime.runtime.length}\n- Migration-source dependencies: ${runtime.migrationSource.length}\n- Rollback-only dependencies: ${runtime.rollbackOnly.length}\n- Legacy fallbacks: ${runtime.legacyFallback.length}\n- FeltDB runtime mode: ${runtime.feltDbRuntime.mode}\n- Postgres-free startup: ${isolation?.startupVerified ? 'PASS' : 'NOT MEASURED'}\n- Postgres-free recovery: ${isolation?.recoveryVerified ? 'PASS' : 'NOT MEASURED'}\n- Network isolation: ${isolation?.networkIsolationVerified ? 'PASS' : 'NOT MEASURED'}\n` : ''; return `# FeltDB Migration Sherpa Report\n\nGenerated: ${new Date().toISOString()}\n\n## Measured result\n\n- PostgreSQL entities discovered: ${measurements.before.postgresTables}\n- FeltDB-native collections generated: ${measurements.after.feltCollections}\n- Foreign keys eliminated from generated model: ${measurements.reductions.foreignKeys}\n- Canonical state domains: ${contract.domains.length}\n- Canonical UI consumers: ${contract.canonicalConsumers}\n- Remaining legacy UI consumers: ${contract.legacyConsumers}\n- Semantic proof checks: ${passed}/${proof.length} PASS\n${runtimeLines}\nNo unmeasured performance or reduction claims are included.\n`; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DependencyClassification, FeltDBRuntimeContract, PostgresDependencyManifest, RuntimeDependencyProof } from '../types/index.js';
|
|
2
|
+
export declare function scanPostgresDependencies(sourceRoot: string, feltDbRuntime: FeltDBRuntimeContract, overrides?: Record<string, DependencyClassification>): Promise<PostgresDependencyManifest>;
|
|
3
|
+
export declare function requirePostgresRuntimeFree(manifest: PostgresDependencyManifest): void;
|
|
4
|
+
export declare function configurationMigrationPlan(manifest: PostgresDependencyManifest): Array<Record<string, unknown>>;
|
|
5
|
+
export interface PostgresIsolationHarness {
|
|
6
|
+
startWithPostgresBlocked(): Promise<void>;
|
|
7
|
+
executeWorkflow(): Promise<void>;
|
|
8
|
+
restart(): Promise<void>;
|
|
9
|
+
verifyRecovery(): Promise<void>;
|
|
10
|
+
postgresConnectionAttempts(): Promise<number>;
|
|
11
|
+
cleanup?(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function verifyPostgresIsolation(manifest: PostgresDependencyManifest, harness?: PostgresIsolationHarness): Promise<RuntimeDependencyProof>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, relative } from 'node:path';
|
|
3
|
+
import { hash } from '../util.js';
|
|
4
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'target', '.feltdb', 'coverage']);
|
|
5
|
+
const ENV = /^(?:export\s+)?(POSTGRES_(?:HOST|PORT|DB|USER|PASSWORD)|PG(?:HOST|PORT|DATABASE|USER|PASSWORD)|DATABASE_(?:URL|HOST|PORT|NAME|USER|PASSWORD))\s*[:=]/i;
|
|
6
|
+
const CLIENT = /\b(?:@neondatabase\/serverless|node-postgres|tokio-postgres|postgres\.js|pglite|@electric-sql\/pglite|sqlx|diesel|knex|typeorm|prisma|drizzle-orm|require\(['"]pg['"]\)|from\s+['"]pg['"])/i;
|
|
7
|
+
const CONNECTION = /\b(?:new\s+(?:Pool|Client)\s*\(|createPool\s*\(|postgres\s*\(|connect\s*\(.*postgres)/i;
|
|
8
|
+
const SQL = /\b(?:SELECT\s+.+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM|JOIN\s+\w+|CREATE\s+(?:TABLE|INDEX)|ALTER\s+TABLE)\b/i;
|
|
9
|
+
const STARTUP = /(?:wait-for-postgres|dockerize\s+-wait\s+tcp:\/\/(?:postgres|[^\s:]+):5432|pg_isready|psql\s)/i;
|
|
10
|
+
const HEALTH = /(?:pg_isready|postgres(?:ql)?[^\n]{0,30}health|health[^\n]{0,30}(?:postgres|5432))/i;
|
|
11
|
+
const IMAGE = /(?:image\s*:\s*postgres(?:ql)?(?::|\s|$)|container.*postgres|services?\s*:\s*postgres)/i;
|
|
12
|
+
const DEPLOY = /(?:depends_on[^\n]*postgres|postgres\s*:\s*(?:condition|required)|(?:host|hostname)\s*:\s*postgres|tcp:\/\/postgres:5432)/i;
|
|
13
|
+
const TEXT = /(?:^|\/)(?:Dockerfile[^/]*|[^/]*\.(?:[cm]?[jt]sx?|py|rb|go|rs|sql|ya?ml|toml|json|env|example|md|sh)|\.env[^/]*)$/i;
|
|
14
|
+
async function walk(root, dir = root, out = []) { for (const n of await readdir(dir)) {
|
|
15
|
+
if (SKIP.has(n))
|
|
16
|
+
continue;
|
|
17
|
+
const p = join(dir, n), s = await stat(p);
|
|
18
|
+
if (s.isDirectory())
|
|
19
|
+
await walk(root, p, out);
|
|
20
|
+
else if (TEXT.test(p))
|
|
21
|
+
out.push(p);
|
|
22
|
+
} return out.sort(); }
|
|
23
|
+
function context(file, line) { const p = file.toLowerCase(); if (/(?:^|\/)(?:migrations?|migration-sherpa|feltdb-migration)(?:\/|\.|$)/.test(p))
|
|
24
|
+
return 'MIGRATION_SOURCE'; if (/(?:^|\/)(?:rollback|backout)(?:\/|\.|$)/.test(p) || /rollback[-_ ]only/i.test(line))
|
|
25
|
+
return 'ROLLBACK_ONLY'; if (/(?:^|\/)(?:legacy|fallback)(?:\/|\.|$)/.test(p) || /legacy[-_ ]fallback/i.test(line))
|
|
26
|
+
return 'LEGACY_FALLBACK'; if (/(?:^|\/)(?:test|tests|e2e|fixtures?|__tests__)(?:\/|\.|$)|(?:\.|_)(?:test|tests|spec|acceptance_tests)\./.test(p))
|
|
27
|
+
return 'TEST_ONLY'; if (/(?:^|\/)(?:dev|development|examples?)(?:\/|\.|$)|compose\.dev/.test(p))
|
|
28
|
+
return 'DEVELOPMENT_ONLY'; if (/(?:^|\/)(?:docs?|ops)(?:\/|$)|readme|\.md$|\.github\/workflows\/build|(?:baseline|results|report|metrics)[^/]*\.json$|(?:^|\/)persistence_[^/]*\.json$/.test(p))
|
|
29
|
+
return 'BUILD_ONLY'; return 'RUNTIME'; }
|
|
30
|
+
function redact(line) { return line.replace(/^((?:export\s+)?(?:POSTGRES_|PG|DATABASE_)[A-Z_]+\s*[:=]\s*).*/i, '$1<redacted>').trim().slice(0, 300); }
|
|
31
|
+
function action(code) { return code === 'POSTGRES_RUNTIME_ENV' ? 'Remove or replace with the configured FeltDB runtime contract.' : code === 'POSTGRES_RUNTIME_HEALTHCHECK' ? 'Replace with a FeltDB health/readiness check.' : 'Remove from the application runtime or explicitly classify it as migration, rollback, test, development, build, or fallback infrastructure.'; }
|
|
32
|
+
export async function scanPostgresDependencies(sourceRoot, feltDbRuntime, overrides = {}) {
|
|
33
|
+
const findings = [];
|
|
34
|
+
for (const absolute of await walk(sourceRoot)) {
|
|
35
|
+
const file = relative(sourceRoot, absolute), content = await readFile(absolute, 'utf8');
|
|
36
|
+
const isManifest = basename(file) === 'package.json', isDeployment = /(?:docker|compose|kubernetes|k8s|helm|deploy|\.github\/workflows|fly\.toml)/i.test(file);
|
|
37
|
+
let packageJson = {};
|
|
38
|
+
if (isManifest)
|
|
39
|
+
try {
|
|
40
|
+
packageJson = JSON.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch { /* scan malformed JSON as text */ }
|
|
43
|
+
for (const [i, line] of content.split(/\r?\n/).entries()) {
|
|
44
|
+
const matches = [];
|
|
45
|
+
const env = line.match(ENV), comment = /^\s*(?:\/\/|#(?!\!)|\/\*|\*)/.test(line);
|
|
46
|
+
if (env) {
|
|
47
|
+
const generic = env[1].toUpperCase().startsWith('DATABASE_');
|
|
48
|
+
const postgresValue = /(?:postgres(?:ql)?:\/\/|\bpostgres\b|\b5432\b)/i.test(line);
|
|
49
|
+
if (!generic || postgresValue || CLIENT.test(content))
|
|
50
|
+
matches.push([postgresValue ? 'POSTGRES_RUNTIME_URL' : 'POSTGRES_RUNTIME_ENV', 'environment']);
|
|
51
|
+
}
|
|
52
|
+
if (CLIENT.test(line) && !comment)
|
|
53
|
+
matches.push(['POSTGRES_RUNTIME_CLIENT', 'connection']);
|
|
54
|
+
if (CONNECTION.test(line) && !comment && CLIENT.test(content))
|
|
55
|
+
matches.push(['POSTGRES_RUNTIME_CONNECTION', 'connection']);
|
|
56
|
+
if (SQL.test(line) && !comment && (/\.sql$/i.test(file) || CLIENT.test(content) || /(?:query|execute|raw)\s*\(/i.test(line)))
|
|
57
|
+
matches.push(['POSTGRES_RUNTIME_SQL', 'sql']);
|
|
58
|
+
if (STARTUP.test(line) && !comment)
|
|
59
|
+
matches.push(['POSTGRES_RUNTIME_STARTUP_DEPENDENCY', 'startup']);
|
|
60
|
+
if (HEALTH.test(line) && !comment)
|
|
61
|
+
matches.push(['POSTGRES_RUNTIME_HEALTHCHECK', 'healthcheck']);
|
|
62
|
+
if ((IMAGE.test(line) || (isDeployment && /^\s+postgres\s*:\s*$/.test(line))) && !comment)
|
|
63
|
+
matches.push(['POSTGRES_RUNTIME_CONTAINER', 'container']);
|
|
64
|
+
if ((DEPLOY.test(line) || (isDeployment && /^\s*-\s*postgres\s*$/.test(line))) && !comment)
|
|
65
|
+
matches.push(['POSTGRES_RUNTIME_DEPLOYMENT_DEPENDENCY', 'deployment']);
|
|
66
|
+
for (const [code, kind] of matches) {
|
|
67
|
+
const key = `${file}:${i + 1}`, base = context(file, line);
|
|
68
|
+
let classification = overrides[key] ?? overrides[file] ?? base;
|
|
69
|
+
if (/pglite/i.test(line) && classification === 'RUNTIME' && /(legacy|fallback)/i.test(content))
|
|
70
|
+
classification = 'LEGACY_FALLBACK';
|
|
71
|
+
const runtime = classification === 'RUNTIME';
|
|
72
|
+
findings.push({ id: hash([file, i + 1, code, line]).slice(0, 16), code, file, line: i + 1, classification, runtime, kind, evidence: redact(line), action: action(code) });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isManifest)
|
|
76
|
+
for (const section of ['dependencies', 'devDependencies'])
|
|
77
|
+
for (const [name] of Object.entries(packageJson[section] ?? {})) {
|
|
78
|
+
if (!CLIENT.test(name))
|
|
79
|
+
continue;
|
|
80
|
+
const key = `${file}:${section}:${name}`, classification = overrides[key] ?? overrides[file] ?? (section === 'devDependencies' ? 'BUILD_ONLY' : context(file, name));
|
|
81
|
+
findings.push({ id: hash(key).slice(0, 16), code: 'POSTGRES_RUNTIME_CLIENT', file, line: 1, classification, runtime: classification === 'RUNTIME', kind: 'dependency', evidence: `${section}.${name}`, action: action('POSTGRES_RUNTIME_CLIENT') });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const unique = [...new Map(findings.map(f => [`${f.file}:${f.line}:${f.code}:${f.evidence}`, f])).values()].sort((a, b) => `${a.file}:${a.line}:${a.code}`.localeCompare(`${b.file}:${b.line}:${b.code}`));
|
|
85
|
+
const group = (c) => unique.filter(f => f.classification === c);
|
|
86
|
+
const core = { version: 1, generatedAt: new Date().toISOString(), sourceRoot, findings: unique, runtime: group('RUNTIME'), migrationSource: group('MIGRATION_SOURCE'), legacyFallback: group('LEGACY_FALLBACK'), testOnly: group('TEST_ONLY'), developmentOnly: group('DEVELOPMENT_ONLY'), buildOnly: group('BUILD_ONLY'), rollbackOnly: group('ROLLBACK_ONLY'), feltDbRuntime };
|
|
87
|
+
return { ...core, runtimeDependencyHash: hash({ ...core, generatedAt: undefined }) };
|
|
88
|
+
}
|
|
89
|
+
export function requirePostgresRuntimeFree(manifest) { if (manifest.runtime.length) {
|
|
90
|
+
const f = manifest.runtime[0];
|
|
91
|
+
throw new Error(`FELTDB CUTOVER BLOCKED: ${f.code} at ${f.file}:${f.line} (${manifest.runtime.length} runtime finding${manifest.runtime.length === 1 ? '' : 's'})`);
|
|
92
|
+
} }
|
|
93
|
+
export function configurationMigrationPlan(manifest) {
|
|
94
|
+
const removals = manifest.findings.filter(f => f.kind === 'environment' && f.runtime).map(f => ({ change: 'REMOVE_RUNTIME_CONFIG', file: f.file, line: f.line, reason: 'FeltDB-native runtime', findingId: f.id, requiresApproval: true }));
|
|
95
|
+
const additions = manifest.feltDbRuntime.requiredEnvironment.map(key => ({ change: 'ADD_FELTDB_CONFIG', file: '.env.example', key, reason: `FeltDB ${manifest.feltDbRuntime.mode} runtime`, requiresApproval: true }));
|
|
96
|
+
return [...removals, ...additions];
|
|
97
|
+
}
|
|
98
|
+
export async function verifyPostgresIsolation(manifest, harness) { requirePostgresRuntimeFree(manifest); let startup = false, recovery = false, network = false; if (harness)
|
|
99
|
+
try {
|
|
100
|
+
await harness.startWithPostgresBlocked();
|
|
101
|
+
startup = true;
|
|
102
|
+
await harness.executeWorkflow();
|
|
103
|
+
await harness.restart();
|
|
104
|
+
await harness.verifyRecovery();
|
|
105
|
+
recovery = true;
|
|
106
|
+
network = (await harness.postgresConnectionAttempts()) === 0;
|
|
107
|
+
if (!network)
|
|
108
|
+
throw new Error('PostgreSQL connection attempted during network isolation');
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await harness.cleanup?.();
|
|
112
|
+
} const count = (code) => manifest.runtime.filter(f => f.code === code).length; return { postgresRuntimeDependency: false, postgresConnections: count('POSTGRES_RUNTIME_CONNECTION') + count('POSTGRES_RUNTIME_CLIENT'), postgresSqlExecutions: count('POSTGRES_RUNTIME_SQL'), postgresEnvironmentVariables: count('POSTGRES_RUNTIME_ENV') + count('POSTGRES_RUNTIME_URL'), postgresStartupDependencies: count('POSTGRES_RUNTIME_STARTUP_DEPENDENCY'), postgresHealthChecks: count('POSTGRES_RUNTIME_HEALTHCHECK'), postgresRuntimeContainers: count('POSTGRES_RUNTIME_CONTAINER'), postgresRuntimeDeploymentReferences: count('POSTGRES_RUNTIME_DEPLOYMENT_DEPENDENCY'), feltDbRuntime: manifest.feltDbRuntime.requiredEnvironment.length > 0 || manifest.feltDbRuntime.mode === 'browser', verifiedWithoutPostgres: startup && recovery && network, startupVerified: startup, recoveryVerified: recovery, networkIsolationVerified: network, runtimeDependencyHash: manifest.runtimeDependencyHash }; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Analysis, StateContract, StateDomain } from '../types/index.js';
|
|
2
|
+
export declare function discoverStateContract(a: Analysis, configured?: Array<Partial<StateDomain> & {
|
|
3
|
+
name: string;
|
|
4
|
+
authoritativeState: string;
|
|
5
|
+
}>): Promise<StateContract>;
|
|
6
|
+
export declare function verifyObservableState(contract: StateContract, domain: string, input: {
|
|
7
|
+
status?: string;
|
|
8
|
+
currentAction?: string;
|
|
9
|
+
history?: unknown[];
|
|
10
|
+
}): {
|
|
11
|
+
active: boolean;
|
|
12
|
+
empty: boolean;
|
|
13
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { hash } from '../util.js';
|
|
4
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'target', '.feltdb']);
|
|
5
|
+
async function walk(root, dir = root, out = []) { for (const n of await readdir(dir)) {
|
|
6
|
+
if (SKIP.has(n))
|
|
7
|
+
continue;
|
|
8
|
+
const p = join(dir, n), s = await stat(p);
|
|
9
|
+
if (s.isDirectory())
|
|
10
|
+
await walk(root, p, out);
|
|
11
|
+
else if (/\.[cm]?[jt]sx?$/.test(n))
|
|
12
|
+
out.push(p);
|
|
13
|
+
} return out.sort(); }
|
|
14
|
+
function semantic(line) { if (/current.?action/i.test(line))
|
|
15
|
+
return 'currentAction'; if (/status/i.test(line))
|
|
16
|
+
return 'status'; if (/plans?/i.test(line))
|
|
17
|
+
return 'plan'; if (/results?/i.test(line))
|
|
18
|
+
return 'result'; if (/activit|events?|history/i.test(line))
|
|
19
|
+
return 'activity'; return 'state'; }
|
|
20
|
+
export async function discoverStateContract(a, configured = []) {
|
|
21
|
+
const consumers = [];
|
|
22
|
+
for (const file of await walk(a.sourceRoot)) {
|
|
23
|
+
const text = await readFile(file, 'utf8');
|
|
24
|
+
for (const [i, line] of text.split(/\r?\n/).entries()) {
|
|
25
|
+
if (!/(fetch\(|axios|useQuery|useSWR|poll|subscribe|\.sql\b|SELECT\s|postgres|pg\.)/i.test(line))
|
|
26
|
+
continue;
|
|
27
|
+
const canonical = /feltdb|canonical|stateContract/i.test(line);
|
|
28
|
+
const endpoint = line.match(/['"`]([^'"`]*\/api\/[^'"`]*)['"`]/)?.[1];
|
|
29
|
+
consumers.push({ id: hash([relative(a.sourceRoot, file), i + 1, line]).slice(0, 16), file: relative(a.sourceRoot, file), line: i + 1, surface: file.split('/').pop(), semantic: semantic(line), source: canonical ? 'canonical' : /sql|select|postgres|pg\.|\/api\//i.test(line) ? 'legacy' : 'unknown', endpoint });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const domains = configured.map(d => ({ name: d.name, authoritativeState: d.authoritativeState, history: d.history, fields: d.fields ?? {}, uiSurfaces: d.uiSurfaces ?? consumers.map(c => c.surface), lifecycleStates: d.lifecycleStates ?? ['NEW', 'ACTIVE', 'PAUSED', 'COMPLETED', 'FAILED'], emptyWhen: d.emptyWhen ?? 'authoritative state is absent and history is empty', refresh: d.refresh ?? 'subscription', tenantKey: d.tenantKey }));
|
|
33
|
+
const unresolved = [];
|
|
34
|
+
for (const d of domains) {
|
|
35
|
+
for (const sem of ['status', 'currentAction', 'plan', 'activity']) {
|
|
36
|
+
const matching = consumers.filter(c => c.semantic === sem);
|
|
37
|
+
if (new Set(matching.map(c => c.source)).size > 1)
|
|
38
|
+
unresolved.push(`STATE_CONTRACT_MISMATCH:${d.name}:${sem}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!domains.length)
|
|
42
|
+
unresolved.push('No canonical state domains configured');
|
|
43
|
+
consumers.filter(c => c.source !== 'canonical').forEach(c => unresolved.push(`LEGACY_CONSUMER:${c.file}:${c.line}`));
|
|
44
|
+
const core = { contractVersion: '1.0', generatedAt: new Date().toISOString(), domains, consumers, legacyConsumers: consumers.filter(c => c.source === 'legacy').length, canonicalConsumers: consumers.filter(c => c.source === 'canonical').length, unresolved };
|
|
45
|
+
return { ...core, contractHash: hash({ ...core, generatedAt: undefined }) };
|
|
46
|
+
}
|
|
47
|
+
export function verifyObservableState(contract, domain, input) { const d = contract.domains.find(x => x.name === domain); if (!d)
|
|
48
|
+
throw new Error(`Unknown state domain: ${domain}`); const active = !!input.status && !['NEW', 'EMPTY', 'COMPLETED', 'FAILED'].includes(input.status); const empty = !input.status && !(input.history?.length); if (active && empty)
|
|
49
|
+
throw new Error(`STATE_CONTRACT_MISMATCH:${domain}: active state rendered empty`); return { active, empty }; }
|