Kunstencentrum De Wervel DEMO-ORGANISATIE
Apart prototype · "Memory Core" slaat echte gegevens op met rechten in de database · alle andere pagina's zijn DEMO (fictief, browsergeheugen, rechten gesimuleerd) · geen e-mail, connectoren of AI gekoppeld.
Voor integratie

Documentatie & API-contracten

Technische documentatie (Engels) voor WIRL-D Core, Steve en de modules.

# WIRL-D Organization Knowledge Hub ("Organisatiegeheugen") — prototype documentation

Status: independent prototype. Not connected to WIRL-D production. Nothing is published or deployed.
Personal first use: `/core` and `/core/research` offer **Start mijn kennisbank**. The authenticated, idempotent `start_personal_knowledge_bank()` creates “Mijn kennisbank”, admin membership and audited owner binding atomically only for a caller without memberships. Existing members gain no owner privileges. File/paste intake uses stored-byte verification and pending review; owner-only real-source research remains separate staging.
Internal Memory Core API v1 is implemented separately from these draft demo contracts: see `docs/remediation/integration-api.md` for signed server-to-server retrieval, staged admin ingestion, explicit organization/actor mapping, actual tests and unconfigured setup. It is not yet connected to WIRL-D; no customer login integration is claimed.
Latest contract audit (2026-10-21): WIRL-D now matches ES256 paths/body binding and staged upload; seven signer/verifier checks pass. Trusted local read-only setup diagnostics are available; keys/org/admin/mappings and unapplied quotas/ingestion ceiling still block activation. No real E2E/readiness claim.
All data is fictional (DEMO). API contract version: `v1` (draft).

## 1. What works vs. what is demo-only

| Area | Works in prototype | Demo/placeholder | Needs backend / credentials |
|---|---|---|---|
| Navigation, 17 views + docs | yes | – | – |
| Upload | file type allow-list, originals' metadata, TXT/MD/CSV text read in browser, item → review queue, conservative `beperkt` access | files live only in browser memory | storage bucket, PDF/DOCX/PPTX/XLSX parsers, OCR, malware scan |
| Spreadsheet import | CSV paste/upload, column mapping, validation (formula, numeric, ISO date, unknown KPI), duplicate detection → review, snapshot | XLSX shows "parser not configured" | server XLSX parser |
| Brand rules | approve / reject / correct, version history, source locators | extraction results are seeded | document parser + LLM extraction |
| Search | access-filtered keyword + synonym retrieval, "why matched", citations | "semantic" = synonyms | embeddings + vector index per tenant |
| Steve | deterministic answers for 5 example intents + fallback retrieval, citations, confidence, staleness | no LLM | Lovable AI Gateway model behind server-side retrieval |
| Email intake | allowlist, verification status, scope, revocation, quarantine of unknown sender & dangerous attachment, dedupe by message-ID, triage | messages are simulated; nothing is sent/received | email provider inbound webhook, DNS (MX/SPF/DKIM), signature validation, malware scan, DPA |
| Connectors | catalogue, BUILD/CONNECT/MIRROR/REPLACE/LEAVE, test button (always fails honestly → stays "niet geconfigureerd") | sample records labelled simulated | real adapters + server-side secrets |
| Permissions | role switcher filters library, search, Steve, decisions | enforced in browser only | WIRL-D Core roles; server-side checks + RLS |
| Audit | every mutation appends an event | in-memory | append-only table |

State resets on page reload (or "Demo resetten").

## 2. Data ownership map

| Data | Owner (system of record) | Knowledge Hub holds |
|---|---|---|
| Users, orgs, roles, entitlements | WIRL-D Core | references only |
| Events | Events module | indexed metadata + summary + link (`isIndexedCopy=true`) |
| Tickets / visitor counts | Ticketing | KPI aggregates with source reference |
| Contacts / CRM | CRM | never copied; references only |
| Newsletters / website | Newsletter Studio / Website Studio | indexed copy for analysis |
| Accounting | external package (LEAVE) | reports only |
| Uploaded documents, brand rules, memories, ideas, reviews, citations | **Knowledge Hub** | originals + versions |

## 3. Provenance model
Every `KnowledgeItem` carries: `orgId, source, sourceSystem, sourceRecordId?, sourceUrl?, owner, access, version, status, contentType, processing, uploadedAt, modifiedAt, ingestedAt, retention, licence?, confidence, isIndexedCopy`.
Derived objects (`BrandRule`, `ExtractedFact`, `OrganizationMemory`, `SteveFinding`) reference `Citation { itemId, label, locator }`. A memory without evidence cannot be a fact.
Memory loop: Journal/activity → ReviewTask → reviewed KnowledgeItem/Fact → OrganizationMemory (with corrections history, `superseded`, `needs verification`).

## 4. Permission model
- Identity and roles come from WIRL-D Core (JWT claims `org_id`, `roles[]`).
- Sensitivity labels: `publiek < intern < vertrouwelijk < beperkt`. Role → allowed labels (see /permissions).
- Retrieval order: authenticate → resolve org membership → compute allowed labels/collections → **filter in the query** → score → return. Embeddings and snippets are stored per `org_id`; no cross-tenant index.
- Email senders get submit-only scopes; sending mail never grants dashboard access.
- Platform support access: explicit, time-boxed grant by org admin, audited.

## 5. Email intake security checklist
- [ ] Provider adapter + inbound webhook configured
- [ ] Webhook signature (HMAC) verified; replay window ≤ 5 min
- [ ] Domain DNS: MX, SPF, DKIM, DMARC
- [ ] Sender on allowlist **and** verified (confirmation link); display name ignored
- [ ] Sender status `actief`; scope checked per message
- [ ] Dedupe on provider message-ID + body hash; loop detection (`Auto-Submitted`, own address in `From`)
- [ ] Attachment type/size check; executables & macro files rejected; malware scan before parsing
- [ ] Attachments default `beperkt` until classified
- [ ] Unknown sender → quarantine (30 days), no processing, no reply
- [ ] No auto-forward / auto-reply unless explicitly configured
- [ ] Sender notice, retention, deletion requests, DPA with provider — requires legal review (no GDPR claim made)

## 6. Steve triage schema (`SteveTriage v1`)
```json
{
  "summary": "string",
  "topic": "string",
  "related": [{ "kind": "Initiatief|Doel|Besluit|Document", "id": "string", "title": "string", "why": "string" }],
  "duplicateOf": "idea_id | null",
  "coverage": "nieuw | deels gedekt | al gedekt",
  "recommendation": "string",
  "proposedOwner": "string",
  "urgency": "null unless grounded in a cited source",
  "openQuestions": ["string"],
  "confidence": 0.0,
  "evidence": [{ "itemId": "string", "label": "string", "locator": "string" }],
  "draft": null
}
```
Human decision options: link to initiative, propose initiative/task (creates ReviewTask, nothing in Core until approved), backlog, request clarification, defer, decline — rationale required.

## 7. API contract examples (v1, draft)
All endpoints: `Authorization: Bearer <Core JWT>`, `X-Org-Id`, cursor pagination (`?cursor=&limit=`), `Idempotency-Key` on POST, errors as `{ "error": { "code", "message", "retryable" } }`.

```http
POST /v1/orgs/{orgId}/items                     # ingest item (multipart or source reference)
Idempotency-Key: 6f1c...
{ "title": "...", "contentType": "PDF", "source": "Upload", "access": "beperkt", "retention": "7y",
  "sourceSystem": "events", "sourceRecordId": "evt_301", "isIndexedCopy": true }
→ 202 { "id": "ki_..", "processing": "in wachtrij", "version": 1 }

POST /v1/orgs/{orgId}/sources/{connectionId}/sync   → 202 { "syncRunId": "..", "mode": "read-only" }
GET  /v1/orgs/{orgId}/search?q=jongerenpas&kind=Document&cursor=
→ 200 { "results": [{ "id", "kind", "title", "snippet", "why": [..], "access", "citation": {..}, "updatedAt", "canonical": {"system","recordId","url"} }], "nextCursor": null }
GET  /v1/orgs/{orgId}/citations/{citationId}
GET  /v1/orgs/{orgId}/goals/{goalId}/references       # initiatives/events linked by reference
POST /v1/inbound/email/{provider}                     # signed webhook; returns 202 even for quarantine
{ "messageId": "<..>", "from": "...", "to": "ideas+dewervel@...", "subject": "...", "text": "...", "attachments": [{ "name", "type", "size", "sha256", "url" }], "headers": {..} }
POST /v1/orgs/{orgId}/allowlist                       { "email", "person", "scope": ["ideeën"] } → 201 { "status": "in afwachting" }
POST /v1/orgs/{orgId}/allowlist/{id}:revoke
POST /v1/orgs/{orgId}/reviews/{id}:decide             { "decision": "approve|reject|clarify|restrict|stale|delete", "comment" }
POST /v1/orgs/{orgId}/memories/{id}:approve | :correct { "text", "reason", "evidence": [..] } | :supersede
POST /v1/orgs/{orgId}/steve/proposals                 { "kind": "task|initiative", "triage": SteveTriage, "requiresApproval": true }
```
Retries: exponential backoff on `retryable=true`; webhooks idempotent on `messageId`.

## 8. Integration order
1 Core identity/tenancy → 2 item/source registry + citations → 3 Dashboard links/search/review → 4 Steve retrieval/proposals → 5 domain references (goals/events/content/ticketing/newsletters/design/CRM) → 6 read-only connectors + inbound email → 7 permissioned write-back after tests.

## 9. Known limitations
- No backend: no persistence, no real auth, permissions only simulated in the browser.
- No parsing beyond TXT/MD/CSV; no OCR; no embeddings; no LLM.
- No real email; no connector calls; sample API records are static.
- World graph uses a fixed column layout; large graphs need a layout engine.
- Not a legal/GDPR compliance assessment.

## Memory Core v1 — persistent layer (real, not demo)

Lives at `/core` (sign-in required). Separate prototype backend; never connected to WIRL-D production.

**Works for real**
- Sign-in: email/password (email confirmation on) and Google.
- Organisations (tenants) with per-org roles in `org_members` (beheerder, medewerker, bestuur, vrijwilliger). Creator becomes beheerder; beheerders add existing accounts by email.
- Upload to private storage bucket `knowledge` at `<org_id>/<item_id>/v<n>-<file>`; SHA-256 recorded; max 10 MB; macros/scripts/html/svg/zip blocked.
- Server-side verification of stored bytes (SHA-256, MIME, signature check, 10 MB); text extraction server-side for TXT, MD, CSV, TSV, JSON only. PDF/DOCX/PPTX/XLSX/images are stored but not processed and cannot be approved. See docs/remediation/phase-a-results.md.
- Labels: collection + access (publiek/intern/vertrouwelijk/beperkt). Users can only assign labels their role can read.
- Review: new items and new versions are `te_beoordelen`; only beheerder can set goedgekeurd/afgewezen/gearchiveerd (DB trigger enforced).
- Versioning: `item_versions`, version must increase by exactly 1; files of each version kept.
- Audit trail: written by DB triggers/functions (`audit_events`), readable by beheerder + bestuur; clients cannot insert/edit/delete it.
- Search: Postgres full-text (`dutch`) over title, collection and extracted text; RLS filters by org, role and status before results are returned.

**Visibility rule (RLS)**: member of org AND role may read the access label AND (status goedgekeurd OR you uploaded it OR you are beheerder). Storage downloads follow the same rule via `can_see_item`.

**Not working / not configured**
- OCR (scanned PDFs, images), PPTX/XLSX parsing: file stored, no text.
- Malware scanning: none. Do not upload untrusted files.
- Resolved in Phase A: hash/MIME/text are no longer accepted from the browser; items and versions are only written by the service-only `finalize_upload` function.
- No embeddings/semantic search, no Steve/LLM on real data, no invitation emails, no deletion/retention jobs, no member removal UI, no GDPR export.
- Demo pages (everything except /core) still use the in-memory store and are not tenant-secured.

## AI-bronnenonderzoek (real, private staging)
`/core/research` reuses the persistent Core. Firecrawl is linked; Lovable AI synthesizes source-backed findings. Only the explicitly bound owner can start or import; drafts never feed the WIRL-D/Steve read API. Source URLs, available publication dates, retrieval dates, confidence, contradictions and duplicate warnings appear in the dossier. Selected import uses the guarded item/version/audit flow; immediate approval requires a separate explicit checkbox.

Practical intake: beheerder and medewerker can add supported TXT/MD/CSV/TSV/JSON files or pasted text from `/core` and `/core/research`, without an owner-confirmation page. Pasted text becomes a Markdown file and follows the existing private upload, server verification, pending review, version and audit path. An optional source URL is preserved in the stored text, not a new structured metadata column. A successful save links directly to the item's review page.

Owner claims remain available in the database, but are not a prerequisite for ordinary intake. AI research/import stays restricted to an explicitly bound owner. A maintainer can bind a securely identified requesting user only when existing organization admin membership is verified. Current requesting account was identified using the trusted `auth-session --self` action, but has no organization membership: no binding or organization was created. The signed-in page was checked and shows organization creation directly. 49 relevant unit/regression tests passed; actual save/review E2E remains blocked by absent org membership. See `docs/remediation/integration-api.md` for remaining WIRL-D key/mapping/rate-limit blockers. Nothing published.