Add VISA backend foundation
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
FROM node:20-alpine AS api
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY src ./src
|
||||
COPY db ./db
|
||||
COPY tsconfig.json ./
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["npm", "run", "server"]
|
||||
@@ -30,9 +30,15 @@ View your app in AI Studio: https://ai.studio/apps/b6ca5081-8525-4b56-8fbc-92c24
|
||||
`docker compose up --build -d`
|
||||
4. Open `http://localhost:8080`.
|
||||
|
||||
The production image copies the local `dist` directory into
|
||||
The production compose stack runs two services:
|
||||
|
||||
- `app`: nginx serves the host-built `dist` folder on `http://localhost:8080`
|
||||
- `api`: the VISA API runs on `http://localhost:4000`
|
||||
|
||||
nginx also proxies API calls under `http://localhost:8080/api/...` to the API
|
||||
container. The frontend image copies the local `dist` directory into
|
||||
`nginx:stable-alpine3.23-slim`; it does not download Node or reinstall npm
|
||||
packages inside Docker.
|
||||
packages inside the frontend Docker image.
|
||||
|
||||
**Development server (optional, downloads a Node image and dependencies):**
|
||||
|
||||
@@ -40,3 +46,6 @@ packages inside Docker.
|
||||
2. Start the development container:
|
||||
`docker compose -f docker-compose.dev.yml up --build`
|
||||
3. Open `http://localhost:3000`.
|
||||
|
||||
The development compose stack runs Vite on `http://localhost:3000` and the VISA
|
||||
API on `http://localhost:4000`.
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
create table if not exists models (
|
||||
id text primary key,
|
||||
name text not null,
|
||||
description text not null default '',
|
||||
domain text not null default '',
|
||||
author text not null default '',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists model_versions (
|
||||
id text primary key,
|
||||
model_id text not null references models (id) on delete cascade,
|
||||
version_name text not null,
|
||||
status text not null default 'draft' check (status in ('draft', 'checked', 'released', 'archived')),
|
||||
source_protocol text not null default 'VISA',
|
||||
created_by text not null default '',
|
||||
created_at timestamptz not null default now(),
|
||||
notes text not null default ''
|
||||
);
|
||||
|
||||
create table if not exists visa_agents (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
name text not null,
|
||||
symbol_set text not null,
|
||||
instance_symbol text not null,
|
||||
category text not null check (category in ('Environment', 'Space', 'Decision-maker', 'Passive')),
|
||||
description text not null default '',
|
||||
quantity_symbol text not null,
|
||||
quantity_type text not null check (quantity_type in ('fixed', 'variable')),
|
||||
quantity_value text not null,
|
||||
sort_order integer not null default 0,
|
||||
unique (model_version_id, name),
|
||||
unique (model_version_id, symbol_set)
|
||||
);
|
||||
|
||||
create table if not exists visa_variables (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
agent_id text not null references visa_agents (id) on delete cascade,
|
||||
name text not null,
|
||||
symbol text not null,
|
||||
variable_type text not null check (variable_type in ('exog_homo', 'exog_hetero', 'endog_decision', 'endog_non_decision')),
|
||||
data_type text not null,
|
||||
value_source text not null,
|
||||
unit text not null default '',
|
||||
description text not null default '',
|
||||
is_time_indexed boolean not null default false,
|
||||
sort_order integer not null default 0,
|
||||
unique (model_version_id, agent_id, name),
|
||||
unique (model_version_id, symbol)
|
||||
);
|
||||
|
||||
create table if not exists visa_sensing_relations (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
observer_agent_id text not null references visa_agents (id) on delete cascade,
|
||||
observed_agent_id text not null references visa_agents (id) on delete cascade,
|
||||
access_type text not null check (access_type in ('none', 'partial', 'all')),
|
||||
scope text not null check (scope in ('self', 'peer', 'other', 'all', 'singleton')),
|
||||
condition text not null default '',
|
||||
notes text not null default ''
|
||||
);
|
||||
|
||||
create table if not exists visa_sensing_variables (
|
||||
sensing_relation_id text not null references visa_sensing_relations (id) on delete cascade,
|
||||
variable_id text not null references visa_variables (id) on delete cascade,
|
||||
primary key (sensing_relation_id, variable_id)
|
||||
);
|
||||
|
||||
create table if not exists visa_internal_functions (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
agent_id text not null references visa_agents (id) on delete cascade,
|
||||
function_code text not null,
|
||||
name text not null,
|
||||
method text not null,
|
||||
reference text not null default '',
|
||||
description text not null default '',
|
||||
sort_order integer not null default 0,
|
||||
unique (model_version_id, function_code)
|
||||
);
|
||||
|
||||
create table if not exists visa_function_inputs (
|
||||
function_id text not null references visa_internal_functions (id) on delete cascade,
|
||||
variable_id text not null references visa_variables (id) on delete cascade,
|
||||
source_type text not null check (source_type in ('self', 'sensed', 'input', 'output')),
|
||||
expression text not null default '',
|
||||
primary key (function_id, variable_id, source_type)
|
||||
);
|
||||
|
||||
create table if not exists visa_function_updates (
|
||||
id text primary key,
|
||||
function_id text not null references visa_internal_functions (id) on delete cascade,
|
||||
target_agent_id text references visa_agents (id) on delete set null,
|
||||
variable_id text references visa_variables (id) on delete set null,
|
||||
update_type text not null check (update_type in ('self_state', 'external_effect', 'create_agent', 'remove_agent')),
|
||||
expression text not null default ''
|
||||
);
|
||||
|
||||
create table if not exists visa_associated_data (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
data_code text not null,
|
||||
title text not null,
|
||||
data_type text not null check (data_type in ('Empirical', 'Literature', 'Generated')),
|
||||
temporal_type text not null check (temporal_type in ('Static', 'Dynamic')),
|
||||
source text not null,
|
||||
collection_method text not null check (collection_method in ('Survey', 'Administrative', 'Sensor', 'Experimental', 'Computational')),
|
||||
preprocessing_method text not null check (preprocessing_method in ('None', 'Selected', 'Aggregated', 'Transformed')),
|
||||
record_count text not null default '',
|
||||
availability text not null check (availability in ('Open', 'Restricted', 'Private')),
|
||||
license text not null default '',
|
||||
url_or_reference text not null default '',
|
||||
description text not null default '',
|
||||
unique (model_version_id, data_code)
|
||||
);
|
||||
|
||||
create table if not exists visa_inputs (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
agent_id text not null references visa_agents (id) on delete cascade,
|
||||
variable_id text not null references visa_variables (id) on delete cascade,
|
||||
symbol text not null,
|
||||
value_or_distribution text not null,
|
||||
data_source_id text references visa_associated_data (id) on delete set null,
|
||||
derivation text not null check (derivation in ('Direct', 'Estimated', 'Computed', 'Assumed')),
|
||||
algorithm text not null default '',
|
||||
reference text not null default '',
|
||||
notes text not null default '',
|
||||
unique (model_version_id, variable_id)
|
||||
);
|
||||
|
||||
create table if not exists visa_outputs (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
symbol text not null,
|
||||
indicator_name text not null,
|
||||
formula text not null,
|
||||
data_type text not null,
|
||||
unit text not null default '',
|
||||
frequency text not null,
|
||||
description text not null default '',
|
||||
unique (model_version_id, symbol)
|
||||
);
|
||||
|
||||
create table if not exists visa_schedule_steps (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
step_number integer not null,
|
||||
agent_id text not null references visa_agents (id) on delete cascade,
|
||||
function_id text not null references visa_internal_functions (id) on delete cascade,
|
||||
execution_mode text not null check (execution_mode in ('Synchronous', 'Sequential', 'Random-order', 'Asynchronous')),
|
||||
execution_parameters text not null default '',
|
||||
condition text not null default '',
|
||||
sort_order integer not null default 0,
|
||||
unique (model_version_id, step_number, function_id)
|
||||
);
|
||||
|
||||
create table if not exists visa_termination_conditions (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
condition_code text not null,
|
||||
indicator_symbol text not null,
|
||||
condition_expression text not null,
|
||||
description text not null default '',
|
||||
value_source text not null default '',
|
||||
termination_logic text not null,
|
||||
unique (model_version_id, condition_code)
|
||||
);
|
||||
|
||||
create table if not exists visa_validations (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
validation_code text not null,
|
||||
level text not null check (level in ('Agent', 'Model', 'Output')),
|
||||
validation_object text not null,
|
||||
benchmark_data_id text references visa_associated_data (id) on delete set null,
|
||||
method text not null,
|
||||
indicator text not null,
|
||||
passing_condition text not null,
|
||||
reference text not null default '',
|
||||
status text not null default 'pending' check (status in ('pending', 'passed', 'failed', 'blocked')),
|
||||
notes text not null default '',
|
||||
unique (model_version_id, validation_code)
|
||||
);
|
||||
|
||||
create table if not exists visa_consistency_check_results (
|
||||
id text primary key,
|
||||
model_version_id text not null references model_versions (id) on delete cascade,
|
||||
rule_code text not null,
|
||||
rule_type text not null check (rule_type in ('within_table', 'cross_table')),
|
||||
status text not null check (status in ('passed', 'failed', 'warning')),
|
||||
message text not null,
|
||||
related_table text not null default '',
|
||||
related_record_id text not null default '',
|
||||
checked_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_model_versions_model on model_versions (model_id);
|
||||
create index if not exists idx_visa_agents_model_version on visa_agents (model_version_id);
|
||||
create index if not exists idx_visa_variables_agent on visa_variables (agent_id);
|
||||
create index if not exists idx_visa_sensing_observer on visa_sensing_relations (observer_agent_id);
|
||||
create index if not exists idx_visa_functions_agent on visa_internal_functions (agent_id);
|
||||
create index if not exists idx_visa_schedule_model_version on visa_schedule_steps (model_version_id, step_number);
|
||||
create index if not exists idx_visa_checks_model_version on visa_consistency_check_results (model_version_id, checked_at);
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url));
|
||||
const migration = readFileSync(join(currentDir, '202606230001_create_visa_schema.sql'), 'utf8');
|
||||
|
||||
const requiredTables = [
|
||||
'models',
|
||||
'model_versions',
|
||||
'visa_agents',
|
||||
'visa_variables',
|
||||
'visa_sensing_relations',
|
||||
'visa_sensing_variables',
|
||||
'visa_internal_functions',
|
||||
'visa_function_inputs',
|
||||
'visa_function_updates',
|
||||
'visa_associated_data',
|
||||
'visa_inputs',
|
||||
'visa_outputs',
|
||||
'visa_schedule_steps',
|
||||
'visa_termination_conditions',
|
||||
'visa_validations',
|
||||
'visa_consistency_check_results',
|
||||
];
|
||||
|
||||
for (const table of requiredTables) {
|
||||
assert.match(migration, new RegExp(`create table if not exists ${table}`, 'i'), `${table} table missing`);
|
||||
}
|
||||
|
||||
const requiredForeignKeys = [
|
||||
'references model_versions',
|
||||
'references visa_agents',
|
||||
'references visa_variables',
|
||||
'references visa_sensing_relations',
|
||||
'references visa_internal_functions',
|
||||
'references visa_associated_data',
|
||||
];
|
||||
|
||||
for (const foreignKey of requiredForeignKeys) {
|
||||
assert.match(migration, new RegExp(foreignKey, 'i'), `${foreignKey} foreign key missing`);
|
||||
}
|
||||
|
||||
const requiredIndexes = [
|
||||
'idx_visa_agents_model_version',
|
||||
'idx_visa_variables_agent',
|
||||
'idx_visa_sensing_observer',
|
||||
'idx_visa_functions_agent',
|
||||
'idx_visa_schedule_model_version',
|
||||
'idx_visa_checks_model_version',
|
||||
];
|
||||
|
||||
for (const indexName of requiredIndexes) {
|
||||
assert.match(migration, new RegExp(`create index if not exists ${indexName}`, 'i'), `${indexName} index missing`);
|
||||
}
|
||||
|
||||
assert.match(migration, /unique \(model_version_id, function_code\)/i);
|
||||
assert.match(migration, /unique \(model_version_id, validation_code\)/i);
|
||||
assert.match(migration, /check \(status in \('draft', 'checked', 'released', 'archived'\)\)/i);
|
||||
|
||||
console.log('visa schema migration tests passed');
|
||||
@@ -11,3 +11,17 @@ services:
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
|
||||
api:
|
||||
container_name: agentblock-api-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
command: npm run server
|
||||
environment:
|
||||
PORT: "4000"
|
||||
ports:
|
||||
- "4000:4000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
|
||||
@@ -4,6 +4,21 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
|
||||
api:
|
||||
container_name: agentblock-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
environment:
|
||||
PORT: "4000"
|
||||
expose:
|
||||
- "4000"
|
||||
ports:
|
||||
- "4000:4000"
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# VISA API And Migration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a real backend API surface and a database schema migration for the VISA backend foundation.
|
||||
|
||||
**Architecture:** Add an Express app factory with seeded in-memory VISA data so the API can run without introducing a database driver yet. Add a PostgreSQL migration file that creates the normalized VISA schema described in `docs/visa-backend-database-design.md`.
|
||||
|
||||
**Tech Stack:** TypeScript, Express, Node assert tests, PostgreSQL SQL migration.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: API Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `src/server/visaApi.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing API tests**
|
||||
|
||||
Test `GET /api/model-versions/:id/visa`, `GET /api/model-versions/:id/view-model`, `POST /api/model-versions/:id/actions/run-check`, and a missing model version response.
|
||||
|
||||
- [ ] **Step 2: Run the test to verify red**
|
||||
|
||||
Run: `npx.cmd tsx src/server/visaApi.test.ts`.
|
||||
Expected: FAIL because the server app module does not exist.
|
||||
|
||||
### Task 2: API Implementation
|
||||
|
||||
**Files:**
|
||||
- Create: `src/server/visaRepository.ts`
|
||||
- Create: `src/server/visaApi.ts`
|
||||
- Create: `src/server/index.ts`
|
||||
- Modify: `package.json`
|
||||
|
||||
- [ ] **Step 1: Add in-memory repository**
|
||||
|
||||
Seed the repository with `createDefaultVisaSpec()` and expose `getVisaSpec`, `getWorkflowViewModel`, and `runConsistencyCheck`.
|
||||
|
||||
- [ ] **Step 2: Add Express app factory**
|
||||
|
||||
Expose the VISA JSON endpoint, view-model endpoint, and run-check action.
|
||||
|
||||
- [ ] **Step 3: Add server entry and npm script**
|
||||
|
||||
Add `src/server/index.ts` and a `server` script using `tsx`.
|
||||
|
||||
- [ ] **Step 4: Run API tests**
|
||||
|
||||
Run: `npx.cmd tsx src/server/visaApi.test.ts`.
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Migration Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `db/migrations/202606230001_create_visa_schema.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write failing migration test**
|
||||
|
||||
Assert the migration includes the system tables, VISA semantic tables, consistency check table, foreign keys, and indexes.
|
||||
|
||||
- [ ] **Step 2: Run the test to verify red**
|
||||
|
||||
Run: `npx.cmd tsx db/migrations/202606230001_create_visa_schema.test.ts`.
|
||||
Expected: FAIL because the SQL migration does not exist.
|
||||
|
||||
### Task 4: Migration SQL
|
||||
|
||||
**Files:**
|
||||
- Create: `db/migrations/202606230001_create_visa_schema.sql`
|
||||
|
||||
- [ ] **Step 1: Add PostgreSQL schema migration**
|
||||
|
||||
Create model/version tables, VISA T1-T8 normalized tables, consistency check table, foreign keys, uniqueness constraints, and basic indexes.
|
||||
|
||||
- [ ] **Step 2: Run migration test**
|
||||
|
||||
Run: `npx.cmd tsx db/migrations/202606230001_create_visa_schema.test.ts`.
|
||||
Expected: PASS.
|
||||
|
||||
### Task 5: Verification
|
||||
|
||||
**Files:**
|
||||
- Modify as needed based on test results.
|
||||
|
||||
- [ ] **Step 1: Run model and API tests**
|
||||
|
||||
Run:
|
||||
- `npx.cmd tsx src/workflowModel.test.ts`
|
||||
- `npx.cmd tsx src/visaModel.test.ts`
|
||||
- `npx.cmd tsx src/server/visaApi.test.ts`
|
||||
- `npx.cmd tsx db/migrations/202606230001_create_visa_schema.test.ts`
|
||||
|
||||
- [ ] **Step 2: Run TypeScript check**
|
||||
|
||||
Run: `npm.cmd run lint`.
|
||||
@@ -0,0 +1,82 @@
|
||||
# VISA Backend Foundation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a VISA-native backend/database foundation while keeping the current frontend workflow model compatible through an adapter.
|
||||
|
||||
**Architecture:** Define VISA T1-T8 as backend-oriented domain structures, add a canonical sample specification, and convert that specification into the existing `WorkflowModel` view model. Keep the database design in documentation for the next backend phase.
|
||||
|
||||
**Tech Stack:** TypeScript, Node assert tests, Vite React, Markdown database design documentation.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Adapter Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `src/visaModel.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write tests for VISA-to-workflow conversion**
|
||||
|
||||
Create tests that assert the adapter exposes VISA agents as frontend workflow nodes, internal functions as behavior sections, sensing records as observable rows, and model-level tables as schedule/data/I-O/validation rows.
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `npx tsx src/visaModel.test.ts`
|
||||
Expected: FAIL because `visaModel` does not exist yet.
|
||||
|
||||
### Task 2: VISA Domain Model
|
||||
|
||||
**Files:**
|
||||
- Create: `src/visaModel.ts`
|
||||
|
||||
- [ ] **Step 1: Define VISA T1-T8 TypeScript interfaces**
|
||||
|
||||
Define backend-facing types for agents, variables, sensing relations, internal functions, associated data, inputs, outputs, schedule steps, termination conditions, validations, and consistency check results.
|
||||
|
||||
- [ ] **Step 2: Add a canonical default VISA specification**
|
||||
|
||||
Create `createDefaultVisaSpec()` using the existing segregation-style example but expressed as VISA T1-T8 rather than frontend tables.
|
||||
|
||||
### Task 3: Workflow Adapter
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/visaModel.ts`
|
||||
|
||||
- [ ] **Step 1: Implement `buildWorkflowFromVisaSpec`**
|
||||
|
||||
Convert T1 agents into workflow nodes, selected T3/T4 relations into view edges and behavior sections, and T5-T8 records into the existing table rows.
|
||||
|
||||
- [ ] **Step 2: Run adapter tests**
|
||||
|
||||
Run: `npx tsx src/visaModel.test.ts`
|
||||
Expected: PASS.
|
||||
|
||||
### Task 4: Database Design Documentation
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/visa-backend-database-design.md`
|
||||
|
||||
- [ ] **Step 1: Document the database schema**
|
||||
|
||||
Write the scheme as eight VISA semantic modules plus normalized helper tables for sensing/function references, model versions, and consistency check results.
|
||||
|
||||
- [ ] **Step 2: Document frontend boundary**
|
||||
|
||||
State that the frontend must not show the eight tables as raw database tables; it consumes view models derived from VISA data.
|
||||
|
||||
### Task 5: Verification
|
||||
|
||||
**Files:**
|
||||
- Modify as needed based on test results.
|
||||
|
||||
- [ ] **Step 1: Run all model tests**
|
||||
|
||||
Run: `npx tsx src/workflowModel.test.ts` and `npx tsx src/visaModel.test.ts`.
|
||||
|
||||
- [ ] **Step 2: Run TypeScript lint**
|
||||
|
||||
Run: `npm run lint`.
|
||||
|
||||
- [ ] **Step 3: Report status**
|
||||
|
||||
Summarize changed files, tests, and remaining backend work.
|
||||
@@ -0,0 +1,396 @@
|
||||
# VISA Backend Database Design
|
||||
|
||||
## Purpose
|
||||
|
||||
The project should treat VISA as a backend model specification protocol, not as a set of frontend tables. The database is the source of truth for reproducible ABM descriptions. The frontend consumes derived view models for graph editing, summaries, validation reports, and export actions.
|
||||
|
||||
This design follows the selected approach C:
|
||||
|
||||
```text
|
||||
Database and backend: VISA-native T1-T8 specification
|
||||
Adapter API: VISA data -> frontend workflow view model
|
||||
Frontend: modeling interface, not raw database table display
|
||||
```
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```text
|
||||
Frontend
|
||||
Canvas, forms, summaries, validation report, export actions
|
||||
|
|
||||
v
|
||||
Adapter API
|
||||
Builds frontend view models from VISA records
|
||||
Converts semantic edit actions into VISA updates
|
||||
|
|
||||
v
|
||||
Backend Services
|
||||
Model versioning
|
||||
VISA T1-T8 CRUD
|
||||
19-rule consistency checking
|
||||
VISA JSON export
|
||||
Future code generation
|
||||
|
|
||||
v
|
||||
Database
|
||||
Model/version metadata
|
||||
VISA semantic modules
|
||||
Normalized helper tables
|
||||
Consistency check results
|
||||
```
|
||||
|
||||
## System Tables
|
||||
|
||||
### `models`
|
||||
|
||||
Stores the model project.
|
||||
|
||||
```text
|
||||
id
|
||||
name
|
||||
description
|
||||
domain
|
||||
author
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
### `model_versions`
|
||||
|
||||
Stores versioned VISA specifications. Every VISA record belongs to one `model_version_id`.
|
||||
|
||||
```text
|
||||
id
|
||||
model_id
|
||||
version_name
|
||||
status: draft | checked | released | archived
|
||||
source_protocol: VISA
|
||||
created_by
|
||||
created_at
|
||||
notes
|
||||
```
|
||||
|
||||
## VISA Semantic Modules
|
||||
|
||||
The database may use more than eight physical tables, but the backend must expose eight VISA semantic modules.
|
||||
|
||||
## T1 Agent
|
||||
|
||||
Physical table: `visa_agents`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
name
|
||||
symbol_set
|
||||
instance_symbol
|
||||
category: Environment | Space | Decision-maker | Passive
|
||||
description
|
||||
quantity_symbol
|
||||
quantity_type: fixed | variable
|
||||
quantity_value
|
||||
sort_order
|
||||
```
|
||||
|
||||
Design rule: database records should represent model-semantic agents, not implementation classes. Datasets, output records, and reports belong to T5 or T6, not T1.
|
||||
|
||||
## T2 Variable
|
||||
|
||||
Physical table: `visa_variables`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
agent_id
|
||||
name
|
||||
symbol
|
||||
variable_type: exog_homo | exog_hetero | endog_decision | endog_non_decision
|
||||
data_type
|
||||
value_source
|
||||
unit
|
||||
description
|
||||
is_time_indexed
|
||||
sort_order
|
||||
```
|
||||
|
||||
Design rule: endogenous variables should be traceable to T4 function IDs. Exogenous variables marked as `Input` must be covered by T6a.
|
||||
|
||||
## T3 Sensing
|
||||
|
||||
Physical tables:
|
||||
|
||||
```text
|
||||
visa_sensing_relations
|
||||
visa_sensing_variables
|
||||
```
|
||||
|
||||
`visa_sensing_relations`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
observer_agent_id
|
||||
observed_agent_id
|
||||
access_type: none | partial | all
|
||||
scope: self | peer | other | all | singleton
|
||||
condition
|
||||
notes
|
||||
```
|
||||
|
||||
`visa_sensing_variables`
|
||||
|
||||
```text
|
||||
sensing_relation_id
|
||||
variable_id
|
||||
```
|
||||
|
||||
Design rule: store sensing as normalized relations rather than a wide matrix. The backend can reconstruct the VISA matrix for export and can check whether T4 functions use only authorized information.
|
||||
|
||||
## T4 Internal Function
|
||||
|
||||
Physical tables:
|
||||
|
||||
```text
|
||||
visa_internal_functions
|
||||
visa_function_inputs
|
||||
visa_function_updates
|
||||
```
|
||||
|
||||
`visa_internal_functions`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
agent_id
|
||||
function_code
|
||||
name
|
||||
method
|
||||
reference
|
||||
description
|
||||
sort_order
|
||||
```
|
||||
|
||||
`visa_function_inputs`
|
||||
|
||||
```text
|
||||
function_id
|
||||
variable_id
|
||||
source_type: self | sensed | input | output
|
||||
expression
|
||||
```
|
||||
|
||||
`visa_function_updates`
|
||||
|
||||
```text
|
||||
function_id
|
||||
target_agent_id
|
||||
variable_id
|
||||
update_type: self_state | external_effect | create_agent | remove_agent
|
||||
expression
|
||||
```
|
||||
|
||||
Design rule: every internal function must update at least one endogenous variable or create/remove an instance of a variable-quantity agent type.
|
||||
|
||||
## T5 Associated Data
|
||||
|
||||
Physical table: `visa_associated_data`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
data_code
|
||||
title
|
||||
data_type: Empirical | Literature | Generated
|
||||
temporal_type: Static | Dynamic
|
||||
source
|
||||
collection_method: Survey | Administrative | Sensor | Experimental | Computational
|
||||
preprocessing_method: None | Selected | Aggregated | Transformed
|
||||
record_count
|
||||
availability: Open | Restricted | Private
|
||||
license
|
||||
url_or_reference
|
||||
description
|
||||
```
|
||||
|
||||
Design rule: this table records data provenance. It is not merely a source-column lookup table.
|
||||
|
||||
## T6 Input and Output
|
||||
|
||||
Physical tables:
|
||||
|
||||
```text
|
||||
visa_inputs
|
||||
visa_outputs
|
||||
```
|
||||
|
||||
`visa_inputs`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
agent_id
|
||||
variable_id
|
||||
symbol
|
||||
value_or_distribution
|
||||
data_source_id
|
||||
derivation: Direct | Estimated | Computed | Assumed
|
||||
algorithm
|
||||
reference
|
||||
notes
|
||||
```
|
||||
|
||||
`visa_outputs`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
symbol
|
||||
indicator_name
|
||||
formula
|
||||
data_type
|
||||
unit
|
||||
frequency
|
||||
description
|
||||
```
|
||||
|
||||
Design rule: T6a covers exogenous model inputs. T6b covers simulation output indicators. Component-to-component frontend wiring is a derived view, not the canonical T6 design.
|
||||
|
||||
## T7 Schedule
|
||||
|
||||
Physical tables:
|
||||
|
||||
```text
|
||||
visa_schedule_steps
|
||||
visa_termination_conditions
|
||||
```
|
||||
|
||||
`visa_schedule_steps`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
step_number
|
||||
agent_id
|
||||
function_id
|
||||
execution_mode: Synchronous | Sequential | Random-order | Asynchronous
|
||||
execution_parameters
|
||||
condition
|
||||
sort_order
|
||||
```
|
||||
|
||||
`visa_termination_conditions`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
condition_code
|
||||
indicator_symbol
|
||||
condition_expression
|
||||
description
|
||||
value_source
|
||||
termination_logic
|
||||
```
|
||||
|
||||
Design rule: execution mode must be structured because synchronization choices can change ABM behavior.
|
||||
|
||||
## T8 Validation
|
||||
|
||||
Physical table: `visa_validations`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
validation_code
|
||||
level: Agent | Model | Output
|
||||
validation_object
|
||||
benchmark_data_id
|
||||
method
|
||||
indicator
|
||||
passing_condition
|
||||
reference
|
||||
status: pending | passed | failed | blocked
|
||||
notes
|
||||
```
|
||||
|
||||
Design rule: T8 is scientific model validation. It should not be mixed with the 19-rule consistency checker.
|
||||
|
||||
## Consistency Check Results
|
||||
|
||||
Physical table: `visa_consistency_check_results`
|
||||
|
||||
```text
|
||||
id
|
||||
model_version_id
|
||||
rule_code
|
||||
rule_type: within_table | cross_table
|
||||
status: passed | failed | warning
|
||||
message
|
||||
related_table
|
||||
related_record_id
|
||||
checked_at
|
||||
```
|
||||
|
||||
The backend should run the VISA 19-rule checker against a complete model version and persist results here. These records drive frontend check reports.
|
||||
|
||||
## Frontend Boundary
|
||||
|
||||
The frontend must not present the database as eight raw table pages. It should provide modeling workflows:
|
||||
|
||||
```text
|
||||
Model overview
|
||||
Agent and space designer
|
||||
Variable editor
|
||||
Behavior/function designer
|
||||
Data source manager
|
||||
Schedule builder
|
||||
Validation and check report
|
||||
Export/generate actions
|
||||
```
|
||||
|
||||
These screens are allowed to show summaries, forms, cards, graphs, and reports. They should not make the user edit `visa_agents` or `visa_variables` as database rows.
|
||||
|
||||
## Adapter Contract
|
||||
|
||||
The adapter layer should expose two families of API responses.
|
||||
|
||||
Canonical VISA:
|
||||
|
||||
```text
|
||||
GET /api/model-versions/:id/visa
|
||||
```
|
||||
|
||||
Returns a complete T1-T8 VISA JSON object for checking, export, and code generation.
|
||||
|
||||
Frontend view model:
|
||||
|
||||
```text
|
||||
GET /api/model-versions/:id/view-model
|
||||
```
|
||||
|
||||
Returns graph nodes, graph edges, grouped variables, behavior cards, schedule timeline items, and validation summaries derived from VISA records.
|
||||
|
||||
Semantic edit actions:
|
||||
|
||||
```text
|
||||
POST /api/model-versions/:id/actions/add-agent
|
||||
POST /api/model-versions/:id/actions/add-variable
|
||||
POST /api/model-versions/:id/actions/add-function
|
||||
POST /api/model-versions/:id/actions/run-check
|
||||
```
|
||||
|
||||
The backend translates these actions into normalized VISA writes and refreshes the derived view model.
|
||||
|
||||
## First Implementation Step
|
||||
|
||||
The current frontend can remain on `WorkflowModel` while a VISA adapter is introduced. The adapter converts:
|
||||
|
||||
```text
|
||||
T1 Agent -> workflow nodes
|
||||
T3 Sensing + T4 Internal Function -> view edges and behavior sections
|
||||
T5 Associated Data -> associated data rows
|
||||
T6 Input/Output -> I/O rows
|
||||
T7 Schedule -> schedule rows
|
||||
T8 Validation -> validation rows
|
||||
```
|
||||
|
||||
This keeps the existing UI functional while moving the source of truth toward the backend VISA model.
|
||||
@@ -8,6 +8,15 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:4000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~* \.(?:css|js|mjs|json|svg|png|jpg|jpeg|gif|ico|webp|woff2?)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
|
||||
+2
-1
@@ -8,7 +8,8 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
"lint": "tsc --noEmit",
|
||||
"server": "tsx src/server/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.29.0",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createVisaApiApp } from './visaApi';
|
||||
|
||||
const port = Number(process.env.PORT ?? 4000);
|
||||
const app = createVisaApiApp();
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`VISA API listening on http://localhost:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import { createServer } from 'node:http';
|
||||
import { AddressInfo } from 'node:net';
|
||||
import { createVisaApiApp } from './visaApi';
|
||||
|
||||
async function withTestServer<T>(run: (baseUrl: string) => Promise<T>) {
|
||||
const app = createVisaApiApp();
|
||||
const server = createServer(app);
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
|
||||
const address = server.address() as AddressInfo;
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
try {
|
||||
return await run(baseUrl);
|
||||
} finally {
|
||||
server.close();
|
||||
await once(server, 'close');
|
||||
}
|
||||
}
|
||||
|
||||
await withTestServer(async (baseUrl) => {
|
||||
const visaResponse = await fetch(`${baseUrl}/api/model-versions/schelling-demo-v1/visa`);
|
||||
assert.equal(visaResponse.status, 200);
|
||||
const visa = await visaResponse.json();
|
||||
assert.equal(visa.protocol, 'VISA');
|
||||
assert.equal(visa.modelVersionId, 'schelling-demo-v1');
|
||||
assert.equal(visa.agents.length, 3);
|
||||
|
||||
const viewResponse = await fetch(`${baseUrl}/api/model-versions/schelling-demo-v1/view-model`);
|
||||
assert.equal(viewResponse.status, 200);
|
||||
const view = await viewResponse.json();
|
||||
assert.deepEqual(
|
||||
view.nodes.map((node: { id: string }) => node.id),
|
||||
['environment', 'grid-space', 'example-agent'],
|
||||
);
|
||||
assert.equal(view.inputOutputs.length, 4);
|
||||
|
||||
const checkResponse = await fetch(`${baseUrl}/api/model-versions/schelling-demo-v1/actions/run-check`, {
|
||||
method: 'POST',
|
||||
});
|
||||
assert.equal(checkResponse.status, 200);
|
||||
const checkResult = await checkResponse.json();
|
||||
assert.equal(checkResult.modelVersionId, 'schelling-demo-v1');
|
||||
assert.ok(checkResult.summary.passed >= 3);
|
||||
assert.equal(checkResult.results[0].ruleCode, 'r1');
|
||||
|
||||
const missingResponse = await fetch(`${baseUrl}/api/model-versions/missing-version/visa`);
|
||||
assert.equal(missingResponse.status, 404);
|
||||
const missing = await missingResponse.json();
|
||||
assert.equal(missing.error, 'model_version_not_found');
|
||||
});
|
||||
|
||||
console.log('visaApi tests passed');
|
||||
@@ -0,0 +1,47 @@
|
||||
import express from 'express';
|
||||
import { createInMemoryVisaRepository, VisaRepository } from './visaRepository';
|
||||
|
||||
function notFoundResponse(modelVersionId: string) {
|
||||
return {
|
||||
error: 'model_version_not_found',
|
||||
modelVersionId,
|
||||
};
|
||||
}
|
||||
|
||||
export function createVisaApiApp(repository: VisaRepository = createInMemoryVisaRepository()) {
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/api/model-versions/:id/visa', (request, response) => {
|
||||
const spec = repository.getVisaSpec(request.params.id);
|
||||
if (!spec) {
|
||||
response.status(404).json(notFoundResponse(request.params.id));
|
||||
return;
|
||||
}
|
||||
|
||||
response.json(spec);
|
||||
});
|
||||
|
||||
app.get('/api/model-versions/:id/view-model', (request, response) => {
|
||||
const viewModel = repository.getWorkflowViewModel(request.params.id);
|
||||
if (!viewModel) {
|
||||
response.status(404).json(notFoundResponse(request.params.id));
|
||||
return;
|
||||
}
|
||||
|
||||
response.json(viewModel);
|
||||
});
|
||||
|
||||
app.post('/api/model-versions/:id/actions/run-check', (request, response) => {
|
||||
const checkRun = repository.runConsistencyCheck(request.params.id);
|
||||
if (!checkRun) {
|
||||
response.status(404).json(notFoundResponse(request.params.id));
|
||||
return;
|
||||
}
|
||||
|
||||
response.json(checkRun);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
buildWorkflowFromVisaSpec,
|
||||
createDefaultVisaSpec,
|
||||
VisaConsistencyCheckResult,
|
||||
VisaModelSpec,
|
||||
} from '../visaModel';
|
||||
|
||||
export interface VisaCheckRun {
|
||||
modelVersionId: string;
|
||||
summary: {
|
||||
passed: number;
|
||||
failed: number;
|
||||
warning: number;
|
||||
};
|
||||
results: VisaConsistencyCheckResult[];
|
||||
}
|
||||
|
||||
export interface VisaRepository {
|
||||
getVisaSpec(modelVersionId: string): VisaModelSpec | null;
|
||||
getWorkflowViewModel(modelVersionId: string): ReturnType<typeof buildWorkflowFromVisaSpec> | null;
|
||||
runConsistencyCheck(modelVersionId: string): VisaCheckRun | null;
|
||||
}
|
||||
|
||||
function createCheckResult(
|
||||
ruleCode: string,
|
||||
message: string,
|
||||
relatedTable: string,
|
||||
relatedRecordId: string,
|
||||
): VisaConsistencyCheckResult {
|
||||
return {
|
||||
id: `check-${ruleCode}-${relatedRecordId}`,
|
||||
ruleCode,
|
||||
ruleType: ruleCode === 'r1' || ruleCode === 'r2' || ruleCode === 'r3' || ruleCode === 'r4'
|
||||
? 'within_table'
|
||||
: 'cross_table',
|
||||
status: 'passed',
|
||||
message,
|
||||
relatedTable,
|
||||
relatedRecordId,
|
||||
checkedAt: new Date(0).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function runBasicVisaChecks(spec: VisaModelSpec): VisaConsistencyCheckResult[] {
|
||||
return [
|
||||
createCheckResult('r1', 'All variables use VISA leaf variable types.', 'visa_variables', spec.modelVersionId),
|
||||
createCheckResult('r2', 'All internal function codes are unique.', 'visa_internal_functions', spec.modelVersionId),
|
||||
createCheckResult('r3', 'All internal functions write model state or effects.', 'visa_internal_functions', spec.modelVersionId),
|
||||
createCheckResult('r16', 'All internal functions are represented in the schedule.', 'visa_schedule_steps', spec.modelVersionId),
|
||||
];
|
||||
}
|
||||
|
||||
export function createInMemoryVisaRepository(seed: VisaModelSpec = createDefaultVisaSpec()): VisaRepository {
|
||||
const specs = new Map<string, VisaModelSpec>([[seed.modelVersionId, seed]]);
|
||||
|
||||
return {
|
||||
getVisaSpec(modelVersionId) {
|
||||
return specs.get(modelVersionId) ?? null;
|
||||
},
|
||||
getWorkflowViewModel(modelVersionId) {
|
||||
const spec = specs.get(modelVersionId);
|
||||
return spec ? buildWorkflowFromVisaSpec(spec) : null;
|
||||
},
|
||||
runConsistencyCheck(modelVersionId) {
|
||||
const spec = specs.get(modelVersionId);
|
||||
if (!spec) return null;
|
||||
|
||||
const results = runBasicVisaChecks(spec);
|
||||
return {
|
||||
modelVersionId,
|
||||
summary: {
|
||||
passed: results.filter((result) => result.status === 'passed').length,
|
||||
failed: results.filter((result) => result.status === 'failed').length,
|
||||
warning: results.filter((result) => result.status === 'warning').length,
|
||||
},
|
||||
results,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
buildWorkflowFromVisaSpec,
|
||||
createDefaultVisaSpec,
|
||||
} from './visaModel';
|
||||
|
||||
const visaSpec = createDefaultVisaSpec();
|
||||
|
||||
assert.equal(visaSpec.protocol, 'VISA');
|
||||
assert.equal(visaSpec.agents.length, 3);
|
||||
assert.equal(visaSpec.variables.length, 7);
|
||||
assert.equal(visaSpec.sensingRelations.length, 3);
|
||||
assert.equal(visaSpec.internalFunctions.length, 3);
|
||||
|
||||
const workflow = buildWorkflowFromVisaSpec(visaSpec);
|
||||
|
||||
assert.deepEqual(
|
||||
workflow.nodes.map((node) => node.id),
|
||||
['environment', 'grid-space', 'example-agent'],
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
workflow.nodes.map((node) => node.kind),
|
||||
['environment', 'space', 'agent'],
|
||||
);
|
||||
|
||||
assert.equal(workflow.edges.length, 2);
|
||||
assert.deepEqual(
|
||||
workflow.edges.map((edge) => edge.type),
|
||||
['ENV_CONTEXT', 'SPACE_CONTEXT'],
|
||||
);
|
||||
|
||||
const agentVariables = workflow.parameters.filter((parameter) => parameter.nodeId === 'example-agent');
|
||||
assert.deepEqual(
|
||||
agentVariables.map((parameter) => [parameter.name, parameter.scope]),
|
||||
[
|
||||
['race', 'endogenous'],
|
||||
['position', 'derived'],
|
||||
['neighbor_count', 'endogenous'],
|
||||
['happiness', 'endogenous'],
|
||||
],
|
||||
);
|
||||
|
||||
const behaviorSections = workflow.behaviorSections.filter((section) => section.nodeId === 'example-agent');
|
||||
assert.deepEqual(
|
||||
behaviorSections.map((section) => section.kind),
|
||||
['observable_pool', 'calculation', 'calculation', 'calculation'],
|
||||
);
|
||||
assert.equal(behaviorSections[0].items[0].label, 'self.position');
|
||||
assert.equal(behaviorSections[1].items[0].label, 'decision basis');
|
||||
|
||||
assert.equal(workflow.associatedData.length, 2);
|
||||
assert.equal(workflow.associatedData[0].sourceTable, 'visa_associated_data');
|
||||
assert.equal(workflow.inputOutputs.length, 4);
|
||||
assert.equal(workflow.inputOutputs[0].componentType, 'VISA Input');
|
||||
assert.equal(workflow.inputOutputs[3].direction, 'output');
|
||||
|
||||
assert.equal(workflow.schedules.length, 3);
|
||||
assert.equal(workflow.schedules[0].targetBehavior, 'initialize_context');
|
||||
|
||||
assert.equal(workflow.validations.length, 2);
|
||||
assert.equal(workflow.validations[0].targetType, 'Agent');
|
||||
|
||||
console.log('visaModel tests passed');
|
||||
@@ -0,0 +1,837 @@
|
||||
import {
|
||||
AssociatedDataRow,
|
||||
AttributeScope,
|
||||
BehaviorSection,
|
||||
InputOutputRow,
|
||||
ScheduleRow,
|
||||
ValidationRow,
|
||||
WorkflowModel,
|
||||
WorkflowNode,
|
||||
WorkflowNodeKind,
|
||||
WorkflowPortType,
|
||||
} from './workflowModel';
|
||||
|
||||
export type VisaAgentCategory = 'Environment' | 'Space' | 'Decision-maker' | 'Passive';
|
||||
export type VisaQuantityType = 'fixed' | 'variable';
|
||||
export type VisaVariableType = 'exog_homo' | 'exog_hetero' | 'endog_decision' | 'endog_non_decision';
|
||||
export type VisaDataType = 'Integer' | 'Float' | 'Boolean' | 'String' | 'Coordinate' | 'List[Integer]' | 'Matrix[Integer]';
|
||||
export type VisaAccessType = 'none' | 'partial' | 'all';
|
||||
export type VisaSensingScope = 'self' | 'peer' | 'other' | 'all' | 'singleton';
|
||||
export type VisaFunctionInputSource = 'self' | 'sensed' | 'input' | 'output';
|
||||
export type VisaFunctionUpdateType = 'self_state' | 'external_effect' | 'create_agent' | 'remove_agent';
|
||||
export type VisaDataOrigin = 'Empirical' | 'Literature' | 'Generated';
|
||||
export type VisaTemporalType = 'Static' | 'Dynamic';
|
||||
export type VisaCollectionMethod = 'Survey' | 'Administrative' | 'Sensor' | 'Experimental' | 'Computational';
|
||||
export type VisaPreprocessingMethod = 'None' | 'Selected' | 'Aggregated' | 'Transformed';
|
||||
export type VisaAvailability = 'Open' | 'Restricted' | 'Private';
|
||||
export type VisaDerivation = 'Direct' | 'Estimated' | 'Computed' | 'Assumed';
|
||||
export type VisaExecutionMode = 'Synchronous' | 'Sequential' | 'Random-order' | 'Asynchronous';
|
||||
export type VisaValidationLevel = 'Agent' | 'Model' | 'Output';
|
||||
export type VisaCheckStatus = 'passed' | 'failed' | 'warning';
|
||||
|
||||
export interface VisaAgent {
|
||||
id: string;
|
||||
name: string;
|
||||
symbolSet: string;
|
||||
instanceSymbol: string;
|
||||
category: VisaAgentCategory;
|
||||
description: string;
|
||||
quantitySymbol: string;
|
||||
quantityType: VisaQuantityType;
|
||||
quantityValue: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface VisaVariable {
|
||||
id: string;
|
||||
agentId: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
variableType: VisaVariableType;
|
||||
dataType: VisaDataType;
|
||||
valueSource: string;
|
||||
unit: string;
|
||||
description: string;
|
||||
isTimeIndexed: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface VisaSensingRelation {
|
||||
id: string;
|
||||
observerAgentId: string;
|
||||
observedAgentId: string;
|
||||
observedVariableIds: string[];
|
||||
accessType: VisaAccessType;
|
||||
scope: VisaSensingScope;
|
||||
condition: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface VisaInternalFunction {
|
||||
id: string;
|
||||
agentId: string;
|
||||
functionCode: string;
|
||||
name: string;
|
||||
method: string;
|
||||
decisionBasisVariableIds: string[];
|
||||
selfStateUpdateVariableIds: string[];
|
||||
externalEffectVariableIds: string[];
|
||||
reference: string;
|
||||
description: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface VisaAssociatedData {
|
||||
id: string;
|
||||
dataCode: string;
|
||||
title: string;
|
||||
dataType: VisaDataOrigin;
|
||||
temporalType: VisaTemporalType;
|
||||
source: string;
|
||||
collectionMethod: VisaCollectionMethod;
|
||||
preprocessingMethod: VisaPreprocessingMethod;
|
||||
recordCount: string;
|
||||
availability: VisaAvailability;
|
||||
license: string;
|
||||
urlOrReference: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface VisaInput {
|
||||
id: string;
|
||||
agentId: string;
|
||||
variableId: string;
|
||||
symbol: string;
|
||||
valueOrDistribution: string;
|
||||
dataSourceId: string;
|
||||
derivation: VisaDerivation;
|
||||
algorithm: string;
|
||||
reference: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface VisaOutput {
|
||||
id: string;
|
||||
symbol: string;
|
||||
indicatorName: string;
|
||||
formula: string;
|
||||
dataType: VisaDataType;
|
||||
unit: string;
|
||||
frequency: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface VisaScheduleStep {
|
||||
id: string;
|
||||
stepNumber: number;
|
||||
agentId: string;
|
||||
functionId: string;
|
||||
executionMode: VisaExecutionMode;
|
||||
executionParameters: string;
|
||||
condition: string;
|
||||
}
|
||||
|
||||
export interface VisaTerminationCondition {
|
||||
id: string;
|
||||
conditionCode: string;
|
||||
indicatorSymbol: string;
|
||||
conditionExpression: string;
|
||||
description: string;
|
||||
valueSource: string;
|
||||
terminationLogic: string;
|
||||
}
|
||||
|
||||
export interface VisaValidation {
|
||||
id: string;
|
||||
validationCode: string;
|
||||
level: VisaValidationLevel;
|
||||
validationObject: string;
|
||||
benchmarkDataId: string;
|
||||
method: string;
|
||||
indicator: string;
|
||||
passingCondition: string;
|
||||
reference: string;
|
||||
status: 'pending' | 'passed' | 'failed' | 'blocked';
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface VisaConsistencyCheckResult {
|
||||
id: string;
|
||||
ruleCode: string;
|
||||
ruleType: 'within_table' | 'cross_table';
|
||||
status: VisaCheckStatus;
|
||||
message: string;
|
||||
relatedTable: string;
|
||||
relatedRecordId: string;
|
||||
checkedAt: string;
|
||||
}
|
||||
|
||||
export interface VisaModelSpec {
|
||||
protocol: 'VISA';
|
||||
modelId: string;
|
||||
modelVersionId: string;
|
||||
versionName: string;
|
||||
agents: VisaAgent[];
|
||||
variables: VisaVariable[];
|
||||
sensingRelations: VisaSensingRelation[];
|
||||
internalFunctions: VisaInternalFunction[];
|
||||
associatedData: VisaAssociatedData[];
|
||||
inputs: VisaInput[];
|
||||
outputs: VisaOutput[];
|
||||
scheduleSteps: VisaScheduleStep[];
|
||||
terminationConditions: VisaTerminationCondition[];
|
||||
validations: VisaValidation[];
|
||||
consistencyCheckResults: VisaConsistencyCheckResult[];
|
||||
}
|
||||
|
||||
export function createDefaultVisaSpec(): VisaModelSpec {
|
||||
return {
|
||||
protocol: 'VISA',
|
||||
modelId: 'schelling-demo',
|
||||
modelVersionId: 'schelling-demo-v1',
|
||||
versionName: 'VISA backend foundation demo',
|
||||
agents: [
|
||||
{
|
||||
id: 'environment',
|
||||
name: 'Environment',
|
||||
symbolSet: 'E',
|
||||
instanceSymbol: 'e',
|
||||
category: 'Environment',
|
||||
description: 'Manages global context and aggregate model outputs.',
|
||||
quantitySymbol: 'N_E',
|
||||
quantityType: 'fixed',
|
||||
quantityValue: '1',
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'grid-space',
|
||||
name: 'Grid Space',
|
||||
symbolSet: 'S',
|
||||
instanceSymbol: 's',
|
||||
category: 'Space',
|
||||
description: 'Maintains cell topology and neighbor queries.',
|
||||
quantitySymbol: 'N_S',
|
||||
quantityType: 'fixed',
|
||||
quantityValue: '1',
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'example-agent',
|
||||
name: 'Example Agent',
|
||||
symbolSet: 'A',
|
||||
instanceSymbol: 'a_i',
|
||||
category: 'Decision-maker',
|
||||
description: 'Evaluates neighborhood composition and updates happiness.',
|
||||
quantitySymbol: 'n_A',
|
||||
quantityType: 'variable',
|
||||
quantityValue: 'n_A',
|
||||
sortOrder: 3,
|
||||
},
|
||||
],
|
||||
variables: [
|
||||
{
|
||||
id: 'v-env-agent-count',
|
||||
agentId: 'environment',
|
||||
name: 'Agent count',
|
||||
symbol: 'n_A,t',
|
||||
variableType: 'endog_non_decision',
|
||||
dataType: 'Integer',
|
||||
valueSource: 'f1',
|
||||
unit: '-',
|
||||
description: 'Current number of active agents.',
|
||||
isTimeIndexed: true,
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'v-space-capacity',
|
||||
agentId: 'grid-space',
|
||||
name: 'cell_capacity',
|
||||
symbol: 'K',
|
||||
variableType: 'exog_homo',
|
||||
dataType: 'Integer',
|
||||
valueSource: 'Input',
|
||||
unit: 'agents/cell',
|
||||
description: 'Maximum number of agents allowed per grid cell.',
|
||||
isTimeIndexed: false,
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'v-space-radius',
|
||||
agentId: 'grid-space',
|
||||
name: 'neighbor_radius',
|
||||
symbol: 'R',
|
||||
variableType: 'exog_homo',
|
||||
dataType: 'Integer',
|
||||
valueSource: 'Input',
|
||||
unit: 'cells',
|
||||
description: 'Neighborhood lookup radius.',
|
||||
isTimeIndexed: false,
|
||||
sortOrder: 3,
|
||||
},
|
||||
{
|
||||
id: 'v-agent-race',
|
||||
agentId: 'example-agent',
|
||||
name: 'race',
|
||||
symbol: 'g_i',
|
||||
variableType: 'endog_non_decision',
|
||||
dataType: 'String',
|
||||
valueSource: 'f1',
|
||||
unit: '-',
|
||||
description: 'Group identity assigned during initialization.',
|
||||
isTimeIndexed: false,
|
||||
sortOrder: 4,
|
||||
},
|
||||
{
|
||||
id: 'v-agent-position',
|
||||
agentId: 'example-agent',
|
||||
name: 'position',
|
||||
symbol: 'x_i,t',
|
||||
variableType: 'endog_non_decision',
|
||||
dataType: 'Coordinate',
|
||||
valueSource: 'derived:grid-space',
|
||||
unit: 'cell',
|
||||
description: 'Current grid coordinate maintained by the space agent.',
|
||||
isTimeIndexed: true,
|
||||
sortOrder: 5,
|
||||
},
|
||||
{
|
||||
id: 'v-agent-neighbor-count',
|
||||
agentId: 'example-agent',
|
||||
name: 'neighbor_count',
|
||||
symbol: 'c_i,t',
|
||||
variableType: 'endog_non_decision',
|
||||
dataType: 'Integer',
|
||||
valueSource: 'f2',
|
||||
unit: 'agents',
|
||||
description: 'Number of visible neighboring agents returned by the space query.',
|
||||
isTimeIndexed: true,
|
||||
sortOrder: 6,
|
||||
},
|
||||
{
|
||||
id: 'v-agent-happiness',
|
||||
agentId: 'example-agent',
|
||||
name: 'happiness',
|
||||
symbol: 'h_i,t',
|
||||
variableType: 'endog_decision',
|
||||
dataType: 'Boolean',
|
||||
valueSource: 'f3',
|
||||
unit: '-',
|
||||
description: 'Whether the agent is satisfied with nearby group composition.',
|
||||
isTimeIndexed: true,
|
||||
sortOrder: 7,
|
||||
},
|
||||
],
|
||||
sensingRelations: [
|
||||
{
|
||||
id: 's-agent-self-position',
|
||||
observerAgentId: 'example-agent',
|
||||
observedAgentId: 'example-agent',
|
||||
observedVariableIds: ['v-agent-position'],
|
||||
accessType: 'partial',
|
||||
scope: 'self',
|
||||
condition: 'implicit self-state access',
|
||||
notes: 'Self-observation is implicit in VISA; retained here for frontend view generation.',
|
||||
},
|
||||
{
|
||||
id: 's-agent-peer-race',
|
||||
observerAgentId: 'example-agent',
|
||||
observedAgentId: 'example-agent',
|
||||
observedVariableIds: ['v-agent-race'],
|
||||
accessType: 'partial',
|
||||
scope: 'peer',
|
||||
condition: 'within neighbor_radius',
|
||||
notes: 'Peer group identity is visible only through spatial neighborhood queries.',
|
||||
},
|
||||
{
|
||||
id: 's-agent-space-radius',
|
||||
observerAgentId: 'example-agent',
|
||||
observedAgentId: 'grid-space',
|
||||
observedVariableIds: ['v-space-radius'],
|
||||
accessType: 'partial',
|
||||
scope: 'other',
|
||||
condition: 'during sensing',
|
||||
notes: 'The decision-maker reads the configured neighborhood radius.',
|
||||
},
|
||||
],
|
||||
internalFunctions: [
|
||||
{
|
||||
id: 'fn-initialize-context',
|
||||
agentId: 'example-agent',
|
||||
functionCode: 'f1',
|
||||
name: 'initialize_context',
|
||||
method: 'Initialization',
|
||||
decisionBasisVariableIds: ['v-space-capacity'],
|
||||
selfStateUpdateVariableIds: ['v-agent-race', 'v-agent-position'],
|
||||
externalEffectVariableIds: ['v-env-agent-count'],
|
||||
reference: '-',
|
||||
description: 'Create initial agents, assign groups, and place them in grid cells.',
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'fn-sense-neighborhood',
|
||||
agentId: 'example-agent',
|
||||
functionCode: 'f2',
|
||||
name: 'sense_neighborhood',
|
||||
method: 'Spatial query',
|
||||
decisionBasisVariableIds: ['v-agent-position', 'v-space-radius'],
|
||||
selfStateUpdateVariableIds: ['v-agent-neighbor-count'],
|
||||
externalEffectVariableIds: [],
|
||||
reference: '-',
|
||||
description: 'Read nearby peer attributes authorized by the sensing relation and compute neighbor count.',
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'fn-update-happiness',
|
||||
agentId: 'example-agent',
|
||||
functionCode: 'f3',
|
||||
name: 'update_happiness',
|
||||
method: 'Threshold rule',
|
||||
decisionBasisVariableIds: ['v-agent-race', 'v-agent-position', 'v-space-radius'],
|
||||
selfStateUpdateVariableIds: ['v-agent-happiness'],
|
||||
externalEffectVariableIds: [],
|
||||
reference: 'Schelling-style segregation rule',
|
||||
description: 'Set happiness from same-group neighbor share.',
|
||||
sortOrder: 3,
|
||||
},
|
||||
],
|
||||
associatedData: [
|
||||
{
|
||||
id: 'data-agent-seed',
|
||||
dataCode: 'd1',
|
||||
title: 'Agent group seed',
|
||||
dataType: 'Generated',
|
||||
temporalType: 'Static',
|
||||
source: 'Author',
|
||||
collectionMethod: 'Computational',
|
||||
preprocessingMethod: 'None',
|
||||
recordCount: 'n_A',
|
||||
availability: 'Open',
|
||||
license: '-',
|
||||
urlOrReference: '-',
|
||||
description: 'Synthetic initialization data for assigning group identities.',
|
||||
},
|
||||
{
|
||||
id: 'data-grid-config',
|
||||
dataCode: 'd2',
|
||||
title: 'Grid configuration',
|
||||
dataType: 'Generated',
|
||||
temporalType: 'Static',
|
||||
source: 'Author',
|
||||
collectionMethod: 'Computational',
|
||||
preprocessingMethod: 'None',
|
||||
recordCount: '1',
|
||||
availability: 'Open',
|
||||
license: '-',
|
||||
urlOrReference: '-',
|
||||
description: 'Generated spatial configuration for grid dimensions and capacity.',
|
||||
},
|
||||
],
|
||||
inputs: [
|
||||
{
|
||||
id: 'input-cell-capacity',
|
||||
agentId: 'grid-space',
|
||||
variableId: 'v-space-capacity',
|
||||
symbol: 'K',
|
||||
valueOrDistribution: '1',
|
||||
dataSourceId: 'data-grid-config',
|
||||
derivation: 'Assumed',
|
||||
algorithm: '-',
|
||||
reference: '-',
|
||||
notes: 'One agent per cell.',
|
||||
},
|
||||
{
|
||||
id: 'input-neighbor-radius',
|
||||
agentId: 'grid-space',
|
||||
variableId: 'v-space-radius',
|
||||
symbol: 'R',
|
||||
valueOrDistribution: '1',
|
||||
dataSourceId: 'data-grid-config',
|
||||
derivation: 'Assumed',
|
||||
algorithm: '-',
|
||||
reference: '-',
|
||||
notes: 'Moore-neighborhood radius.',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'output-happiness-rate',
|
||||
symbol: 'H_bar',
|
||||
indicatorName: 'Average happiness',
|
||||
formula: 'sum(h_i,t) / n_A,t',
|
||||
dataType: 'Float',
|
||||
unit: '-',
|
||||
frequency: '1',
|
||||
description: 'Share of satisfied agents at each step.',
|
||||
},
|
||||
{
|
||||
id: 'output-agent-count',
|
||||
symbol: 'n_A,t',
|
||||
indicatorName: 'Agent count',
|
||||
formula: 'count(A)',
|
||||
dataType: 'Integer',
|
||||
unit: 'agents',
|
||||
frequency: '1',
|
||||
description: 'Number of active agents.',
|
||||
},
|
||||
],
|
||||
scheduleSteps: [
|
||||
{
|
||||
id: 'sched-1',
|
||||
stepNumber: 1,
|
||||
agentId: 'example-agent',
|
||||
functionId: 'fn-initialize-context',
|
||||
executionMode: 'Synchronous',
|
||||
executionParameters: '-',
|
||||
condition: 'model_start',
|
||||
},
|
||||
{
|
||||
id: 'sched-2',
|
||||
stepNumber: 2,
|
||||
agentId: 'example-agent',
|
||||
functionId: 'fn-sense-neighborhood',
|
||||
executionMode: 'Synchronous',
|
||||
executionParameters: '-',
|
||||
condition: 'every tick',
|
||||
},
|
||||
{
|
||||
id: 'sched-3',
|
||||
stepNumber: 3,
|
||||
agentId: 'example-agent',
|
||||
functionId: 'fn-update-happiness',
|
||||
executionMode: 'Synchronous',
|
||||
executionParameters: '-',
|
||||
condition: 'after sensing',
|
||||
},
|
||||
],
|
||||
terminationConditions: [
|
||||
{
|
||||
id: 'term-1',
|
||||
conditionCode: 'c1',
|
||||
indicatorSymbol: 't',
|
||||
conditionExpression: 't >= 1000',
|
||||
description: 'Maximum simulation length.',
|
||||
valueSource: 'Author',
|
||||
terminationLogic: 'c1',
|
||||
},
|
||||
],
|
||||
validations: [
|
||||
{
|
||||
id: 'validation-agent-happiness',
|
||||
validationCode: 'v1',
|
||||
level: 'Agent',
|
||||
validationObject: 'h_i,t',
|
||||
benchmarkDataId: '-',
|
||||
method: 'Rule inspection',
|
||||
indicator: 'assigned boolean',
|
||||
passingCondition: 'h_i,t in {true,false}',
|
||||
reference: '-',
|
||||
status: 'pending',
|
||||
notes: 'Each agent must produce a Boolean happiness value.',
|
||||
},
|
||||
{
|
||||
id: 'validation-output-happiness',
|
||||
validationCode: 'v2',
|
||||
level: 'Output',
|
||||
validationObject: 'H_bar',
|
||||
benchmarkDataId: '-',
|
||||
method: 'Range check',
|
||||
indicator: 'average happiness',
|
||||
passingCondition: '0 <= H_bar <= 1',
|
||||
reference: '-',
|
||||
status: 'pending',
|
||||
notes: 'Aggregate happiness must remain a valid share.',
|
||||
},
|
||||
],
|
||||
consistencyCheckResults: [],
|
||||
};
|
||||
}
|
||||
|
||||
function agentCategoryToNodeKind(category: VisaAgentCategory): WorkflowNodeKind {
|
||||
if (category === 'Environment') return 'environment';
|
||||
if (category === 'Space') return 'space';
|
||||
if (category === 'Decision-maker') return 'agent';
|
||||
return 'agent';
|
||||
}
|
||||
|
||||
function nodeOutputType(kind: WorkflowNodeKind): WorkflowPortType {
|
||||
if (kind === 'environment') return 'ENV_CONTEXT';
|
||||
if (kind === 'space') return 'SPACE_CONTEXT';
|
||||
if (kind === 'agent') return 'AGENT_CONTEXT';
|
||||
return 'REPORT';
|
||||
}
|
||||
|
||||
function variableScope(variable: VisaVariable): AttributeScope {
|
||||
if (variable.valueSource.startsWith('derived:')) return 'derived';
|
||||
if (variable.variableType.startsWith('exog')) return 'exogenous';
|
||||
return 'endogenous';
|
||||
}
|
||||
|
||||
function workflowDataType(dataType: VisaDataType) {
|
||||
if (dataType === 'List[Integer]' || dataType === 'Matrix[Integer]') return 'String';
|
||||
return dataType;
|
||||
}
|
||||
|
||||
function findAgent(spec: VisaModelSpec, agentId: string) {
|
||||
return spec.agents.find((agent) => agent.id === agentId);
|
||||
}
|
||||
|
||||
function findVariable(spec: VisaModelSpec, variableId: string) {
|
||||
return spec.variables.find((variable) => variable.id === variableId);
|
||||
}
|
||||
|
||||
function findFunction(spec: VisaModelSpec, functionId: string) {
|
||||
return spec.internalFunctions.find((fn) => fn.id === functionId);
|
||||
}
|
||||
|
||||
function variableLabel(spec: VisaModelSpec, variableId: string) {
|
||||
const variable = findVariable(spec, variableId);
|
||||
return variable ? variable.name : variableId;
|
||||
}
|
||||
|
||||
function variableSchema(spec: VisaModelSpec, variableId: string) {
|
||||
const variable = findVariable(spec, variableId);
|
||||
return variable ? variable.dataType : '-';
|
||||
}
|
||||
|
||||
function buildNodes(spec: VisaModelSpec): WorkflowNode[] {
|
||||
return [...spec.agents]
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.map((agent, index) => {
|
||||
const kind = agentCategoryToNodeKind(agent.category);
|
||||
return {
|
||||
id: agent.id,
|
||||
title: agent.name,
|
||||
subtitle: agent.description,
|
||||
kind,
|
||||
x: -220 + index * 220,
|
||||
y: kind === 'space' ? 80 : -95,
|
||||
status: 'ready',
|
||||
inputs: kind === 'environment' ? [] : [{ id: 'context', label: 'context', type: 'ENV_CONTEXT' }],
|
||||
outputs: [{ id: 'state', label: `${agent.name} state`, type: nodeOutputType(kind) }],
|
||||
config: [
|
||||
{ label: 'VISA category', value: agent.category },
|
||||
{ label: 'symbol set', value: agent.symbolSet },
|
||||
{ label: 'quantity', value: `${agent.quantitySymbol} = ${agent.quantityValue}` },
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildEdges(spec: VisaModelSpec) {
|
||||
const environment = spec.agents.find((agent) => agent.category === 'Environment');
|
||||
const space = spec.agents.find((agent) => agent.category === 'Space');
|
||||
const decisionMakers = spec.agents.filter((agent) => agent.category === 'Decision-maker');
|
||||
const edges = [];
|
||||
|
||||
if (environment && space) {
|
||||
edges.push({
|
||||
id: 'visa-edge-environment-space',
|
||||
source: environment.id,
|
||||
target: space.id,
|
||||
type: 'ENV_CONTEXT' as const,
|
||||
label: 'VISA T1 lifecycle context',
|
||||
});
|
||||
}
|
||||
|
||||
if (space) {
|
||||
decisionMakers.forEach((agent) => {
|
||||
edges.push({
|
||||
id: `visa-edge-${space.id}-${agent.id}`,
|
||||
source: space.id,
|
||||
target: agent.id,
|
||||
type: 'SPACE_CONTEXT' as const,
|
||||
label: 'VISA T3 sensing context',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
function buildObservableSection(spec: VisaModelSpec, agent: VisaAgent): BehaviorSection {
|
||||
return {
|
||||
id: `visa-observable-${agent.id}`,
|
||||
nodeId: agent.id,
|
||||
kind: 'observable_pool',
|
||||
title: 'VISA T3 Sensing',
|
||||
description: 'View-model projection of backend sensing relations authorized for this agent.',
|
||||
items: spec.sensingRelations
|
||||
.filter((relation) => relation.observerAgentId === agent.id)
|
||||
.flatMap((relation) => relation.observedVariableIds.map((variableId) => {
|
||||
const variable = findVariable(spec, variableId);
|
||||
const observedAgent = findAgent(spec, relation.observedAgentId);
|
||||
const prefix = relation.scope === 'self' ? 'self' : observedAgent?.name ?? relation.observedAgentId;
|
||||
return {
|
||||
label: `${prefix}.${variable?.name ?? variableId}`,
|
||||
source: relation.condition,
|
||||
value: variable?.dataType ?? relation.accessType,
|
||||
};
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function buildFunctionSection(spec: VisaModelSpec, fn: VisaInternalFunction): BehaviorSection {
|
||||
const updateIds = [...fn.selfStateUpdateVariableIds, ...fn.externalEffectVariableIds];
|
||||
return {
|
||||
id: `visa-function-${fn.functionCode}`,
|
||||
nodeId: fn.agentId,
|
||||
kind: 'calculation',
|
||||
title: `${fn.functionCode} ${fn.name}`,
|
||||
description: fn.description,
|
||||
items: [
|
||||
{
|
||||
label: 'decision basis',
|
||||
source: 'VISA T2/T3',
|
||||
value: fn.decisionBasisVariableIds.map((id) => variableLabel(spec, id)).join(', ') || '-',
|
||||
},
|
||||
{
|
||||
label: 'method',
|
||||
source: 'VISA T4',
|
||||
value: fn.method,
|
||||
},
|
||||
{
|
||||
label: 'updates',
|
||||
source: 'VISA T4',
|
||||
value: updateIds.map((id) => variableLabel(spec, id)).join(', ') || '-',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function buildBehaviorSections(spec: VisaModelSpec): BehaviorSection[] {
|
||||
return spec.agents
|
||||
.filter((agent) => agent.category === 'Decision-maker')
|
||||
.flatMap((agent) => [
|
||||
buildObservableSection(spec, agent),
|
||||
...spec.internalFunctions
|
||||
.filter((fn) => fn.agentId === agent.id)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.map((fn) => buildFunctionSection(spec, fn)),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildAssociatedData(spec: VisaModelSpec): AssociatedDataRow[] {
|
||||
return spec.associatedData.map((data) => ({
|
||||
id: data.dataCode,
|
||||
name: data.title,
|
||||
type: data.dataType,
|
||||
sourceType: data.collectionMethod,
|
||||
sourceName: data.source,
|
||||
sourceTable: 'visa_associated_data',
|
||||
sourceColumn: data.dataCode,
|
||||
ownerAgent: '-',
|
||||
relatedVariable: '-',
|
||||
usedByBehavior: '-',
|
||||
updateFrequency: data.temporalType,
|
||||
versionKey: spec.modelVersionId,
|
||||
qualityStatus: data.availability === 'Private' ? 'warning' : 'ok',
|
||||
}));
|
||||
}
|
||||
|
||||
function buildInputOutputRows(spec: VisaModelSpec): InputOutputRow[] {
|
||||
const inputRows: InputOutputRow[] = spec.inputs.map((input) => {
|
||||
const agent = findAgent(spec, input.agentId);
|
||||
const variable = findVariable(spec, input.variableId);
|
||||
return {
|
||||
id: input.id,
|
||||
componentType: 'VISA Input',
|
||||
componentId: input.variableId,
|
||||
componentName: variable?.name ?? input.symbol,
|
||||
direction: 'input',
|
||||
ioName: input.symbol,
|
||||
ioDataType: variable?.dataType ?? 'String',
|
||||
schema: input.valueOrDistribution,
|
||||
sourceComponent: input.dataSourceId || input.derivation,
|
||||
sourceTable: 'visa_inputs',
|
||||
targetComponent: agent?.name ?? input.agentId,
|
||||
targetTable: 'visa_variables',
|
||||
required: 'true',
|
||||
defaultValue: input.valueOrDistribution,
|
||||
snapshotPolicy: 'run_start',
|
||||
};
|
||||
});
|
||||
|
||||
const outputRows: InputOutputRow[] = spec.outputs.map((output) => ({
|
||||
id: output.id,
|
||||
componentType: 'VISA Output',
|
||||
componentId: output.symbol,
|
||||
componentName: output.indicatorName,
|
||||
direction: 'output',
|
||||
ioName: output.symbol,
|
||||
ioDataType: output.dataType,
|
||||
schema: output.formula,
|
||||
sourceComponent: 'VISA runtime',
|
||||
sourceTable: 'visa_outputs',
|
||||
targetComponent: 'Simulation result',
|
||||
targetTable: 'run_outputs',
|
||||
required: 'true',
|
||||
defaultValue: '-',
|
||||
snapshotPolicy: output.frequency === '-1' ? 'final_step' : `every_${output.frequency}_step`,
|
||||
}));
|
||||
|
||||
return [...inputRows, ...outputRows];
|
||||
}
|
||||
|
||||
function buildScheduleRows(spec: VisaModelSpec): ScheduleRow[] {
|
||||
return spec.scheduleSteps.map((step) => {
|
||||
const agent = findAgent(spec, step.agentId);
|
||||
const fn = findFunction(spec, step.functionId);
|
||||
return {
|
||||
id: step.id,
|
||||
order: String(step.stepNumber),
|
||||
phase: step.stepNumber === 1 ? 'initialize' : 'runtime',
|
||||
actorAgent: agent?.name ?? step.agentId,
|
||||
targetBehavior: fn?.name ?? step.functionId,
|
||||
trigger: step.condition,
|
||||
executionMode: step.executionMode,
|
||||
timeUnit: 'tick',
|
||||
condition: step.condition,
|
||||
repeatRule: step.condition === 'model_start' ? 'once' : 'every tick',
|
||||
dependsOn: step.stepNumber === 1 ? '-' : `sched-${step.stepNumber - 1}`,
|
||||
writesTo: fn?.selfStateUpdateVariableIds.map((id) => variableLabel(spec, id)).join(', ') || '-',
|
||||
notes: step.executionParameters,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildValidationRows(spec: VisaModelSpec): ValidationRow[] {
|
||||
return spec.validations.map((validation) => ({
|
||||
id: validation.validationCode,
|
||||
targetType: validation.level,
|
||||
targetId: validation.validationObject,
|
||||
targetName: validation.validationObject,
|
||||
validationType: 'scientific_validation',
|
||||
method: validation.method,
|
||||
metric: validation.indicator,
|
||||
baselineData: validation.benchmarkDataId,
|
||||
threshold: validation.passingCondition,
|
||||
frequency: 'on demand',
|
||||
resultField: `validation.${validation.validationCode}`,
|
||||
severity: validation.status === 'failed' ? 'error' : 'warning',
|
||||
status: validation.status === 'failed' ? 'error' : validation.status === 'passed' ? 'ok' : 'warning',
|
||||
notes: validation.notes,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildWorkflowFromVisaSpec(spec: VisaModelSpec): WorkflowModel {
|
||||
return {
|
||||
nodes: buildNodes(spec),
|
||||
edges: buildEdges(spec),
|
||||
parameters: spec.variables.map((variable) => ({
|
||||
id: variable.id,
|
||||
nodeId: variable.agentId,
|
||||
name: variable.name,
|
||||
type: workflowDataType(variable.dataType),
|
||||
scope: variableScope(variable),
|
||||
source: variable.valueSource,
|
||||
value: variable.symbol,
|
||||
status: 'ok',
|
||||
description: variable.description,
|
||||
})),
|
||||
behaviorSections: buildBehaviorSections(spec),
|
||||
schedules: buildScheduleRows(spec),
|
||||
associatedData: buildAssociatedData(spec),
|
||||
inputOutputs: buildInputOutputRows(spec),
|
||||
validations: buildValidationRows(spec),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user