# Technology choices and portable ports

Nonfinal architecture review · 0.4.0. Generated from `assets/fixtures-architecture.js`; edit that source and rerun `node tools/generate-docs.js`.



## stack

- Domain
- TypeScript packages: resolver-domain, product-domain, contracts
- PROP

- HTTP
- Hono adapters on Workers (intake + admin API; product API)
- PROP

- Jobs
- Queues: resolution-job queue + product-ingestion queue; PgJobTransport kept as fallback adapter
- PROP

- State
- Managed PostgreSQL, schemas resolver + product, via Hyperdrive
- PROP

- Evidence
- R2 private bucket via ArtifactStore
- PROP

- Collectors
- SourceCollector package on Workers+Cron, Node, Browser Run/Playwright
- PROP

- Operator UI
- React + Vite static assets on an access-controlled origin
- PROP

- LLM
- ModelProvider adapter; vendor OPEN
- OPEN

## ports

### id

ArtifactStore

### status

PROP

### purpose

Write-once evidence objects by checksum with atomic immutable metadata; read the complete object as Uint8Array, then select evidence slices in the calling layer.

### semantics

- Atomic conditional create; no overwrite of bytes or immutable metadata
- Conflicting metadata returns IMMUTABLE_METADATA_CONFLICT; capture provenance is stored separately
- get returns the whole object as bytes; selectors run after retrieval; streaming/ranges are future capabilities

### adapters

- R2ArtifactStore (Workers binding)
- S3ArtifactStore (Node)
- FsArtifactStore (tests, local)

### id

JobTransport

### status

PROP

### purpose

Internal resolution jobs between modules.

### semantics

- at-least-once; consumers idempotent by job id
- ack only after the job’s durable write commits
- payload = references only (capture id, artifact ref); stays under the transport message size limit
- no ordering assumption; bounded attempts then dead-letter

### adapters

- QueuesJobTransport
- PgJobTransport (job table + SKIP LOCKED)
- InMemoryJobTransport (tests)

### id

EventPublisher

### status

PROP

### purpose

Relay outbox rows to the product-ingestion transport.

### semantics

- publishes rows already committed; never called inside a domain transaction
- duplicates allowed; event id stable

### adapters

- QueuesEventPublisher
- InMemoryEventPublisher

### id

ModelProvider

### status

PROP

### purpose

LLM calls for act_llm_extractor and act_llm_reviewer.

### semantics

- input is a prepared bundle; no tools, no network, no secrets passed
- prompt id + version recorded with every output
- output validated by caller before storage

### adapters

- Provider-specific adapter per vendor (SDK lives only here)
- RecordedModelProvider (replay tests)

### id

IdentityVerifier

### status

PROP

### purpose

Verify caller identity for capture, heartbeat and admin requests.

### semantics

- returns principal + tenant; details owned by Trust & tenancy

### adapters

- OAuth2JwksVerifier
- AccessJwtVerifier (operator)

### id

UnitOfWork + repositories

### status

PROP

### purpose

Transaction boundary for domain services; repositories (MappingRepository, CatalogItemRepository, ReviewCaseRepository, AttributeDefinitionRepository) enlist in it.

### semantics

- one database transaction per unit
- domain events collected and written as outbox rows before commit (INV-06)
- optimistic revision checks raise ConflictError

### adapters

- PgUnitOfWork (postgres.js or node-postgres via Hyperdrive or direct)
- InMemoryUnitOfWork (tests)

### id

SearchIndex

### status

PROP

### purpose

Product search and attribute filtering.

### semantics

- updated inside or after the projection transaction (choice OPEN with ADR-010)
- rebuildable from projections

### adapters

- PgSearchIndex (JSONB + GIN)
- External index adapter (future)

```typescript
[object Object],[object Object]
```

## spikes

### id

SPK-01

### status

PROP

### question

Do Hyperdrive + the chosen driver preserve AW-01 (decision + facts + outbox in one transaction) including row locks and unique/exclusion constraint errors?

### method

Run AW-01 from a Worker through Hyperdrive with concurrent decisions on the same case; kill the Worker mid-transaction

### exit

No partial commits; stale revision → conflict; no outbox row without its facts

### measure

- transaction duration
- connection wait time
- error mapping fidelity

### blocks

ADR-003, ADR-007

### id

SPK-02

### status

PROP

### question

Does the product consumer stay correct under duplicate and out-of-order delivery?

### method

Inject duplicates, reordered revisions and missing dependencies on the product-ingestion queue (reuse the prototype scenario-8 cases)

### exit

Projection equals the resolver export at the end of every run

### measure

- parked event count and time parked
- ingestion lag

### blocks

ADR-003, ADR-009

### id

SPK-03

### status

PROP

### question

Do Browser Run session limits fit src_halden_us and src_lumen_web runs?

### method

Read current limits; run one full collection per source

### exit

Runs complete within limits or a local Playwright fallback is chosen

### measure

- pages per run
- session duration
- concurrent sessions

### blocks

Collector placement

### id

SPK-04

### status

PROP

### question

Is JSONB + GIN on product.item_view adequate for Q2, or are generated columns / a search index needed?

### method

Load representative synthetic data shaped like the fixtures (classes, attribute counts, offers per item); run Q1–Q3

### exit

Q1–Q3 meet targets that Jonathan sets after the first slice

### measure

- p95 latency
- rows touched
- index size
- write amplification on apply

### blocks

ADR-010

### id

SPK-05

### status

PROP

### question

Does the transactional cursor and pinned rebuild remain correct under concurrency, pruning and restore?

### method

Hold a writer before commit; contend another writer; snapshot, renew/expire pins and prune; restore under a new trusted epoch; compare projection with accepted state

### exit

No missed committed events, no pre-restore history resurrection, and bounded measured contention

### measure

- cursor lock wait
- export lifetime
- retained bytes under pins
- rebuild and cutover time
- discarded-epoch deliveries
- Live cursor advancement after cutover; independent shadow progress; pruning with a delivery gap and parked dependency

### blocks

ADR-004, DJ-03

### id

SPK-06

### status

OPEN

### question

Only if DEC-01 stays open: can D1 serve product projections (Q1–Q3) and reproduce inbox + projection atomicity?

### method

Port the product schema to D1; run SPK-02 and SPK-04 cases

### exit

Parity with PostgreSQL on correctness; measured results within targets

### measure

- same as SPK-02/04
- rows read/written

### blocks

ADR-010

