@feltdb/core 0.7.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/application-development.d.ts +10 -3
- package/dist/application-development.d.ts.map +1 -1
- package/dist/application-development.js +11 -1
- package/dist/application-manifest.d.ts +2 -0
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +1 -1
- package/dist/application-understanding.d.ts +76 -0
- package/dist/application-understanding.d.ts.map +1 -0
- package/dist/application-understanding.js +282 -0
- package/dist/canonical-application.d.ts +1 -0
- package/dist/canonical-application.d.ts.map +1 -1
- package/dist/canonical-application.js +3 -2
- package/dist/cli/ai-model-config.js +3 -3
- package/dist/cli/cli.js +1 -1
- package/dist/cli/commands.js +321 -45
- package/dist/cli/index.js +1 -1
- package/dist/cli/managed-environment.js +6 -0
- package/dist/cli/managed-project.js +6 -0
- package/dist/cli/proposal-client.js +16 -3
- package/dist/cli/publish-engine.js +8 -5
- package/dist/cli/repository-evidence.js +55 -0
- package/dist/cli/source-sync.js +220 -61
- package/dist/convergence.d.ts +55 -0
- package/dist/convergence.d.ts.map +1 -0
- package/dist/convergence.js +63 -0
- package/dist/create/cli.js +10 -1
- package/dist/create/create.js +10 -12
- package/dist/create/managed-account.js +72 -7
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +14 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +14 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +79 -14
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1929 -261
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +191 -22
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/create/template/default-project/feltdb.flow +5 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +9 -1
- package/dist/create/template/default-project/src/pages/SignUp.tsx +1 -1
- package/dist/flowspec.d.ts +1 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +51 -12
- package/dist/http-client.d.ts +166 -14
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +91 -31
- package/dist/http-db.d.ts +3 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +28 -3
- package/dist/index-core.d.ts +5 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +5 -0
- package/dist/intent.d.ts +98 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +343 -0
- package/dist/module-intent.d.ts +51 -0
- package/dist/module-intent.d.ts.map +1 -0
- package/dist/module-intent.js +230 -0
- package/dist/module.d.ts +105 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +566 -0
- package/dist/proposal.d.ts +92 -4
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +150 -6
- package/dist/studio/app.d.ts +3 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +2 -1
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -1
- package/dist/studio/components/ContractInspector.d.ts +7 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
- package/dist/studio/components/ManagedInstancePanel.js +1 -5
- package/dist/studio/components/ModuleExplorer.d.ts +6 -0
- package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +10 -1
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -1
- package/dist/studio/components/StateExplorer.d.ts +6 -7
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.d.ts +2 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/components-Duq2xQWt.js +2845 -0
- package/dist/studio/index.js +193 -147
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/index.d.ts +2 -0
- package/dist/studio/utils/index.d.ts.map +1 -1
- package/dist/studio/utils/index.js +14 -12
- package/dist/studio/utils/managed-instance.d.ts +0 -4
- package/dist/studio/utils/managed-instance.d.ts.map +1 -1
- package/dist/studio/utils/managed-instance.js +0 -4
- package/dist/studio/utils/proposal-api.d.ts +56 -0
- package/dist/studio/utils/proposal-api.d.ts.map +1 -0
- package/dist/studio/utils/proposal-api.js +94 -0
- package/dist/studio/utils/service-api.d.ts +81 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -0
- package/dist/studio/utils/service-api.js +51 -0
- package/dist/studio-app/assets/{feltdb_wasm-B-Ccgccb.js → feltdb_wasm-bIqcRzAr.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BE79okwX.wasm +0 -0
- package/dist/studio-app/assets/{index-BRrmvaKw.css → index-Cd_NPw14.css} +1 -1
- package/dist/studio-app/assets/index-XGYdlElN.js +29 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +2 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +128 -4
- package/package.json +1 -1
- package/dist/studio/components-Bm4sjhcX.js +0 -2162
- package/dist/studio-app/assets/dist-DA1MYSzp.js +0 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
- package/dist/studio-app/assets/index-CEIqowCh.js +0 -42
- package/dist/studio-app/assets/lib-B2_7fcn7.js +0 -69
- package/dist/studio-app/assets/worker-CARZ5g7K.js +0 -69
|
@@ -9,10 +9,11 @@ export function developmentProposalConnection(cwd = process.cwd()) {
|
|
|
9
9
|
throw new Error('No running FeltDB proposal service. Start feltdb dev.');
|
|
10
10
|
const config = fs.existsSync(path.join(cwd, 'feltdb.config.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, 'feltdb.config.json'), 'utf8')) : {};
|
|
11
11
|
const published = fs.existsSync(path.join(cwd, '.feltdb', 'last-published.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, '.feltdb', 'last-published.json'), 'utf8')) : {};
|
|
12
|
+
const managed = fs.existsSync(path.join(cwd, '.feltdb', 'managed.json')) ? JSON.parse(fs.readFileSync(path.join(cwd, '.feltdb', 'managed.json'), 'utf8')) : {};
|
|
12
13
|
const flowApplication = fs.existsSync(path.join(cwd, 'feltdb.flow')) ? parseFlowSpec(fs.readFileSync(path.join(cwd, 'feltdb.flow'), 'utf8')).app : undefined;
|
|
13
14
|
return { endpoint: endpoint.replace(/\/$/, ''), proposalAuthorityToken: stored?.proposalAuthorityToken,
|
|
14
|
-
applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || published.applicationId || config.applicationId || flowApplication,
|
|
15
|
-
environment:
|
|
15
|
+
applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || managed.applicationId || published.applicationId || config.applicationId || flowApplication,
|
|
16
|
+
environment: managed.environment || published.environment || 'local' };
|
|
16
17
|
}
|
|
17
18
|
export class ProposalServiceClient {
|
|
18
19
|
constructor(connection, token = process.env.FELTDB_MANAGED_CONTROL_API_KEY || '') {
|
|
@@ -38,11 +39,23 @@ export class ProposalServiceClient {
|
|
|
38
39
|
}
|
|
39
40
|
actor(input = {}) { return { application_id: this.connection.applicationId, environment: this.connection.environment, ...input }; }
|
|
40
41
|
create(input) { return this.request('/v1/proposals', { method: 'POST', body: JSON.stringify(input) }); }
|
|
41
|
-
|
|
42
|
+
executeIntent(rawRequest, actor = 'cli-developer') { return this.request('/v1/intents', { method: 'POST', body: JSON.stringify(this.actor({ raw_request: rawRequest, actor })) }); }
|
|
43
|
+
intents() { return this.request(this.scoped('/v1/intents')); }
|
|
44
|
+
list(filters = {}) { const query = new URLSearchParams(); if (this.connection.applicationId)
|
|
45
|
+
query.set('application_id', this.connection.applicationId); query.set('environment', filters.environment || this.connection.environment || 'local'); if (filters.status)
|
|
46
|
+
query.set('status', filters.status); if (filters.limit)
|
|
47
|
+
query.set('limit', String(filters.limit)); if (filters.cursor)
|
|
48
|
+
query.set('cursor', filters.cursor); return this.request(`/v1/proposals?${query}`); }
|
|
42
49
|
get(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}`)); }
|
|
43
50
|
history(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}/history`)); }
|
|
51
|
+
status(id, currentEvidenceHash) { const path = this.scoped(`/v1/proposals/${encodeURIComponent(id)}/status`); return this.request(currentEvidenceHash ? `${path}¤t_evidence_hash=${encodeURIComponent(currentEvidenceHash)}` : path); }
|
|
44
52
|
validate(id, actor = 'local-developer', proposedManifest) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/validate`, { method: 'POST', body: JSON.stringify(this.actor({ actor, proposed_manifest: proposedManifest })) }); }
|
|
45
53
|
preview(id, actor = 'local-developer') { return this.request(`/v1/proposals/${encodeURIComponent(id)}/preview`, { method: 'POST', body: JSON.stringify(this.actor({ actor })) }); }
|
|
54
|
+
getPreview(id) { return this.request(this.scoped(`/v1/previews/${encodeURIComponent(id)}`)); }
|
|
55
|
+
approve(id, actor = 'local-developer', approveAuthorization = false) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/approve`, { method: 'POST', body: JSON.stringify(this.actor({ actor, approve_authorization: approveAuthorization })) }); }
|
|
56
|
+
reject(id, actor = 'local-developer', reason) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/reject`, { method: 'POST', body: JSON.stringify(this.actor({ actor, reason })) }); }
|
|
57
|
+
convergence(id) { return this.request(this.scoped(`/v1/proposals/${encodeURIComponent(id)}/convergence`)); }
|
|
58
|
+
recordConvergence(id, result) { return this.request(`/v1/proposals/${encodeURIComponent(id)}/convergence`, { method: 'POST', body: JSON.stringify(this.actor(result)) }); }
|
|
46
59
|
transition(id, status, actor = 'local-developer', repositoryAuthority = false, approveAuthorization = false) {
|
|
47
60
|
return this.request(`/v1/proposals/${encodeURIComponent(id)}/status`, { method: 'PATCH', headers: repositoryAuthority && this.connection.proposalAuthorityToken
|
|
48
61
|
? { 'FeltDB-Proposal-Authority': this.connection.proposalAuthorityToken, 'FeltDB-Repository-Apply': '1' } : {}, body: JSON.stringify(this.actor({ status, actor, approve_authorization: approveAuthorization })) });
|
|
@@ -25,12 +25,14 @@ export function discoverPublishConfiguration(cwd = process.cwd(), environmentOve
|
|
|
25
25
|
throw new Error('feltdb.config.json is required for publish');
|
|
26
26
|
const project = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
27
27
|
const file = loadEnv(path.join(cwd, '.env.local'));
|
|
28
|
+
const managedFile = path.join(cwd, '.feltdb', 'managed.json');
|
|
29
|
+
const managed = fs.existsSync(managedFile) ? JSON.parse(fs.readFileSync(managedFile, 'utf8')) : {};
|
|
28
30
|
const value = (name) => process.env[name] || file[name];
|
|
29
31
|
const configuration = {
|
|
30
|
-
url: value('VITE_FELTDB_MANAGED_URL'), token: value('FELTDB_MANAGED_CONTROL_API_KEY'),
|
|
31
|
-
tenantId: value('VITE_FELTDB_MANAGED_TENANT_ID'), applicationId: value('VITE_FELTDB_MANAGED_APPLICATION_ID'),
|
|
32
|
-
namespace: value('VITE_FELTDB_MANAGED_NAMESPACE') || project.namespace,
|
|
33
|
-
environment: environmentOverride || value('VITE_FELTDB_MANAGED_ENVIRONMENT') || 'production',
|
|
32
|
+
url: value('VITE_FELTDB_MANAGED_URL') || managed.url, token: value('FELTDB_MANAGED_CONTROL_API_KEY'),
|
|
33
|
+
tenantId: managed.tenantId || value('VITE_FELTDB_MANAGED_TENANT_ID'), applicationId: value('VITE_FELTDB_MANAGED_APPLICATION_ID') || managed.applicationId,
|
|
34
|
+
namespace: managed.namespace || value('VITE_FELTDB_MANAGED_NAMESPACE') || project.namespace,
|
|
35
|
+
environment: environmentOverride || managed.environment || value('VITE_FELTDB_MANAGED_ENVIRONMENT') || 'production',
|
|
34
36
|
};
|
|
35
37
|
const missing = Object.entries(configuration).filter(([, value]) => !value).map(([name]) => name);
|
|
36
38
|
if (missing.length)
|
|
@@ -75,10 +77,11 @@ export function buildPublishBundle(cwd, configuration) {
|
|
|
75
77
|
'feltdb.agent_hash': contractHash(manifest.agents),
|
|
76
78
|
'feltdb.capability_hash': contractHash(manifest.capabilities),
|
|
77
79
|
'feltdb.contract_hash': contractHash({ dsl_version: spec.version, manifest }),
|
|
80
|
+
'feltdb.flow_hash': `sha256:${sha256(flowSource.replace(/\r\n/g, '\n'))}`,
|
|
78
81
|
'feltdb.artifact_hash': artifactHash,
|
|
79
82
|
'feltdb.namespace': configuration.namespace,
|
|
80
83
|
};
|
|
81
|
-
manifest.artifacts = artifacts.map(value => ({ name: value.path, uri: `
|
|
84
|
+
manifest.artifacts = artifacts.map(value => ({ name: value.path, uri: `artifact://${value.hash}`, media_type: value.mediaType, ...(value.path === 'feltdb.flow' ? { content: value.content } : {}) }));
|
|
82
85
|
const git = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
83
86
|
const sourceRevision = git.status === 0 ? git.stdout.trim() : undefined;
|
|
84
87
|
if (sourceRevision)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
const sourceExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.rs']);
|
|
5
|
+
function sourceFiles(root) {
|
|
6
|
+
const found = [];
|
|
7
|
+
const visit = (directory) => {
|
|
8
|
+
if (found.length >= 300 || !fs.existsSync(directory))
|
|
9
|
+
return;
|
|
10
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
11
|
+
if (found.length >= 300)
|
|
12
|
+
break;
|
|
13
|
+
const absolute = path.join(directory, entry.name);
|
|
14
|
+
if (entry.isDirectory() && !['node_modules', 'dist', 'build', '.git', '.feltdb-data'].includes(entry.name))
|
|
15
|
+
visit(absolute);
|
|
16
|
+
else if (entry.isFile() && sourceExtensions.has(path.extname(entry.name)) && fs.statSync(absolute).size <= 64 * 1024)
|
|
17
|
+
found.push(absolute);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
visit(path.join(root, 'src'));
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
/** Produces bounded claims and file references; source text never leaves this function. */
|
|
24
|
+
export function collectRepositoryEvidence(cwd = process.cwd()) {
|
|
25
|
+
const observations = [];
|
|
26
|
+
try {
|
|
27
|
+
const status = execFileSync('git', ['status', '--porcelain=v1'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
28
|
+
observations.push({ source: 'git', claim: 'git.working_tree', value: status.trim() ? 'dirty' : 'clean', source_ref: '.git' });
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
for (const absolute of sourceFiles(cwd)) {
|
|
32
|
+
if (observations.length >= 50)
|
|
33
|
+
break;
|
|
34
|
+
const relative = path.relative(cwd, absolute).split(path.sep).join('/');
|
|
35
|
+
let source = '';
|
|
36
|
+
try {
|
|
37
|
+
source = fs.readFileSync(absolute, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const add = (claim, value) => observations.push({ source: 'repository', claim, value, source_ref: relative });
|
|
43
|
+
if (/\b(authenticated|useSession|session|signIn|signOut)\b/i.test(source))
|
|
44
|
+
add('repository.authentication', 'authenticated session implementation observed');
|
|
45
|
+
if (/\borganization(?:Id|_id)?\b/i.test(source) && /\b(project|settings|billing)\b/i.test(source))
|
|
46
|
+
add('repository.organization_boundary', 'organization-scoped implementation observed');
|
|
47
|
+
if (/\buser(?:Id|_id|\.id)?\b/i.test(source) && /\b(project|settings|billing)\b/i.test(source))
|
|
48
|
+
add('repository.user_boundary', 'user-scoped implementation observed');
|
|
49
|
+
if (/\b(checkout|subscription|billing|stripe)\b/i.test(source))
|
|
50
|
+
add('repository.billing_surface', 'billing implementation surface observed');
|
|
51
|
+
if (/\b(github|repository|installation|webhook)\b/i.test(source))
|
|
52
|
+
add('repository.integration_surface', 'external collaboration implementation surface observed');
|
|
53
|
+
}
|
|
54
|
+
return observations.slice(0, 50);
|
|
55
|
+
}
|
package/dist/cli/source-sync.js
CHANGED
|
@@ -1,35 +1,139 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { createHash } from
|
|
4
|
-
import { spawnSync } from
|
|
5
|
-
import { compileApplicationContract, createContractSnapshot, flowSourceHash, validateFlowProposal, } from
|
|
6
|
-
import { parseFlowSpec } from
|
|
7
|
-
const FLOW_FILE =
|
|
8
|
-
const GENERATED_FILE =
|
|
9
|
-
const SYNC_STATE =
|
|
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(
|
|
11
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
12
12
|
}
|
|
13
13
|
function readJson(file) {
|
|
14
|
-
return fs.existsSync(file)
|
|
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(
|
|
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
|
|
25
|
+
return result.stdout
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((line) => line.slice(3));
|
|
29
|
+
}
|
|
30
|
+
export async function repositoryContext(cwd = process.cwd(), plannedFiles = [FLOW_FILE, GENERATED_FILE]) {
|
|
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"]);
|
|
37
|
+
const root = rootResult.status === 0 ? rootResult.stdout.trim() : path.resolve(cwd);
|
|
38
|
+
const branchResult = git(["branch", "--show-current"]);
|
|
39
|
+
const commitResult = git(["rev-parse", "HEAD"]);
|
|
40
|
+
const context = await loadLocalDevelopmentContract(root);
|
|
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
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function assertProposalSourcePlan(sourcePlan) {
|
|
53
|
+
if (!sourcePlan?.files?.length)
|
|
54
|
+
throw new Error("SOURCE_PLAN_BLOCKED: Proposal has no source operations");
|
|
55
|
+
for (const operation of sourcePlan.files) {
|
|
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))
|
|
60
|
+
throw new Error(`SOURCE_PLAN_BLOCKED: unsupported ${operation.operation} operation for ${operation.path}`);
|
|
61
|
+
}
|
|
62
|
+
if (!sourcePlan.files.some((value) => value.path === FLOW_FILE))
|
|
63
|
+
throw new Error(`SOURCE_PLAN_BLOCKED: ${FLOW_FILE} synchronization is required`);
|
|
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;
|
|
21
121
|
}
|
|
22
122
|
export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
|
|
23
123
|
const flowPath = path.join(cwd, FLOW_FILE);
|
|
24
124
|
if (!fs.existsSync(flowPath))
|
|
25
125
|
throw new Error(`${FLOW_FILE} is required`);
|
|
26
|
-
const source = fs.readFileSync(flowPath,
|
|
27
|
-
const project = readJson(path.join(cwd,
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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";
|
|
33
137
|
const contract = await compileApplicationContract(parseFlowSpec(source), tenantId, applicationId);
|
|
34
138
|
return {
|
|
35
139
|
source,
|
|
@@ -42,45 +146,60 @@ export async function loadLocalDevelopmentContract(cwd = process.cwd()) {
|
|
|
42
146
|
}
|
|
43
147
|
export async function reviewFlowProposal(proposal, cwd = process.cwd()) {
|
|
44
148
|
const current = await loadLocalDevelopmentContract(cwd);
|
|
45
|
-
return validateFlowProposal({
|
|
149
|
+
return validateFlowProposal({
|
|
150
|
+
proposal,
|
|
151
|
+
currentFlow: current.source,
|
|
152
|
+
snapshot: current.snapshot,
|
|
153
|
+
});
|
|
46
154
|
}
|
|
47
155
|
function generatedContractSource(review) {
|
|
48
156
|
const snapshot = createContractSnapshot(review.proposedContract, {
|
|
49
|
-
environment:
|
|
50
|
-
contractVersion: review.proposal.baseContractHash ===
|
|
157
|
+
environment: "local",
|
|
158
|
+
contractVersion: review.proposal.baseContractHash ===
|
|
159
|
+
review.proposedContract.hashes.application
|
|
160
|
+
? 1
|
|
161
|
+
: 2,
|
|
51
162
|
});
|
|
52
|
-
const collections = review.proposedSpec.collections.map(collection => ({
|
|
163
|
+
const collections = review.proposedSpec.collections.map((collection) => ({
|
|
53
164
|
name: collection.name,
|
|
54
|
-
fields: collection.fields.map(field => ({
|
|
165
|
+
fields: collection.fields.map((field) => ({
|
|
166
|
+
name: field.name,
|
|
167
|
+
type: field.type,
|
|
168
|
+
optional: field.optional,
|
|
169
|
+
})),
|
|
55
170
|
}));
|
|
56
171
|
const typeName = (value) => {
|
|
57
|
-
const cleaned = value.replace(/[^A-Za-z0-9_$]/g,
|
|
172
|
+
const cleaned = value.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
58
173
|
return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
59
174
|
};
|
|
60
175
|
const valueType = (value) => {
|
|
61
|
-
const normalized = value.toLowerCase().replace(/\?$/,
|
|
62
|
-
if ([
|
|
63
|
-
return
|
|
64
|
-
if ([
|
|
65
|
-
return
|
|
66
|
-
if ([
|
|
67
|
-
return
|
|
68
|
-
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("[]"))
|
|
69
184
|
return `${valueType(normalized.slice(0, -2))}[]`;
|
|
70
|
-
return
|
|
185
|
+
return "string";
|
|
71
186
|
};
|
|
72
|
-
const types = review.proposedSpec.collections
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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`);
|
|
84
203
|
}
|
|
85
204
|
export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
|
|
86
205
|
const target = path.join(cwd, GENERATED_FILE);
|
|
@@ -97,7 +216,7 @@ export function syncGeneratedSource(review, cwd = process.cwd(), options = {}) {
|
|
|
97
216
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
98
217
|
fs.writeFileSync(target, source);
|
|
99
218
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
100
|
-
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 });
|
|
101
220
|
return { written: [GENERATED_FILE] };
|
|
102
221
|
}
|
|
103
222
|
function assertGeneratedSourceSafe(cwd, replace = false) {
|
|
@@ -114,26 +233,66 @@ export async function applyFlowProposal(proposal, cwd = process.cwd(), options =
|
|
|
114
233
|
const review = await reviewFlowProposal(proposal, cwd);
|
|
115
234
|
const dirty = gitDirty(cwd, [FLOW_FILE, GENERATED_FILE]);
|
|
116
235
|
if (dirty.length && !options.allowDirty)
|
|
117
|
-
throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(
|
|
236
|
+
throw new Error(`DIRTY_SOURCE: uncommitted changes affect ${dirty.join(", ")}`);
|
|
118
237
|
if (review.authorizationChanges.length && !options.approveAuthorization) {
|
|
119
|
-
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(", ")}`);
|
|
120
239
|
}
|
|
121
240
|
if (options.syncSource !== false)
|
|
122
241
|
assertGeneratedSourceSafe(cwd, options.replaceGenerated);
|
|
123
|
-
fs.writeFileSync(path.join(cwd, FLOW_FILE), proposal.proposedFlow.replace(/\r\n/g,
|
|
124
|
-
const synchronized = options.syncSource === false
|
|
125
|
-
|
|
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
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Apply and verify as one developer-visible transaction, restoring exact prior bytes on failure. */
|
|
255
|
+
export async function applyFlowProposalAtomic(proposal, cwd = process.cwd(), options = {}) {
|
|
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
|
+
}));
|
|
267
|
+
try {
|
|
268
|
+
const result = await applyFlowProposal(proposal, cwd, options);
|
|
269
|
+
options.verify?.();
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
for (const file of before) {
|
|
274
|
+
if (file.existed && file.bytes) {
|
|
275
|
+
fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
|
|
276
|
+
fs.writeFileSync(file.absolute, file.bytes);
|
|
277
|
+
}
|
|
278
|
+
else if (fs.existsSync(file.absolute))
|
|
279
|
+
fs.rmSync(file.absolute, { force: true });
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
126
283
|
}
|
|
127
284
|
export function describeProposal(review) {
|
|
128
285
|
return [
|
|
129
|
-
...review.diff.added.map(value => `+ ${value}`),
|
|
130
|
-
...review.diff.changed.map(value => `~ ${value}`),
|
|
131
|
-
...review.diff.removed.map(value => `- ${value}`),
|
|
132
|
-
...review.authorizationChanges.flatMap(value => [
|
|
133
|
-
`AUTHORIZATION CHANGE${value.widening ?
|
|
134
|
-
` before: ${(value.before ?? [
|
|
135
|
-
` after: ${(value.after ?? [
|
|
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(" | ")}`,
|
|
136
293
|
]),
|
|
137
|
-
...review.migration
|
|
294
|
+
...review.migration
|
|
295
|
+
.filter((value) => value.safety !== "safe")
|
|
296
|
+
.map((value) => `${value.safety.toUpperCase()}: ${value.target} — ${value.detail}`),
|
|
138
297
|
];
|
|
139
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)})` : ''}`)]; }
|