@feltdb/core 0.8.0 → 0.8.2

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 (85) hide show
  1. package/dist/application-development.d.ts +7 -2
  2. package/dist/application-development.d.ts.map +1 -1
  3. package/dist/application-development.js +7 -13
  4. package/dist/application-manifest.d.ts +1 -0
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +1 -1
  7. package/dist/application-understanding.d.ts +76 -0
  8. package/dist/application-understanding.d.ts.map +1 -0
  9. package/dist/application-understanding.js +282 -0
  10. package/dist/cli/ai-model-config.js +3 -3
  11. package/dist/cli/commands.js +131 -38
  12. package/dist/cli/proposal-client.js +5 -1
  13. package/dist/cli/publish-engine.js +1 -1
  14. package/dist/cli/repository-evidence.js +55 -0
  15. package/dist/cli/source-sync.js +190 -75
  16. package/dist/convergence.d.ts +55 -0
  17. package/dist/convergence.d.ts.map +1 -0
  18. package/dist/convergence.js +63 -0
  19. package/dist/create/managed-account.js +1 -0
  20. package/dist/create/package-versions.js +1 -1
  21. package/dist/create/server-source/crates/feltdb/src/application.rs +23 -0
  22. package/dist/create/server-source/crates/feltdb/src/authority.rs +349 -0
  23. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +32 -0
  24. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +19 -0
  25. package/dist/create/server-source/crates/feltdb/src/lib.rs +444 -189
  26. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +244 -51
  27. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +297 -0
  28. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +41 -11
  29. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +14 -0
  30. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +8 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1262 -173
  32. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +142 -1
  33. package/dist/create/template/default-project/src/context/AuthContext.tsx +9 -1
  34. package/dist/create/template/default-project/src/pages/SignUp.tsx +1 -1
  35. package/dist/flowspec.d.ts.map +1 -1
  36. package/dist/flowspec.js +6 -1
  37. package/dist/http-client.d.ts +32 -1
  38. package/dist/http-client.d.ts.map +1 -1
  39. package/dist/http-client.js +14 -2
  40. package/dist/http-db.d.ts +6 -0
  41. package/dist/http-db.d.ts.map +1 -1
  42. package/dist/http-db.js +30 -4
  43. package/dist/index-core.d.ts +3 -0
  44. package/dist/index-core.d.ts.map +1 -1
  45. package/dist/index-core.js +3 -0
  46. package/dist/intent.d.ts +98 -0
  47. package/dist/intent.d.ts.map +1 -0
  48. package/dist/intent.js +343 -0
  49. package/dist/module-intent.d.ts +19 -5
  50. package/dist/module-intent.d.ts.map +1 -1
  51. package/dist/module-intent.js +211 -62
  52. package/dist/module.d.ts +41 -12
  53. package/dist/module.d.ts.map +1 -1
  54. package/dist/module.js +543 -38
  55. package/dist/proposal.d.ts +13 -1
  56. package/dist/proposal.d.ts.map +1 -1
  57. package/dist/proposal.js +21 -4
  58. package/dist/state-contract.d.ts +4 -1
  59. package/dist/state-contract.d.ts.map +1 -1
  60. package/dist/state-contract.js +1 -1
  61. package/dist/studio/components/AskFeltDB.d.ts.map +1 -1
  62. package/dist/studio/components/ProposalBrowser.d.ts +1 -1
  63. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -1
  64. package/dist/studio/components/index.js +1 -1
  65. package/dist/studio/{components-CNSNrmA9.js → components-Duq2xQWt.js} +251 -193
  66. package/dist/studio/index.js +1 -1
  67. package/dist/studio/studio.css +1 -1
  68. package/dist/studio/utils/proposal-api.d.ts +4 -1
  69. package/dist/studio/utils/proposal-api.d.ts.map +1 -1
  70. package/dist/studio/utils/proposal-api.js +3 -0
  71. package/dist/studio-app/assets/{feltdb_wasm-CSB6KVgw.js → feltdb_wasm-DB8cX151.js} +1 -1
  72. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  73. package/dist/studio-app/assets/index-B0k4UAlI.js +29 -0
  74. package/dist/studio-app/assets/{index-yazaWMUN.css → index-Cd_NPw14.css} +1 -1
  75. package/dist/studio-app/index.html +2 -2
  76. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  77. package/dist/workspace/local-development-authority.d.ts +2 -0
  78. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  79. package/dist/workspace/local-development-authority.js +65 -2
  80. package/package.json +1 -1
  81. package/dist/studio-app/assets/dist-CCOk39Uc.js +0 -1
  82. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  83. package/dist/studio-app/assets/index-Bncji3aN.js +0 -43
  84. package/dist/studio-app/assets/lib-B2_7fcn7.js +0 -69
  85. package/dist/studio-app/assets/worker-CARZ5g7K.js +0 -69
@@ -1,58 +1,139 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { spawnSync } from 'node:child_process';
5
- import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, } from '@feltdb/core';
6
- import { parseFlowSpec } from '@feltdb/core';
7
- const FLOW_FILE = 'feltdb.flow';
8
- const GENERATED_FILE = 'src/feltdb/generated-contract.ts';
9
- const SYNC_STATE = '.feltdb/source-sync.json';
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, resolveFeltDBModule, } from "@feltdb/core";
6
+ import { parseFlowSpec } from "@feltdb/core";
7
+ const FLOW_FILE = "feltdb.flow";
8
+ const GENERATED_FILE = "src/feltdb/generated-contract.ts";
9
+ const SYNC_STATE = ".feltdb/source-sync.json";
10
10
  function digest(value) {
11
- return `sha256:${createHash('sha256').update(value).digest('hex')}`;
11
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
12
12
  }
13
13
  function readJson(file) {
14
- return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : undefined;
14
+ return fs.existsSync(file)
15
+ ? JSON.parse(fs.readFileSync(file, "utf8"))
16
+ : undefined;
15
17
  }
16
18
  function gitDirty(cwd, files) {
17
- const result = spawnSync('git', ['status', '--porcelain', '--', ...files], { cwd, encoding: 'utf8' });
19
+ const result = spawnSync("git", ["status", "--porcelain", "--", ...files], {
20
+ cwd,
21
+ encoding: "utf8",
22
+ });
18
23
  if (result.status !== 0)
19
24
  return [];
20
- return result.stdout.split(/\r?\n/).filter(Boolean).map(line => line.slice(3));
25
+ return result.stdout
26
+ .split(/\r?\n/)
27
+ .filter(Boolean)
28
+ .map((line) => line.slice(3));
21
29
  }
22
30
  export async function repositoryContext(cwd = process.cwd(), plannedFiles = [FLOW_FILE, GENERATED_FILE]) {
23
- const git = (args) => spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
24
- const rootResult = git(['rev-parse', '--show-toplevel']);
31
+ const git = (args) => spawnSync("git", args, {
32
+ cwd,
33
+ encoding: "utf8",
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ });
36
+ const rootResult = git(["rev-parse", "--show-toplevel"]);
25
37
  const root = rootResult.status === 0 ? rootResult.stdout.trim() : path.resolve(cwd);
26
- const branchResult = git(['branch', '--show-current']);
27
- const commitResult = git(['rev-parse', 'HEAD']);
38
+ const branchResult = git(["branch", "--show-current"]);
39
+ const commitResult = git(["rev-parse", "HEAD"]);
28
40
  const context = await loadLocalDevelopmentContract(root);
29
- return { root, branch: branchResult.status === 0 ? branchResult.stdout.trim() || undefined : undefined,
30
- commit: commitResult.status === 0 ? commitResult.stdout.trim() : undefined, dirty: gitDirty(root, plannedFiles),
31
- flowPath: path.join(root, FLOW_FILE), flowHash: context.flowHash };
41
+ return {
42
+ root,
43
+ branch: branchResult.status === 0
44
+ ? branchResult.stdout.trim() || undefined
45
+ : undefined,
46
+ commit: commitResult.status === 0 ? commitResult.stdout.trim() : undefined,
47
+ dirty: gitDirty(root, plannedFiles),
48
+ flowPath: path.join(root, FLOW_FILE),
49
+ flowHash: context.flowHash,
50
+ };
32
51
  }
33
52
  export function assertProposalSourcePlan(sourcePlan) {
34
- const allowed = new Set([FLOW_FILE, GENERATED_FILE]);
35
53
  if (!sourcePlan?.files?.length)
36
- throw new Error('SOURCE_PLAN_BLOCKED: Proposal has no source operations');
54
+ throw new Error("SOURCE_PLAN_BLOCKED: Proposal has no source operations");
37
55
  for (const operation of sourcePlan.files) {
38
- if (!allowed.has(operation.path) || !['add', 'modify'].includes(operation.operation))
56
+ const allowed = operation.path === FLOW_FILE ||
57
+ operation.path === GENERATED_FILE ||
58
+ /^src\/integrations\/[a-z0-9-]+\/[A-Za-z0-9._-]+\.(?:ts|tsx)$/.test(operation.path);
59
+ if (!allowed || !["add", "modify"].includes(operation.operation))
39
60
  throw new Error(`SOURCE_PLAN_BLOCKED: unsupported ${operation.operation} operation for ${operation.path}`);
40
61
  }
41
- if (!sourcePlan.files.some(value => value.path === FLOW_FILE))
62
+ if (!sourcePlan.files.some((value) => value.path === FLOW_FILE))
42
63
  throw new Error(`SOURCE_PLAN_BLOCKED: ${FLOW_FILE} synchronization is required`);
43
64
  }
65
+ function moduleSource(role, module, owner) {
66
+ const header = `/* Generated from an approved FeltDB ${module.id} module Proposal. Configuration references only; no credential values. */\n`;
67
+ if (role === "module-domain")
68
+ return (header +
69
+ `export type IntegrationOwner=${JSON.stringify(owner)};\nexport const MODULE_MODELS=${JSON.stringify(module.models.map((value) => ({ name: value.name, ownership: value.ownership })))} as const;\nexport interface IntegrationConnection { id:string; ownerType:IntegrationOwner; ownerId:string; externalId:string; status:string; synchronizedAt?:string }\n`);
70
+ if (role === "module-provider-boundary")
71
+ return (header +
72
+ `export const MODULE_ID=${JSON.stringify(module.id)};\nexport const SERVER_CONFIGURATION=${JSON.stringify(module.configuration.filter((value) => value.visibility !== "browser").map((value) => value.name))} as const;\nexport const SERVER_SECRETS=${JSON.stringify(module.secrets.map((value) => value.name))} as const;\nexport const BROWSER_CONFIGURATION=${JSON.stringify(module.configuration.filter((value) => value.visibility === "browser").map((value) => value.name))} as const;\nexport const CAPABILITIES=${JSON.stringify(module.capabilities)} as const;\nexport const EVENTS=${JSON.stringify(module.events)} as const;\nexport interface ModuleTransport { invoke<T>(capability:string,input:unknown):Promise<T>; verifyEvent(payload:string,signature:string,secret:string):Promise<{type:string;data:unknown}> }\nexport function requireServerConfiguration(value:Record<string,string|undefined>){for(const name of [...SERVER_CONFIGURATION,...SERVER_SECRETS]){if(name.startsWith('VITE_'))throw new Error('Server module configuration cannot use VITE_ names');if(!value[name])throw new Error(\`Missing required server configuration: \${name}\`)}return value}\nexport const invokeCapability=<T>(transport:ModuleTransport,capability:typeof CAPABILITIES[number],input:unknown)=>transport.invoke<T>(capability,input);\n`);
73
+ if (role === "module-reconciliation")
74
+ return (header +
75
+ `export const RECONCILIATION=${JSON.stringify(module.reconciliation)} as const;\nexport function reconciliationTrace(event:string){const flow=RECONCILIATION.find(value=>value.event===event);if(!flow)throw new Error(\`Unsupported ${module.id} event: \${event}\`);return [flow.event,flow.workflow,...flow.writes] as const}\n`);
76
+ if (role === "module-ui")
77
+ return (header +
78
+ `export interface IntegrationViewModel { module:${JSON.stringify(module.id)};ownerType:${JSON.stringify(owner)};title:string;connections:Array<{id:string;status:string}>;capabilities:string[] }\nexport function integrationViewModel(connections:Array<{id:string;status:string}>):IntegrationViewModel{return{module:${JSON.stringify(module.id)},ownerType:${JSON.stringify(owner)},title:${JSON.stringify(module.name)},connections,capabilities:${JSON.stringify(module.capabilities)}}}\n`);
79
+ if (role === "module-contract-test")
80
+ return (header +
81
+ `import { reconciliationTrace } from './reconcile.js';\nexport function verifyModuleContract(){const trace=reconciliationTrace(${JSON.stringify(module.reconciliation[0]?.event ?? "")});if(trace[1]!==${JSON.stringify(module.reconciliation[0]?.workflow ?? "")})throw new Error('Module reconciliation contract failed');return true}\n`);
82
+ throw new Error(`SOURCE_PLAN_BLOCKED: unsupported generated role ${role ?? "<missing>"}`);
83
+ }
84
+ function syncProposalSource(proposal, cwd, replace = false) {
85
+ const operations = proposal.source_plan?.files.filter((value) => value.role?.startsWith("module-")) ?? [];
86
+ if (!operations.length)
87
+ return [];
88
+ const spec = parseFlowSpec(proposal.proposedFlow);
89
+ const operationPath = operations[0]?.path;
90
+ const declaration = spec.modules
91
+ ?.map((value) => resolveFeltDBModule(value))
92
+ .find((value) => value.source_artifacts.some((artifact) => artifact.path === operationPath));
93
+ if (!declaration)
94
+ throw new Error("SOURCE_PLAN_BLOCKED: source artifacts do not belong to a resolved module");
95
+ const primary = spec.collections.find((value) => value.name === declaration.placement.primary_application_model);
96
+ const owner = primary?.fields
97
+ .find((value) => value.name === declaration.placement.owner_field)
98
+ ?.type.replace(/^ref /, "") ?? "User";
99
+ const statePath = path.join(cwd, SYNC_STATE);
100
+ const state = readJson(statePath) ?? { version: 1, files: {} };
101
+ const written = [];
102
+ for (const operation of operations) {
103
+ const target = path.join(cwd, operation.path);
104
+ if (fs.existsSync(target)) {
105
+ const actual = digest(fs.readFileSync(target));
106
+ const recorded = state.files?.[operation.path];
107
+ if ((!recorded || recorded !== actual) && !replace)
108
+ throw new Error(`SOURCE_SYNC_CONFLICT: ${operation.path} has developer changes; review it or rerun with --replace-generated`);
109
+ }
110
+ const source = moduleSource(operation.role, declaration, owner);
111
+ fs.mkdirSync(path.dirname(target), { recursive: true });
112
+ fs.writeFileSync(target, source);
113
+ state.files[operation.path] = digest(source);
114
+ written.push(operation.path);
115
+ }
116
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
117
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, {
118
+ mode: 0o600,
119
+ });
120
+ return written;
121
+ }
44
122
  export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
45
123
  const flowPath = path.join(cwd, FLOW_FILE);
46
124
  if (!fs.existsSync(flowPath))
47
125
  throw new Error(`${FLOW_FILE} is required`);
48
- const source = fs.readFileSync(flowPath, 'utf8');
49
- const project = readJson(path.join(cwd, 'feltdb.config.json')) ?? {};
50
- const managed = readJson(path.join(cwd, '.feltdb', 'managed.json')) ?? {};
51
- const published = readJson(path.join(cwd, '.feltdb', 'last-published.json'));
52
- const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID
53
- || managed.applicationId || published?.applicationId || project.applicationId || parseFlowSpec(source).app;
54
- const tenantId = managed.tenantId || published?.tenantId || 'local';
55
- const environment = managed.environment || published?.environment || 'local';
126
+ const source = fs.readFileSync(flowPath, "utf8");
127
+ const project = readJson(path.join(cwd, "feltdb.config.json")) ?? {};
128
+ const managed = readJson(path.join(cwd, ".feltdb", "managed.json")) ?? {};
129
+ const published = readJson(path.join(cwd, ".feltdb", "last-published.json"));
130
+ const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID ||
131
+ managed.applicationId ||
132
+ published?.applicationId ||
133
+ project.applicationId ||
134
+ parseFlowSpec(source).app;
135
+ const tenantId = managed.tenantId || published?.tenantId || "local";
136
+ const environment = managed.environment || published?.environment || "local";
56
137
  const contract = await compileApplicationContract(parseFlowSpec(source), tenantId, applicationId);
57
138
  return {
58
139
  source,
@@ -65,45 +146,60 @@ export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
65
146
  }
66
147
  export async function reviewFlowProposal(proposal, cwd = process.cwd()) {
67
148
  const current = await loadLocalDevelopmentContract(cwd);
68
- return validateFlowProposal({ proposal, currentFlow: current.source, snapshot: current.snapshot });
149
+ return validateFlowProposal({
150
+ proposal,
151
+ currentFlow: current.source,
152
+ snapshot: current.snapshot,
153
+ });
69
154
  }
70
155
  function generatedContractSource(review) {
71
156
  const snapshot = createContractSnapshot(review.proposedContract, {
72
- environment: 'local',
73
- contractVersion: review.proposal.baseContractHash === review.proposedContract.hashes.application ? 1 : 2,
157
+ environment: "local",
158
+ contractVersion: review.proposal.baseContractHash ===
159
+ review.proposedContract.hashes.application
160
+ ? 1
161
+ : 2,
74
162
  });
75
- const collections = review.proposedSpec.collections.map(collection => ({
163
+ const collections = review.proposedSpec.collections.map((collection) => ({
76
164
  name: collection.name,
77
- fields: collection.fields.map(field => ({ name: field.name, type: field.type, optional: field.optional })),
165
+ fields: collection.fields.map((field) => ({
166
+ name: field.name,
167
+ type: field.type,
168
+ optional: field.optional,
169
+ })),
78
170
  }));
79
171
  const typeName = (value) => {
80
- const cleaned = value.replace(/[^A-Za-z0-9_$]/g, '_');
172
+ const cleaned = value.replace(/[^A-Za-z0-9_$]/g, "_");
81
173
  return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
82
174
  };
83
175
  const valueType = (value) => {
84
- const normalized = value.toLowerCase().replace(/\?$/, '');
85
- if (['int', 'integer', 'float', 'number'].includes(normalized))
86
- return 'number';
87
- if (['bool', 'boolean'].includes(normalized))
88
- return 'boolean';
89
- if (['json', 'object'].includes(normalized))
90
- return 'unknown';
91
- if (normalized.endsWith('[]'))
176
+ const normalized = value.toLowerCase().replace(/\?$/, "");
177
+ if (["int", "integer", "float", "number"].includes(normalized))
178
+ return "number";
179
+ if (["bool", "boolean"].includes(normalized))
180
+ return "boolean";
181
+ if (["json", "object"].includes(normalized))
182
+ return "unknown";
183
+ if (normalized.endsWith("[]"))
92
184
  return `${valueType(normalized.slice(0, -2))}[]`;
93
- return 'string';
185
+ return "string";
94
186
  };
95
- const types = review.proposedSpec.collections.map(collection => {
96
- const fields = collection.fields.map(field => ` ${JSON.stringify(field.name)}${field.optional ? '?' : ''}: ${valueType(field.type)};`);
97
- return `export interface ${typeName(collection.name)}Record {\n id: string;\n${fields.join('\n')}\n}`;
98
- }).join('\n\n');
99
- const bindings = review.proposedSpec.collections.map(collection => ` ${JSON.stringify(collection.name)}: database.collection<${typeName(collection.name)}Record>(${JSON.stringify(collection.name)}),`).join('\n');
100
- return `/* Generated by feltdb sync from feltdb.flow. Do not edit this file directly. */\n`
101
- + `export const FELTDB_CONTRACT = ${JSON.stringify(snapshot, null, 2)} as const;\n\n`
102
- + `export const FELTDB_COLLECTIONS = ${JSON.stringify(collections, null, 2)} as const;\n\n`
103
- + `${types}\n\n`
104
- + `export interface FeltDBCollectionBinder {\n collection<T>(name: string): unknown;\n}\n\n`
105
- + `/** Bind the generated, typed collection map to the project's FeltDB instance. */\n`
106
- + `export function bindGeneratedCollections(database: FeltDBCollectionBinder) {\n return {\n${bindings}\n };\n}\n`;
187
+ const types = review.proposedSpec.collections
188
+ .map((collection) => {
189
+ const fields = collection.fields.map((field) => ` ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${valueType(field.type)};`);
190
+ return `export interface ${typeName(collection.name)}Record {\n id: string;\n${fields.join("\n")}\n}`;
191
+ })
192
+ .join("\n\n");
193
+ const bindings = review.proposedSpec.collections
194
+ .map((collection) => ` ${JSON.stringify(collection.name)}: database.collection<${typeName(collection.name)}Record>(${JSON.stringify(collection.name)}),`)
195
+ .join("\n");
196
+ return (`/* Generated by feltdb sync from feltdb.flow. Do not edit this file directly. */\n` +
197
+ `export const FELTDB_CONTRACT = ${JSON.stringify(snapshot, null, 2)} as const;\n\n` +
198
+ `export const FELTDB_COLLECTIONS = ${JSON.stringify(collections, null, 2)} as const;\n\n` +
199
+ `${types}\n\n` +
200
+ `export interface FeltDBCollectionBinder {\n collection<T>(name: string): unknown;\n}\n\n` +
201
+ `/** Bind the generated, typed collection map to the project's FeltDB instance. */\n` +
202
+ `export function bindGeneratedCollections(database: FeltDBCollectionBinder) {\n return {\n${bindings}\n };\n}\n`);
107
203
  }
108
204
  export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
109
205
  const target = path.join(cwd, GENERATED_FILE);
@@ -120,7 +216,7 @@ export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
120
216
  fs.mkdirSync(path.dirname(target), { recursive: true });
121
217
  fs.writeFileSync(target, source);
122
218
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
123
- fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, contractHash: review.proposedContract.hashes.application, files: { [GENERATED_FILE]: digest(source) } }, null, 2)}\n`, { mode: 0o600 });
219
+ fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, contractHash: review.proposedContract.hashes.application, files: { ...(state.files ?? {}), [GENERATED_FILE]: digest(source) } }, null, 2)}\n`, { mode: 0o600 });
124
220
  return { written: [GENERATED_FILE] };
125
221
  }
126
222
  function assertGeneratedSourceSafe(cwd, replace = false) {
@@ -137,20 +233,37 @@ export async function applyFlowProposal(proposal, cwd = process.cwd(), options =
137
233
  const review = await reviewFlowProposal(proposal, cwd);
138
234
  const dirty = gitDirty(cwd, [FLOW_FILE, GENERATED_FILE]);
139
235
  if (dirty.length && !options.allowDirty)
140
- throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(', ')}`);
236
+ throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(", ")}`);
141
237
  if (review.authorizationChanges.length && !options.approveAuthorization) {
142
- throw new Error(`AUTHORIZATION_APPROVAL_REQUIRED: ${review.authorizationChanges.map(change => change.policy).join(', ')}`);
238
+ throw new Error(`AUTHORIZATION_APPROVAL_REQUIRED: ${review.authorizationChanges.map((change) => change.policy).join(", ")}`);
143
239
  }
144
240
  if (options.syncSource !== false)
145
241
  assertGeneratedSourceSafe(cwd, options.replaceGenerated);
146
- fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g, '\n'));
147
- const synchronized = options.syncSource === false ? { written: [] } : syncGeneratedSource(review, cwd, { replace: options.replaceGenerated });
148
- return { ...review, written: [FLOW_FILE, ...synchronized.written] };
242
+ fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g, "\n"));
243
+ const synchronized = options.syncSource === false
244
+ ? { written: [] }
245
+ : syncGeneratedSource(review, cwd, { replace: options.replaceGenerated });
246
+ const moduleWritten = options.syncSource === false
247
+ ? []
248
+ : syncProposalSource(proposal, cwd, Boolean(options.replaceGenerated));
249
+ return {
250
+ ...review,
251
+ written: [FLOW_FILE, ...synchronized.written, ...moduleWritten],
252
+ };
149
253
  }
150
254
  /** Apply and verify as one developer-visible transaction, restoring exact prior bytes on failure. */
151
255
  export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), options = {}) {
152
- const files = [FLOW_FILE, GENERATED_FILE, SYNC_STATE].map(relative => ({ relative, absolute: path.join(cwd, relative) }));
153
- const before = files.map(file => ({ ...file, existed: fs.existsSync(file.absolute), bytes: fs.existsSync(file.absolute) ? fs.readFileSync(file.absolute) : undefined }));
256
+ const planned = proposal.source_plan?.files.map((value) => value.path) ?? [];
257
+ const files = [
258
+ ...new Set([FLOW_FILE, GENERATED_FILE, SYNC_STATE, ...planned]),
259
+ ].map((relative) => ({ relative, absolute: path.join(cwd, relative) }));
260
+ const before = files.map((file) => ({
261
+ ...file,
262
+ existed: fs.existsSync(file.absolute),
263
+ bytes: fs.existsSync(file.absolute)
264
+ ? fs.readFileSync(file.absolute)
265
+ : undefined,
266
+ }));
154
267
  try {
155
268
  const result = await applyFlowProposal(proposal, cwd, options);
156
269
  options.verify?.();
@@ -170,14 +283,16 @@ export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), opt
170
283
  }
171
284
  export function describeProposal(review) {
172
285
  return [
173
- ...review.diff.added.map(value => `+ ${value}`),
174
- ...review.diff.changed.map(value => `~ ${value}`),
175
- ...review.diff.removed.map(value => `- ${value}`),
176
- ...review.authorizationChanges.flatMap(value => [
177
- `AUTHORIZATION CHANGE${value.widening ? ' (possible widening)' : ''}: ${value.policy}`,
178
- ` before: ${(value.before ?? ['<absent>']).join(' | ')}`,
179
- ` after: ${(value.after ?? ['<removed>']).join(' | ')}`,
286
+ ...review.diff.added.map((value) => `+ ${value}`),
287
+ ...review.diff.changed.map((value) => `~ ${value}`),
288
+ ...review.diff.removed.map((value) => `- ${value}`),
289
+ ...review.authorizationChanges.flatMap((value) => [
290
+ `AUTHORIZATION CHANGE${value.widening ? " (possible widening)" : ""}: ${value.policy}`,
291
+ ` before: ${(value.before ?? ["<absent>"]).join(" | ")}`,
292
+ ` after: ${(value.after ?? ["<removed>"]).join(" | ")}`,
180
293
  ]),
181
- ...review.migration.filter(value => value.safety !== 'safe').map(value => `${value.safety.toUpperCase()}: ${value.target} — ${value.detail}`),
294
+ ...review.migration
295
+ .filter((value) => value.safety !== "safe")
296
+ .map((value) => `${value.safety.toUpperCase()}: ${value.target} — ${value.detail}`),
182
297
  ];
183
298
  }
@@ -0,0 +1,55 @@
1
+ import { type ApplicationUnderstanding, type ObservedEvidenceInput } from './application-understanding.js';
2
+ import type { FeltDBContractSnapshot } from './application-development.js';
3
+ import type { FeltDBProposalRecord } from './proposal.js';
4
+ export type ConvergenceFindingStatus = 'satisfied' | 'missing' | 'partial' | 'contradictory' | 'stale' | 'unverified' | 'unexpected';
5
+ export type ConvergenceStatus = 'pending' | 'checking' | 'converged' | 'partially_converged' | 'diverged' | 'blocked' | 'unverified';
6
+ export type ConvergenceSurface = 'contract' | 'flow' | 'schema' | 'relationship' | 'policy' | 'workflow' | 'capability' | 'module' | 'source' | 'test' | 'build' | 'runtime';
7
+ export interface ConvergenceFinding {
8
+ claim: string;
9
+ surface: ConvergenceSurface;
10
+ status: ConvergenceFindingStatus;
11
+ expected?: unknown;
12
+ observed?: unknown;
13
+ confidence: number;
14
+ evidence: string[];
15
+ missing?: string[];
16
+ reason: string;
17
+ }
18
+ export interface SourcePlanObservation {
19
+ path: string;
20
+ status: 'implemented' | 'modified' | 'missing' | 'conflicted' | 'unexpected';
21
+ evidence?: string[];
22
+ }
23
+ export interface VerificationObservation {
24
+ surface: 'test' | 'build' | 'runtime';
25
+ claim: string;
26
+ status: 'satisfied' | 'partial' | 'missing' | 'unverified';
27
+ evidence?: string[];
28
+ }
29
+ export interface ConvergenceResult {
30
+ id?: string;
31
+ proposal_id: string;
32
+ application_id: string;
33
+ environment: string;
34
+ contract_hash: string;
35
+ flow_hash: string;
36
+ evidence_hash: string;
37
+ status: ConvergenceStatus;
38
+ findings: ConvergenceFinding[];
39
+ confidence: number;
40
+ created_at: string;
41
+ verifier_version: 1;
42
+ updated_understanding: ApplicationUnderstanding;
43
+ }
44
+ /** Verifies semantic satisfaction. Source layout is supporting evidence, never the primary convergence boundary. */
45
+ export declare function verifyProposalConvergence(input: {
46
+ proposal: FeltDBProposalRecord;
47
+ currentFlow: string;
48
+ snapshot: FeltDBContractSnapshot;
49
+ source?: SourcePlanObservation[];
50
+ verification?: VerificationObservation[];
51
+ observations?: ObservedEvidenceInput[];
52
+ now?: string;
53
+ }): Promise<ConvergenceResult>;
54
+ export declare function renderConvergenceDiagnostic(result: ConvergenceResult): string[];
55
+ //# sourceMappingURL=convergence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convergence.d.ts","sourceRoot":"","sources":["../src/convergence.ts"],"names":[],"mappings":"AACA,OAAO,EAA6D,KAAK,wBAAwB,EAAE,KAAK,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACtK,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAE3E,OAAO,KAAK,EAAE,oBAAoB,EAA2B,MAAM,eAAe,CAAC;AAEnF,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,eAAe,GAAG,OAAO,GAAG,YAAY,GAAG,YAAY,CAAC;AACrI,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,qBAAqB,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;AACrI,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAC7K,MAAM,WAAW,kBAAkB;IAAG,KAAK,EAAC,MAAM,CAAC;IAAC,OAAO,EAAC,kBAAkB,CAAC;IAAC,MAAM,EAAC,wBAAwB,CAAC;IAAC,QAAQ,CAAC,EAAC,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAC,OAAO,CAAC;IAAC,UAAU,EAAC,MAAM,CAAC;IAAC,QAAQ,EAAC,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAC,MAAM,EAAE,CAAC;IAAC,MAAM,EAAC,MAAM,CAAA;CAAE;AAC/N,MAAM,WAAW,qBAAqB;IAAG,IAAI,EAAC,MAAM,CAAC;IAAC,MAAM,EAAC,aAAa,GAAC,UAAU,GAAC,SAAS,GAAC,YAAY,GAAC,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAC,MAAM,EAAE,CAAA;CAAE;AAC/I,MAAM,WAAW,uBAAuB;IAAG,OAAO,EAAC,MAAM,GAAC,OAAO,GAAC,SAAS,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,MAAM,EAAC,WAAW,GAAC,SAAS,GAAC,SAAS,GAAC,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAC,MAAM,EAAE,CAAA;CAAE;AACpK,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAC,MAAM,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,cAAc,EAAC,MAAM,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,SAAS,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,CAAC;IACxI,MAAM,EAAC,iBAAiB,CAAC;IAAC,QAAQ,EAAC,kBAAkB,EAAE,CAAC;IAAC,UAAU,EAAC,MAAM,CAAC;IAAC,UAAU,EAAC,MAAM,CAAC;IAAC,gBAAgB,EAAC,CAAC,CAAC;IAAC,qBAAqB,EAAC,wBAAwB,CAAC;CACnK;AAKD,oHAAoH;AACpH,wBAAsB,yBAAyB,CAAC,KAAK,EAAC;IAAE,QAAQ,EAAC,oBAAoB,CAAC;IAAC,WAAW,EAAC,MAAM,CAAC;IAAC,QAAQ,EAAC,sBAAsB,CAAC;IAAC,MAAM,CAAC,EAAC,qBAAqB,EAAE,CAAC;IAAC,YAAY,CAAC,EAAC,uBAAuB,EAAE,CAAC;IAAC,YAAY,CAAC,EAAC,qBAAqB,EAAE,CAAC;IAAC,GAAG,CAAC,EAAC,MAAM,CAAA;CAAE,GAAE,OAAO,CAAC,iBAAiB,CAAC,CAqBrS;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAC,iBAAiB,GAAE,MAAM,EAAE,CAA8e"}
@@ -0,0 +1,63 @@
1
+ import { canonicalApplicationBytes } from './canonical-application.js';
2
+ import { collectApplicationUnderstanding } from './application-understanding.js';
3
+ import { parseFlowSpec } from './flowspec.js';
4
+ async function digest(value) { const bytes = canonicalApplicationBytes(value); const result = await crypto.subtle.digest('SHA-256', bytes.slice().buffer); return `sha256:${Array.from(new Uint8Array(result), byte => byte.toString(16).padStart(2, '0')).join('')}`; }
5
+ const ev = (items, claim) => items.filter(value => value.claim === claim).map(value => value.id);
6
+ /** Verifies semantic satisfaction. Source layout is supporting evidence, never the primary convergence boundary. */
7
+ export async function verifyProposalConvergence(input) {
8
+ const expected = parseFlowSpec(input.proposal.proposed_flow);
9
+ const actual = parseFlowSpec(input.currentFlow);
10
+ const findings = [];
11
+ const understanding = await collectApplicationUnderstanding({ intent: `verify ${input.proposal.id}`, snapshot: input.snapshot, currentFlow: input.currentFlow, observations: input.observations, now: input.now });
12
+ const push = (finding) => findings.push(finding);
13
+ const actualCollections = new Map(actual.collections.map(value => [value.name, value]));
14
+ for (const collection of expected.collections) {
15
+ const observed = actualCollections.get(collection.name);
16
+ push({ claim: `schema.collection.${collection.name}`, surface: 'schema', status: observed ? 'satisfied' : 'missing', expected: collection.name, observed: observed?.name, confidence: 1, evidence: ev(understanding.evidence, `schema.collection.${collection.name}`), reason: observed ? 'Collection exists in the canonical contract.' : 'Required collection is absent from the canonical contract.' });
17
+ if (!observed)
18
+ continue;
19
+ for (const field of collection.fields.filter(value => value.type.startsWith('ref '))) {
20
+ const current = observed.fields.find(value => value.name === field.name);
21
+ const status = !current ? 'missing' : current.type === field.type ? 'satisfied' : 'contradictory';
22
+ push({ claim: `schema.relationship.${collection.name}.${field.name}`, surface: 'relationship', status, expected: field.type, observed: current?.type, confidence: 1, evidence: ev(understanding.evidence, `schema.relationship.${collection.name}.${field.name}`), reason: status === 'satisfied' ? 'Relationship matches the Proposal.' : status === 'contradictory' ? 'The canonical relationship points to a different target.' : 'Required relationship is absent.' });
23
+ }
24
+ }
25
+ const compareNamed = (surface, expectedValues, actualValues) => { const names = new Set(actualValues.map(value => value.name)); for (const value of expectedValues)
26
+ push({ claim: `${surface}.${value.name}`, surface, status: names.has(value.name) ? 'satisfied' : 'missing', expected: value.name, observed: names.has(value.name) ? value.name : undefined, confidence: 1, evidence: ev(understanding.evidence, `${surface}.${value.name}`), reason: names.has(value.name) ? `${surface} exists in the canonical contract.` : `Required ${surface} is absent from the canonical contract.` }); };
27
+ compareNamed('policy', expected.policies, actual.policies);
28
+ compareNamed('workflow', expected.workflows, actual.workflows);
29
+ compareNamed('capability', expected.capabilities, actual.capabilities);
30
+ compareNamed('module', expected.modules ?? [], actual.modules ?? []);
31
+ const unexpectedNamed = (surface, expectedValues, actualValues) => { const names = new Set(expectedValues.map(value => value.name)); for (const value of actualValues.filter(value => !names.has(value.name)))
32
+ push({ claim: `${surface}.${value.name}`, surface, status: 'unexpected', observed: value.name, confidence: 1, evidence: ev(understanding.evidence, `${surface}.${surface === 'schema' ? 'collection.' : ''}${value.name}`), reason: `${surface} exists outside the Proposal semantics and does not automatically block convergence.` }); };
33
+ unexpectedNamed('schema', expected.collections, actual.collections);
34
+ unexpectedNamed('policy', expected.policies, actual.policies);
35
+ unexpectedNamed('workflow', expected.workflows, actual.workflows);
36
+ unexpectedNamed('capability', expected.capabilities, actual.capabilities);
37
+ unexpectedNamed('module', expected.modules ?? [], actual.modules ?? []);
38
+ const sourceByPath = new Map((input.source ?? []).map(value => [value.path, value]));
39
+ for (const planned of input.proposal.source_plan.files) {
40
+ const observed = sourceByPath.get(planned.path);
41
+ const status = observed?.status === 'implemented' ? 'satisfied' : observed?.status === 'modified' ? 'partial' : observed?.status === 'conflicted' ? 'contradictory' : observed?.status === 'unexpected' ? 'unexpected' : 'missing';
42
+ push({ claim: `source.${planned.path}`, surface: 'source', status, expected: planned.operation, observed: observed?.status, confidence: observed ? 1 : .2, evidence: observed?.evidence ?? [], reason: observed ? `Planned source is ${observed.status}.` : 'Planned source path was not verified.' });
43
+ }
44
+ for (const observed of input.source ?? [])
45
+ if (!input.proposal.source_plan.files.some(value => value.path === observed.path) && observed.status === 'unexpected')
46
+ push({ claim: `source.${observed.path}`, surface: 'source', status: 'unexpected', observed: observed.status, confidence: 1, evidence: observed.evidence ?? [], reason: 'Repository change was outside the Proposal source plan.' });
47
+ for (const observed of input.verification ?? [])
48
+ push({ claim: observed.claim, surface: observed.surface, status: observed.status, confidence: observed.status === 'unverified' ? .4 : 1, evidence: observed.evidence ?? [], reason: observed.status === 'satisfied' ? `${observed.surface} verification passed.` : `${observed.surface} verification is ${observed.status}.` });
49
+ const semantic = findings.filter(value => ['schema', 'relationship', 'policy', 'workflow', 'capability', 'module'].includes(value.surface));
50
+ const contradictions = semantic.some(value => value.status === 'contradictory');
51
+ const missing = semantic.some(value => value.status === 'missing');
52
+ const partial = findings.some(value => value.status === 'partial' || value.status === 'unverified');
53
+ const status = contradictions || missing ? 'diverged' : partial ? 'partially_converged' : semantic.length ? 'converged' : 'unverified';
54
+ const assessed = findings.filter(value => value.status !== 'unexpected');
55
+ const verified = assessed.filter(value => value.status === 'satisfied').length;
56
+ const confidence = assessed.length ? Math.round((verified / assessed.length) * 100) / 100 : 0;
57
+ const convergenceObservations = findings.slice(0, 50).map(value => ({ source: 'proposal_history', claim: `convergence.${value.claim}`, value: { status: value.status, confidence: value.confidence }, source_ref: `proposal:${input.proposal.id}` }));
58
+ const updatedUnderstanding = await collectApplicationUnderstanding({ intent: `verify ${input.proposal.id}`, snapshot: input.snapshot, currentFlow: input.currentFlow, observations: [...(input.observations ?? []).slice(0, 25), ...convergenceObservations.slice(0, 25)], now: input.now });
59
+ const flowHash = await digest(input.currentFlow.replace(/\r\n/g, '\n'));
60
+ const evidenceHash = await digest({ proposal_id: input.proposal.id, contract_hash: input.snapshot.contract_hash, flow_hash: flowHash, findings });
61
+ return { proposal_id: input.proposal.id, application_id: input.proposal.application_id, environment: input.proposal.environment, contract_hash: input.snapshot.contract_hash, flow_hash: flowHash, evidence_hash: evidenceHash, status, findings, confidence, created_at: input.now ?? new Date().toISOString(), verifier_version: 1, updated_understanding: updatedUnderstanding };
62
+ }
63
+ export function renderConvergenceDiagnostic(result) { const icon = (status) => status === 'satisfied' ? '✓' : status === 'unverified' || status === 'partial' ? '⚠' : status === 'unexpected' ? '+' : '✗'; return [`Verification: ${result.status.toUpperCase().replace('_', ' ')}`, `Confidence: ${Math.round(result.confidence * 100)}%`, ...result.findings.map(value => `${icon(value.status)} ${value.surface.toUpperCase()} ${value.claim} — ${value.status}${value.observed !== undefined && value.status !== 'satisfied' ? ` (observed: ${String(value.observed)})` : ''}`)]; }
@@ -68,6 +68,7 @@ export async function configureManagedAccount(options) {
68
68
  'application:read', 'application:write', 'application:revision:create',
69
69
  'application:revision:read', 'application:revision:promote',
70
70
  'application:environment:read', 'application:environment:write',
71
+ 'connections:read',
71
72
  ],
72
73
  }),
73
74
  }), 'Could not create the managed control-plane key');
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.8.0';
3
+ export const FELTDB_PACKAGE_VERSION = '0.8.2';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -211,6 +211,10 @@ pub struct ArtifactReference {
211
211
  pub name: String,
212
212
  pub uri: String,
213
213
  pub media_type: String,
214
+ /// Immutable source bytes used by trusted control-plane services. The URI
215
+ /// remains the content identity; values are base64 and never interpreted by Rust.
216
+ #[serde(default, skip_serializing_if = "Option::is_none")]
217
+ pub content: Option<String>,
214
218
  }
215
219
  #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
216
220
  pub struct DeploymentIntent {
@@ -1189,6 +1193,25 @@ impl ApplicationStore {
1189
1193
  .map_err(|e| e.to_string())?;
1190
1194
  fs::rename(tmp, &self.path).map_err(|e| e.to_string())
1191
1195
  }
1196
+
1197
+ /// Remove revision material for one application after its tenancy record
1198
+ /// has passed the caller's guarded destruction policy. The append-only
1199
+ /// state and global audit logs remain separate.
1200
+ pub fn purge_application(&self, application_id: &str) -> Result<(), String> {
1201
+ let mut r = self
1202
+ .records
1203
+ .write()
1204
+ .map_err(|_| "application store lock poisoned")?;
1205
+ r.drafts.retain(|value| value.application_id != application_id);
1206
+ r.revisions.retain(|value| value.application_id != application_id);
1207
+ r.promotions.retain(|value| value.application_id != application_id);
1208
+ r.previews.retain(|value| value.application_id != application_id);
1209
+ r.environment_pointers.remove(application_id);
1210
+ r.audit.retain(|value| value.application_id != application_id);
1211
+ r.untrusted_revisions.remove(application_id);
1212
+ r.recoveries.retain(|value| value.application_id != application_id);
1213
+ self.persist(&r)
1214
+ }
1192
1215
  fn event(
1193
1216
  r: &mut RevisionRecords,
1194
1217
  event: &str,