Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions openwiki/.last-update.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"updatedAt": "2026-09-05T12:23:10.048Z",
"updatedAt": "2026-09-06T12:40:17.952Z",
"command": "update",
"gitHead": "d39ecfb983cf472121fc2641d283a8da7b288073",
"gitHead": "aba939a097febf62559dbbff687cfdfe25a4fd93",
"model": "google/gemini-3.7-flash",
"status": "complete",
"language": "en"
Expand Down
7 changes: 6 additions & 1 deletion openwiki/architecture/adapters-and-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ Adapters implement the application-layer repository ports against external syste

### Structured model: OpenRouter

The provider-neutral structured-model port is implemented with LangChain's `ChatOpenRouter`. The operator selects the model through `STORYRAIL_EVIDENCE_PREPARATION_MODEL`; final prepared documents are validated by StoryRail before an immutable attempt is persisted.
The provider-neutral structured-model port is implemented with LangChain's `ChatOpenRouter` in `src/adapters/model/openrouter-structured-model.ts`:
- Accepts `resolveApiKey`, `model`, optional custom `baseUrl` (configured globally via `STORYRAIL_OPENROUTER_BASE_URL` in `src/runtime/openrouter-configuration.ts`), and timeout parameters.
- Normalizes system prompts, input data, and strict Zod output schemas (`name: "storyrail_structured_response", strict: true`).
- Validates model output against strict schema and failure mappers before returning structured results.

### Credential storage

Expand Down Expand Up @@ -70,6 +73,8 @@ All PostgreSQL adapters share a defensive pattern: they serialize the domain obj
| `postgres-site-settings-persistence.ts` | `storyrail.site_settings` | `SiteSettingsRepository` |
| `postgres-policy-run-repository.ts` | `storyrail.policy_runs` | `PolicyRunRepository` |
| `postgres-story-delivery-repository.ts` | `storyrail.story_deliveries` | `StoryDeliveryRepository` |
| `postgres-legacy-delivery-mapping-resolution-repository.ts` | `storyrail.legacy_delivery_mapping_resolutions` | `LegacyDeliveryMappingResolutionRepository` |
| `postgres-story-delivery-reconciliation-repository.ts` | `storyrail.story_delivery_reconciliations` | `StoryDeliveryReconciliationRepository` |

### Model catalog: OpenRouter

Expand Down
35 changes: 29 additions & 6 deletions openwiki/architecture/application-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,33 @@ Rejection is terminal and does not contact a model. It preserves all existing wo
3. Finds the latest Article Revision (`STORY_HAS_NO_ARTICLE`).
4. Resolves the destination directory (`DeliveryDestinationDirectory`) to construct the destination instance with site settings and credentials (including its `instanceId`).
5. Derives the slug via `storyDeliverySlug(revision.headline)`.
6. Checks previous deliveries for that Story and destination instance (`findLatestSucceeded({ storyId, destinationInstanceId })`).
- If no prior delivery for the current instance is found, checks for unbound legacy deliveries (`findLatestLegacySucceeded({ storyId, destination })`). If a legacy mapping exists, it fails closed with `DESTINATION_MAPPING_REQUIRES_REVIEW` so operators can review before writing to a new instance.
- If a prior delivery for the instance exists, uses its `remoteId` to decide whether this is a `create` (first delivery, `remoteId: null`) or `update` (patching an existing remote page using its prior `remoteId`).
7. **Durability first**: Records the delivery row as `outcome: "running"` with `StoryDeliveryRepository.append` before making the external HTTP call.
8. Invokes `destination.deliver(...)`.
9. Updates the delivery record with `StoryDeliveryRepository.complete` to `succeeded` with `remoteId` (parsed from the provider response) or `failed` with failure details. Failed deliveries are never retried silently.
6. **Reconciliation check**: Checks for unresolved deliveries for that Story and destination instance (`findLatestUnresolved({ storyId, destinationInstanceId })`). If an unresolved delivery (`running` or `unknown`) exists, it verifies whether a matching `StoryDeliveryReconciliation` exists. If unreconciled, the workflow fails closed with `DESTINATION_RECONCILIATION_REQUIRED` to avoid duplicate page creation or overwriting the wrong post.
7. **Legacy mapping check**: Checks previous deliveries for that Story and destination instance (`findLatestSucceeded({ storyId, destinationInstanceId })`).
- If no prior delivery for the current instance is found, checks for unbound legacy deliveries (`findLatestLegacySucceeded({ storyId, destination })`). If a legacy mapping exists, it checks for an existing `LegacyDeliveryMappingResolution`. If unreviewed, it fails closed with `DESTINATION_MAPPING_REQUIRES_REVIEW` so operators can review before writing to a new instance.
- Determines the effective `remoteId`: prior successful delivery ID, confirmed legacy resolution ID, or reconciled ID. If present, the operation is `update`; otherwise `create` (`remoteId: null`).
8. **Durability first**: Records the delivery row as `outcome: "running"` with `StoryDeliveryRepository.append` before making the external HTTP call.
9. Invokes `destination.deliver(...)`.
10. Updates the delivery record with `StoryDeliveryRepository.complete` to:
- `succeeded` with `remoteId` and result when the remote system confirms acceptance.
- `failed` with failure details when the remote system explicitly rejects the request.
- `unknown` with `uncertainty` details when the connection drops or the remote body is unparseable, immediately gating future deliveries until reconciled.

## Legacy delivery mapping resolution workflow

`src/application/story-deliveries/resolve-legacy-delivery-mapping.ts` — `createResolveLegacyDeliveryMapping` records an operator's decision (`confirm` or `dismiss`) on an ambiguous legacy delivery:
1. Validates that the Story exists and resolves the current configured destination.
2. Finds the succeeded legacy delivery row, ensuring it is unbound (`destinationInstanceId: null`) and has a valid `remoteId`.
3. Verifies that the legacy mapping is the latest for that destination (`LEGACY_DELIVERY_MAPPING_STALE`).
4. Constructs and persists a `LegacyDeliveryMappingResolution` record via `LegacyDeliveryMappingResolutionRepository.append`.

## Story delivery reconciliation workflow

`src/application/story-deliveries/reconcile-story-delivery.ts` — `createReconcileStoryDelivery` records an operator's decision (`delivered` or `not_delivered`) on an ambiguous or uncertain delivery outcome:
1. Validates that the Story exists and resolves the current configured destination.
2. Finds the unresolved delivery row (`findUnresolvedById` and `findLatestUnresolved`), ensuring it matches the current destination and instance ID (`STORY_DELIVERY_RECONCILIATION_NOT_FOUND`).
3. Verifies that no reconciliation has already been recorded (`STORY_DELIVERY_ALREADY_RECONCILED`).
4. Validates decision consistency: `delivered` on an `update` operation must retain the exact original `remoteId`.
5. Constructs and persists a `StoryDeliveryReconciliation` record via `StoryDeliveryReconciliationRepository.append`.

## Model catalog workflow

Expand Down Expand Up @@ -210,6 +231,8 @@ Persistence contracts are expressed as interfaces in the application layer and i
| `ReviewDecisionPersistence` | `src/application/review-decisions/review-decision-persistence.ts` | `src/adapters/review-persistence/postgres-review-decision-persistence.ts` |
| `StoryRejectionPersistence` | `src/application/story-rejections/story-rejection-persistence.ts` | `src/adapters/story-rejection-persistence/postgres-story-rejection-persistence.ts` |
| `StoryDeliveryRepository` | `src/application/story-deliveries/story-delivery-repository.ts` | `src/adapters/story-delivery-persistence/postgres-story-delivery-repository.ts` |
| `LegacyDeliveryMappingResolutionRepository` | `src/application/story-deliveries/legacy-delivery-mapping-resolution-repository.ts` | `src/adapters/legacy-delivery-mapping-resolution-persistence/postgres-legacy-delivery-mapping-resolution-repository.ts` |
| `StoryDeliveryReconciliationRepository` | `src/application/story-deliveries/story-delivery-reconciliation-repository.ts` | `src/adapters/story-delivery-reconciliation-persistence/postgres-story-delivery-reconciliation-repository.ts` |
| `SiteSettingsRepository` | `src/application/site-settings/site-settings-repository.ts` | `src/adapters/site-settings-persistence/postgres-site-settings-repository.ts` |

The `*.contract.ts` files alongside several ports (`source-repositories.contract.ts`, `story-inspection-repository.contract.ts`, `agent-run-repository.contract.ts`, etc.) are shared harnesses that verify any repository implementation satisfies the same behavior contract. The PostgreSQL adapter tests run these contracts against real PostgreSQL in the integration suite.
29 changes: 28 additions & 1 deletion openwiki/architecture/database-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,33 @@ It alters the `storyrail.agent_tool_calls` table to:
- `story_deliveries_legacy_story_destination_idx` on `(story_id, destination, started_at DESC) WHERE destination_instance_id IS NULL`
- Updates trigger `storyrail.story_delivery_completes_once()` to ensure `destination_instance_id` cannot be changed during completion.

## Migration 0077 — legacy delivery mapping resolutions

`database/migrations/0077-legacy-delivery-mapping-resolutions.sql` captures operator resolutions to ambiguous legacy deliveries as immutable audit facts:
- Creates `storyrail.legacy_delivery_mapping_resolutions` table with columns `resolution_id`, `insertion_position` (identity), `story_id`, `legacy_delivery_id`, `destination`, `destination_instance_id`, `remote_id`, `decision` (`confirm` | `dismiss`), `decided_at`, and `payload` JSONB.
- Enforces payload shape check validating exact key schema and operator actor attribution (`decidedBy.type = 'operator'`).
- Adds partial index `legacy_delivery_mapping_resolutions_latest_idx` on `(story_id, legacy_delivery_id, destination_instance_id, insertion_position DESC)`.
- Adds trigger function `storyrail.legacy_delivery_mapping_resolution_is_valid()` to verify foreign key and prove that the referenced legacy delivery is an immutable, succeeded delivery with `destination_instance_id IS NULL` and matching `remote_id`.
- Adds trigger `storyrail.legacy_delivery_mapping_resolutions_are_immutable()` ensuring resolutions cannot be updated or deleted.

## Migration 0078 — ambiguous delivery reconciliation

`database/migrations/0078-ambiguous-delivery-reconciliation.sql` enables handling and operator reconciliation of uncertain delivery outcomes:
- Alters `storyrail.story_deliveries` outcome check to allow `outcome IN ('running', 'succeeded', 'failed', 'unknown')`.
- Updates `story_deliveries_payload_shape_check` to validate `unknown` outcomes containing an `uncertainty` object with failure codes `DESTINATION_REQUEST_OUTCOME_UNKNOWN` or `DESTINATION_ACCEPTED_RESPONSE_UNVERIFIABLE`.
- Enforces `story_deliveries_unknown_remote_id_check`: `create` operations have `remote_id IS NULL`, while `update` operations retain their non-null `remote_id`.
- Adds partial index `story_deliveries_unresolved_story_instance_idx` on `(story_id, destination_instance_id, started_at DESC, delivery_id DESC) WHERE outcome IN ('running', 'unknown') AND destination_instance_id IS NOT NULL`.
- Creates `storyrail.story_delivery_reconciliations` table (`reconciliation_id`, `insertion_position`, `story_id`, `delivery_id`, `destination`, `destination_instance_id`, `operation`, `slug`, `decision`, `remote_id`, `decided_at`, `payload`).
- Enforces trigger `storyrail.story_delivery_reconciliation_is_valid()` verifying that the snapshot matches the exact unresolved delivery and validates decision consistency (`delivered` requires `remoteId`, `not_delivered` requires `remoteId IS NULL`).
- Adds trigger `storyrail.story_delivery_reconciliations_are_immutable()` ensuring reconciliations cannot be updated or deleted.

## Migration 0079 — agent run recovery

`database/migrations/0079-agent-run-recovery.sql` adds recovery infrastructure for manual agent runs that were interrupted by process termination:
- Adds `recorded_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP` to `storyrail.agent_runs`. Existing running rows receive the migration instant, giving them a fresh recovery window.
- Adds partial index `agent_runs_stale_running_idx` on `(recorded_at ASC, append_position ASC) WHERE outcome = 'running'`.
- Updates trigger `storyrail.agent_run_completion_is_one_way()` ensuring that `recorded_at` cannot be changed during completion and that runs only complete to terminal outcomes.

## Integration test lifecycle

The PostgreSQL integration tests (`src/adapters/source-persistence/postgres-source-repositories.test.ts` and the Story/attachment/assignment/run/article/review/writer-revision/story-rejection suites) connect via `STORYRAIL_TEST_DATABASE_URL`, verify the database name is exactly `storyrail_test`, drop and recreate the `storyrail` schema, apply migrations `0012`, `0017`, `0018`, `0024`, `0025`, `0027`, `0028`, `0030`, `0031`, `0038`, `0041`, `0049`, `0053`, `0054`, `0055`, `0056`, `0057`, `0058`, `0059`, `0060`, `0061`, `0062`, `0063`, `0064`, `0065`, `0066`, `0067`, `0068`, `0069`, `0070`, `0071`, `0072`, `0073`, `0074`, `0075`, and `0076` in order, and truncate the editorial tables (plus delete non-built-in Agent Profiles) between cases. The suite never creates or drops a database.
The PostgreSQL integration tests (`src/adapters/source-persistence/postgres-source-repositories.test.ts` and the Story/attachment/assignment/run/article/review/writer-revision/story-rejection suites) connect via `STORYRAIL_TEST_DATABASE_URL`, verify the database name is exactly `storyrail_test`, drop and recreate the `storyrail` schema, apply migrations `0012`, `0017`, `0018`, `0024`, `0025`, `0027`, `0028`, `0030`, `0031`, `0038`, `0041`, `0049`, `0053`, `0054`, `0055`, `0056`, `0057`, `0058`, `0059`, `0060`, `0061`, `0062`, `0063`, `0064`, `0065`, `0066`, `0067`, `0068`, `0069`, `0070`, `0071`, `0072`, `0073`, `0074`, `0075`, `0076`, `0077`, `0078`, and `0079` in order, and truncate the editorial tables (plus delete non-built-in Agent Profiles) between cases. The suite never creates or drops a database.
Loading
Loading