{"slug":"strategy/product-strategy/SPEC_AMENDMENTS_v1","title":"Specification Amendments — Implementation Corrections","section":"strategy","sectionLabel":"Strategy","status":"unclassified","type":"document","path":"strategy/product-strategy/SPEC_AMENDMENTS_v1.mdx","bytes":23978,"description":null,"updated":null,"source":"integratewise/Integratewise-docs-wiki","content":"\n**Status:** CANONICAL AMENDMENT  \n**Date:** 2026-08-06  \n**Authority:** Supersedes conflicting statements in all prior architecture documents  \n**Scope:** 15 required corrections to make the frozen architecture internally consistent, compatible with the Cloudflare execution model, and suitable as the implementation baseline\n\n---\n\n## Verdict\n\nThe core model is sound:\n\n- Neon is the control-plane authority.\n- Each tenant receives an isolated D1 operational Spine.\n- The Gateway derives authority server-side.\n- The Workbench consumes projections only.\n- AI proposes; Governance decides; Execution acts; Reconciliation verifies.\n- Provider acceptance is never presented as completed work.\n\nThe following corrections are incorporated as implementation-final amendments.\n\n---\n\n## Amendment 1: Per-Tenant D1 Routing Mechanism\n\n### Problem\n\nPrior statements implied that `platform-gateway` resolves `tenant_id` through KV and dynamically binds the corresponding D1 database. A standard Worker accesses D1 through bindings attached to its deployed environment. It cannot turn a D1 database ID retrieved from KV into a new native `D1Database` binding during the request.\n\n### Correction\n\nCloudflare's supported dynamic pattern is a Workers for Platforms dispatch namespace: the Gateway dispatches to a tenant runtime Worker, and that Worker has its tenant D1 database statically attached as a binding.\n\n**Frozen topology:**\n\n```text\nRequest\n  ↓\nPlatform Gateway\n  ├── validates identity\n  ├── derives tenant_id\n  ├── reads tenant runtime routing record\n  └── dispatches by runtime_script_name\n          ↓\nTenant Runtime Worker\n  ├── static env.SPINE_DB → tenant D1\n  ├── tenant runtime configuration\n  └── service bindings → shared platform services\n```\n\n**Gateway dispatch pattern:**\n\n```typescript\nconst tenantRuntime = env.TENANT_DISPATCHER.get(runtimeScriptName);\nreturn tenantRuntime.fetch(sanitizedRequest);\n```\n\n**`tenant_runtime_registry` schema (Neon control plane):**\n\n| Field                    | Type                               | Purpose                                           |\n| ------------------------ | ---------------------------------- | ------------------------------------------------- |\n| `tenant_id`              | TEXT PK                            | Tenant identifier                                 |\n| `runtime_script_name`    | TEXT NOT NULL UNIQUE               | Workers for Platforms script name                 |\n| `dispatch_namespace`     | TEXT NOT NULL                      | Dispatch namespace identifier                     |\n| `d1_database_id`         | TEXT NOT NULL                      | Cloudflare D1 database resource ID                |\n| `runtime_version`        | TEXT NOT NULL                      | Deployed runtime version                          |\n| `deployment_status`      | TEXT NOT NULL                      | `active` / `deploying` / `draining` / `suspended` |\n| `region_or_jurisdiction` | TEXT                               | Data residency constraint if applicable           |\n| `updated_at`             | TIMESTAMPTZ NOT NULL DEFAULT now() | Last modification                                 |\n\n`edge_routing_key` equals `runtime_script_name`. The binding name inside every tenant Worker is stable (`SPINE_DB`); only the attached resource differs.\n\n---\n\n## Amendment 2: Isolation Claim Correction\n\n### Problem\n\nPrior wording claimed \"cryptographic and physical boundary isolation.\" Cloudflare documents that D1 data is encrypted at rest and supports database-per-tenant scale-out, but does not establish that each tenant receives dedicated physical hardware or a tenant-specific encryption key.\n\n### Correction\n\nReplace all instances of \"cryptographic and physical boundary isolation\" with:\n\n> Independent database-resource isolation with separate D1 namespaces, independent lifecycle controls, and encryption at rest.\n\n**The correct guarantee is:**\n\n> Cross-tenant SQL access is eliminated from normal application query paths because each tenant runtime is bound to a different D1 database resource.\n\nAuthorization remains necessary because users and workspaces within the same tenant still require logical isolation.\n\n---\n\n## Amendment 3: D1 Fleet Capacity Policy\n\n### Problem\n\nThe database-per-tenant decision is valid but has explicit capacity boundaries. Current paid-plan limits include up to 50,000 databases per account, a 10 GB maximum per database, and a total account storage ceiling. No capacity policy was defined.\n\n### Correction\n\n**D1 Fleet Capacity Policy:**\n\n| Metric                 | Warning threshold | Hard threshold            |\n| ---------------------- | ----------------- | ------------------------- |\n| Tenant database size   | 7 GB              | 9 GB (migration required) |\n| Account database count | 40,000 databases  | 47,500 databases          |\n\n**Overflow strategy:**\n\n- Additional Cloudflare account / account shard\n- Migration of high-volume tenant to a compatible Spine adapter\n\n**Routing fields:**\n\n| Field                      | Purpose                                           |\n| -------------------------- | ------------------------------------------------- |\n| `control_plane_account_id` | Cloudflare account owning this tenant's resources |\n| `dispatch_namespace`       | Workers for Platforms namespace                   |\n| `runtime_script_name`      | Tenant Worker script identity                     |\n| `d1_database_id`           | D1 database resource                              |\n\nThe logical Spine contract must remain unchanged when a tenant moves between physical storage adapters.\n\n---\n\n## Amendment 4: Catalog Versioning Schemas\n\n### Problem\n\nThe current schema defines `connector_catalog` and `plugin_catalog` with `PRIMARY KEY connector_key` / `PRIMARY KEY plugin_key`. A primary key on the unversioned key permits only one row per connector or plugin, preventing version management.\n\nAdditionally, `BIGINT AUTOINCREMENT` is Neon-incompatible syntax.\n\n### Correction\n\nUse separate identity and release tables:\n\n```sql\n-- Connector identity\nCREATE TABLE connector_catalog (\n    connector_key TEXT PRIMARY KEY,\n    display_name TEXT NOT NULL,\n    category TEXT NOT NULL,\n    developer_id TEXT NOT NULL,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\n-- Connector versions\nCREATE TABLE connector_versions (\n    connector_key TEXT NOT NULL REFERENCES connector_catalog(connector_key),\n    version TEXT NOT NULL,\n    auth_type TEXT NOT NULL,\n    spec_json JSONB NOT NULL,\n    is_published BOOLEAN NOT NULL DEFAULT false,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n    PRIMARY KEY (connector_key, version)\n);\n```\n\nApply the same pattern to `plugin_catalog` / `plugin_versions` and optionally `mcp_tools` / `mcp_tool_versions`.\n\nReplace all instances of `BIGINT AUTOINCREMENT` with PostgreSQL-native syntax:\n\n```sql\nBIGINT GENERATED ALWAYS AS IDENTITY\n```\n\n---\n\n## Amendment 5: Idempotency Persistence\n\n### Problem\n\nCloudflare Queues provides at-least-once delivery, meaning duplicate delivery is possible. The specification relies on the command interface for idempotency without defining durable deduplication state.\n\n### Correction\n\nEvery event consumer and execution handler must be idempotent. Add an operational table to each tenant D1 Spine:\n\n```sql\nCREATE TABLE processed_commands (\n    command_id TEXT PRIMARY KEY NOT NULL,\n    idempotency_key TEXT NOT NULL,\n    command_type TEXT NOT NULL,\n    actor_id TEXT NOT NULL,\n    payload_hash TEXT NOT NULL,\n    status TEXT NOT NULL CHECK (\n        status IN ('processing', 'completed', 'failed')\n    ),\n    result_ref TEXT,\n    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    completed_at TEXT\n);\n\nCREATE UNIQUE INDEX idx_processed_commands_idempotency\nON processed_commands(command_type, actor_id, idempotency_key);\n```\n\nA reused idempotency key with a different payload hash must be rejected as a conflict.\n\n---\n\n## Amendment 6: Transactional Outbox\n\n### Problem\n\nThe requirement that audit or event-publishing failure stops a transaction cannot be guaranteed by publishing directly to an asynchronous queue after a database write.\n\n### Correction\n\nUse a transactional outbox pattern:\n\n```text\nD1 transaction\n  ├── mutate domain record\n  ├── create evidence/source link where required\n  ├── append local audit event\n  └── append outbox event\n          ↓\nOutbox publisher\n          ↓\nCloudflare Queue\n          ↓\nDomain consumers\n```\n\nD1 batch operations execute as transactions and roll back the batch if a statement fails, making them suitable for atomic domain-write-plus-outbox insertion.\n\n**Outbox table (per-tenant D1):**\n\n```sql\nCREATE TABLE event_outbox (\n    event_id TEXT PRIMARY KEY NOT NULL,\n    event_type TEXT NOT NULL,\n    aggregate_type TEXT NOT NULL,\n    aggregate_id TEXT NOT NULL,\n    workspace_id TEXT,\n    payload_json TEXT NOT NULL,\n    payload_hash TEXT NOT NULL,\n    status TEXT NOT NULL DEFAULT 'pending',\n    attempt_count INTEGER NOT NULL DEFAULT 0,\n    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    published_at TEXT\n);\n```\n\n---\n\n## Amendment 7: Dedicated WorkService\n\n### Problem\n\nThe current design makes `ExecutionService` own tasks, `work_items`, and assignments. This conflates two different domains: human operational work (tasks, queues, assignments, deadlines, ownership) and machine execution (provider dispatch, invocation attempts, retries, compensation).\n\n### Correction\n\nIntroduce `WorkService`:\n\n**Commands:**\n\n- `CreateTask`, `UpdateTask`, `AssignWork`, `CompleteWork`, `ReopenWork`, `RouteQueueItem`\n\n**Queries:**\n\n- `GetTask`, `ListAssignedWork`, `ListQueueItems`, `GetWorkTimeline`\n\n**Storage:**\n\n- `tasks`, `work_items`, `assignments`, `queues`, `task_dependencies`\n\n**Events:**\n\n- `work.created`, `work.assigned`, `work.started`, `work.completed`, `work.blocked`\n\n`ExecutionService` retains ownership of execution state and `capability_invocations`.\n\n---\n\n## Amendment 8: Capability Invocation Ownership\n\n### Problem\n\nThe spec gives `capability_invocations` to both `CapabilityService` and `ExecutionService`.\n\n### Correction\n\n**Frozen ownership:**\n\n| Service               | Owns                                                                                                     |\n| --------------------- | -------------------------------------------------------------------------------------------------------- |\n| **CapabilityService** | `capability_registry`, `capability_grants`, capability dependencies, capability schemas                  |\n| **ExecutionService**  | `capability_invocations`, provider dispatch attempts, execution status, provider acceptance, retry state |\n\n**Recommended invocation schema:**\n\n| Field                      | Type          | Purpose                                                        |\n| -------------------------- | ------------- | -------------------------------------------------------------- |\n| `invocation_id`            | TEXT PK       | Unique invocation identifier                                   |\n| `command_id`               | TEXT NOT NULL | Originating command                                            |\n| `capability_key`           | TEXT NOT NULL | Capability being invoked                                       |\n| `capability_version`       | TEXT NOT NULL | Resolved version                                               |\n| `connection_id`            | TEXT          | Provider connection                                            |\n| `actor_id`                 | TEXT NOT NULL | Requesting actor                                               |\n| `workspace_id`             | TEXT          | Workspace scope                                                |\n| `request_payload_hash`     | TEXT NOT NULL | SHA-256 of request                                             |\n| `status`                   | TEXT NOT NULL | `pending` / `dispatched` / `accepted` / `completed` / `failed` |\n| `provider_reference_id`    | TEXT          | External reference from provider                               |\n| `provider_acceptance_json` | TEXT          | Raw provider response                                          |\n| `started_at`               | TEXT NOT NULL | Dispatch time                                                  |\n| `accepted_at`              | TEXT          | Provider acceptance time                                       |\n| `failed_at`                | TEXT          | Failure time                                                   |\n| `failure_class`            | TEXT          | `retryable` / `terminal` / `ambiguous`                         |\n\nCapabilityService validates what may execute. ExecutionService records and manages the actual execution.\n\n---\n\n## Amendment 9: Reconciliation Model Strengthening\n\n### Problem\n\nThe reconciliation state machine is missing transitions, observation history is mutable, timestamps don't auto-update, and event contracts are incomplete.\n\n### Correction\n\n**Missing state transitions:**\n\n```text\npartially_verified → observing\npartially_verified → verified\npartially_verified → failed\n\ncompensation_required → compensated\ncompensation_required → compensation_failed\n```\n\nNot every failure requires compensation. `failed` may be terminal when no external state changed.\n\n**Observation history table (per-tenant D1):**\n\n```sql\nCREATE TABLE reconciliation_observations (\n    observation_id TEXT PRIMARY KEY NOT NULL,\n    reconciliation_run_id TEXT NOT NULL,\n    attempt_number INTEGER NOT NULL,\n    observed_state TEXT,\n    provider_status TEXT,\n    matches_expected INTEGER NOT NULL DEFAULT 0,\n    evidence_ref TEXT,\n    observed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    FOREIGN KEY (reconciliation_run_id)\n      REFERENCES reconciliation_runs(id)\n);\n\nCREATE UNIQUE INDEX idx_reconciliation_observation_attempt\nON reconciliation_observations(reconciliation_run_id, attempt_number);\n```\n\n**Timestamp correction:** Every reconciliation update must explicitly set `updated_at = CURRENT_TIMESTAMP`. D1 does not auto-update `DEFAULT CURRENT_TIMESTAMP` on updates.\n\n**Expanded event contract for `capability.reconciled`:**\n\n| Status                  | Meaning                                       |\n| ----------------------- | --------------------------------------------- |\n| `VERIFIED`              | Provider state matches expected outcome       |\n| `PARTIALLY_VERIFIED`    | Some but not all expected state confirmed     |\n| `FAILED`                | Verification failed, no external state change |\n| `COMPENSATION_REQUIRED` | External state changed but outcome incorrect  |\n| `COMPENSATED`           | Attempted action was reversed                 |\n| `COMPENSATION_FAILED`   | Reversal attempt failed                       |\n\nOnly `VERIFIED` produces a normal completed state. `COMPENSATED` means the attempted action was reversed, not successfully completed.\n\n---\n\n## Amendment 10: Complete Event Envelope\n\n### Problem\n\nCross-service events lack common metadata fields required for deduplication and complete execution tracing.\n\n### Correction\n\nEvery cross-service event must include:\n\n```typescript\nexport interface PlatformEvent<TType extends string, TPayload> {\n  readonly eventId: string;\n  readonly eventType: TType;\n  readonly eventVersion: number;\n\n  readonly tenantId: string;\n  readonly workspaceId?: string;\n\n  readonly aggregateType?: string;\n  readonly aggregateId?: string;\n\n  readonly causationId?: string;\n  readonly correlationId: string;\n  readonly commandId?: string;\n\n  readonly actor?: {\n    readonly actorId: string;\n    readonly actorType: \"user\" | \"agent\" | \"system\" | \"mcp_client\";\n  };\n\n  readonly payload: TPayload;\n  readonly occurredAt: string;\n  readonly producer: string;\n}\n```\n\nWithout `eventId`, `eventVersion`, `correlationId`, and `causationId`, deduplication and complete execution tracing will be unnecessarily difficult.\n\n---\n\n## Amendment 11: Consistent Tenant Identity in D1\n\n### Problem\n\nAlthough each D1 database belongs to one tenant, the current schema inconsistently includes `tenant_id`.\n\n### Correction\n\n**Frozen rule:**\n\n> Every durable D1 business record carries `tenant_id`, even in a per-tenant database.\n\n**Rationale:**\n\n- Defense in depth\n- Portable exports\n- Simpler migration to another Spine adapter\n- Self-identifying evidence\n- Safer telemetry replication\n- Easier forensic inspection\n\nThe tenant runtime must inject `tenant_id`; clients must never supply it as an authoritative value.\n\n---\n\n## Amendment 12: Search Service Isolation\n\n### Problem\n\nPrior references to a \"Cross-Tenant Unified Vectorize/D1 Indexer\" contradict the per-tenant isolation model.\n\n### Correction\n\nRename to **Tenant-Scoped Unified Search and Indexing Service**.\n\nCloudflare AI Search supports tenant isolation through separate instances per tenant or a shared instance with mandatory metadata filtering. Separate tenant instances align most closely with the per-tenant D1 isolation model.\n\n**Recommended V0:**\n\n| Component           | Value                                     |\n| ------------------- | ----------------------------------------- |\n| AI Search namespace | `integratewise-tenants`                   |\n| AI Search instance  | `tenant-{tenant_id}`                      |\n| R2 source           | shared bucket, `/tenants/{tenant_id}/...` |\n\n`SearchService` derives tenant from the authenticated runtime, resolves the instance by `tenant_id`, and never accepts client-selected instance names.\n\nCross-tenant analytics must use sanitized telemetry, not cross-tenant semantic retrieval.\n\n---\n\n## Amendment 13: Cached Projection Fallback Policy\n\n### Problem\n\nThe statement \"query generation failure falls back to cached materialization\" is unsafe — it may silently display stale operational state.\n\n### Correction\n\n**Explicit fallback policy:**\n\n```text\nFresh projection succeeds\n  → freshness = CURRENT\n\nFresh projection fails but policy permits stale read\n  → return cached projection\n  → freshness = STALE\n  → stale_since = <timestamp>\n  → stale_reason = <reason>\n  → disable unsafe actions as required\n\nNo permitted cached projection\n  → explicit projection failure\n```\n\nA stale projection must never silently masquerade as current state. The Workbench must display freshness indicators when serving stale data.\n\n---\n\n## Amendment 14: Cryptographic Approval and Audit Semantics\n\n### Problem\n\nA SHA-256 checksum verifies content integrity but does not itself provide non-repudiation. Claims of \"cryptographic approval record,\" \"non-repudiable audit trail,\" and \"cryptographic signature\" were underspecified.\n\n### Correction\n\n**Required approval record fields:**\n\n| Field                 | Purpose                      |\n| --------------------- | ---------------------------- |\n| `payload_hash`        | SHA-256 of canonical payload |\n| `previous_event_hash` | Hash-chain predecessor       |\n| `signature_algorithm` | Algorithm identifier         |\n| `signature_key_id`    | Signing key identifier       |\n| `signature`           | Signature value              |\n| `signed_at`           | Signature timestamp          |\n\n**Required definitions:**\n\n- Canonical payload encoding (deterministic serialization)\n- Signature algorithm selection and rotation policy\n- Key management and rotation procedure\n- Verification procedure\n- Hash-chain construction rules\n\nHuman approval identity must come from the validated session and be bound into the signed canonical payload.\n\n---\n\n## Amendment 15: Frontend Deployment Target\n\n### Problem\n\nThe repository tree references both Cloudflare Pages and Vercel for the Workbench, creating ambiguity about the canonical deployment target.\n\n### Correction\n\n**Frozen deployment targets:**\n\n| Surface                      | Target                                                      |\n| ---------------------------- | ----------------------------------------------------------- |\n| Product / Workbench          | Vercel microfrontends                                       |\n| Platform Gateway and runtime | Cloudflare Workers                                          |\n| Tenant operational runtime   | Workers for Platforms tenant Worker + per-tenant D1 binding |\n\nThe Product remains replaceable because it communicates only through the Gateway and projection contracts.\n\n---\n\n## Corrected Runtime Topology\n\n```text\n┌──────────────────────────────────────────────────────────┐\n│                 WORKBENCH / MCP / HOST                   │\n└───────────────────────────┬──────────────────────────────┘\n                            ▼\n┌──────────────────────────────────────────────────────────┐\n│                    PLATFORM GATEWAY                      │\n│ Auth · Tenant derivation · Rate limits · Zero Trust      │\n└───────────────────────────┬──────────────────────────────┘\n                            │ tenant runtime dispatch\n                            ▼\n┌──────────────────────────────────────────────────────────┐\n│            WORKERS FOR PLATFORMS DISPATCHER              │\n│ env.TENANT_DISPATCHER.get(runtime_script_name)           │\n└───────────────────────────┬──────────────────────────────┘\n                            ▼\n┌──────────────────────────────────────────────────────────┐\n│                 TENANT RUNTIME WORKER                    │\n│ Static binding: env.SPINE_DB                             │\n│ Runtime tenant identity fixed at deployment              │\n└───────────────────────────┬──────────────────────────────┘\n                            ▼\n┌──────────────────────────────────────────────────────────┐\n│                  PER-TENANT D1 SPINE                     │\n│ Work · entities · projections · memory · governance      │\n│ evidence refs · outbox · execution · reconciliation      │\n└───────────────────────────┬──────────────────────────────┘\n                            │\n       ┌────────────────────┼────────────────────────┐\n       ▼                    ▼                        ▼\n Shared Workers       Shared Cloudflare       Connected providers\n domain services      infrastructure          SaaS · MCP · APIs\n```\n\n---\n\n## Final Freeze Statement\n\n> Neon controls the tenant. A dispatched tenant runtime Worker operates the tenant. Its statically bound D1 database holds the V0 operational Spine. The Spine contract governs all access.\n\nWith these amendments, the architecture becomes internally consistent, compatible with the current Cloudflare execution model, and suitable as the baseline for migrations, typed contracts, and the first vertical implementation loop.\n"}