This document simulates what happens after a new source event arrives and how the markdown context is updated from start to finish.
The product language says "one building.md per property." In the current code
and schema, that file is represented as wiki/<property_id>/index.md, with
supporting markdown files under the same property folder. Building-specific
markdown is served from wiki/<property_id>/02_buildings/<building_id>/index.md.
The implemented ingestion path is:
POST /api/v1/webhook/ingest- HMAC verification and
IngestEventvalidation - API-level idempotency claim in
output/idempotency.duckdb Supervisor.handle(event)- event handler normalization into
normalize/.../*.md - signal classification through
classify_document - entity resolution through
stammdaten.duckdb - target-section lookup through
wiki_chunks.duckdb - LLM extraction of a
PatchPlan - conflict scan
- deterministic patch application to
wiki/<property_id>/*.md _hermes_feedback.jsonlappend- git commit in
wiki_dir - reindex touched markdown files into
wiki_chunks.duckdb - idempotency marked
done - webhook response returned
The schema docs also describe optional SSE broadcast and Hermes inner/outer loops. Those are part of the intended concept, but they are not wired into the current synchronous webhook code path yet.
| Component | Code/docs | Role |
|---|---|---|
| Webhook API | app/api/v1/webhook.py |
Accepts signed ingest events, rejects bad signatures, enforces replay protection, calls the supervisor. |
| Event schema | app/schemas/webhook.py |
Defines event_id, event_type, property_id, optional source_path, and free-form payload. |
| Idempotency store | app/storage/idempotency.py |
Stores event_id as pending, done, or failed; prevents duplicate webhook work. |
| Supervisor | app/services/supervisor.py |
Orchestrates normalize, classify, resolve, locate, extract, conflict scan, patch, reindex. |
| Event handlers | app/services/handlers/* |
Convert source-specific inputs into normalized markdown. Current direct handler keys include email, eml, invoice, bank, letter, manual, document, schedule, lint, erp, chat, slack, whatsapp, and voicenote. |
| Normalizers | app/services/normalize/* |
Parse .eml, invoice/letter PDFs, and bank rows into markdown under normalize/. |
| Classifier | app/services/classify.py |
Uses a small LLM prompt over sender, subject, and excerpt to decide signal/noise. No signal means no wiki patch. |
| Resolver | app/services/resolve.py |
Uses email, IBAN, and explicit IDs to map a source to MIE-*, EIG-*, EH-*, HAUS-*, DL-*, and source IDs. |
| Stammdaten store | app/storage/stammdaten.py |
DuckDB cache loaded from data/stammdaten/stammdaten.json; source of entity identity. |
| Wiki chunks | app/storage/wiki_chunks.py, app/services/reindex.py |
Derived DuckDB index of markdown sections. Used to find likely target sections and for search. |
| Extractor | app/services/extract.py |
Sends normalized source plus located sections to the larger LLM and receives a canonical PatchPlan. |
| Conflict scanner | app/services/conflict.py |
Defers risky overwrites such as status flips, large date drift, or amount deltas to _pending_review.md. |
| Patcher | app/services/patcher/apply.py, app/services/patcher/ops.py |
Applies line-based markdown operations. No LLM at apply time. Refuses writes outside managed sections before # Human Notes. |
| Atomic writer | app/services/patcher/atomic.py |
Writes via temp file, fsync, and replace. |
| Vocabulary | schema/VOCABULARY.md |
Controlled values checked before patching. Unknown values become pending-review items. |
| Wiki schema | schema/WIKI_SCHEMA.md, schema/CLAUDE.md |
Canonical markdown layout, sections, patch operation rules, conflict policy, and long-term Hermes plan. |
| Read API | app/api/v1/properties.py, app/services/wiki.py |
Serves property and building markdown as text/markdown. |
Note: schema examples use dotted event names like email.received. The current
handler registry does not map dotted names yet; email.received would fall
back to DocumentHandler unless the registry is expanded. This simulation uses
the current code-compatible event type email.
A tenant reports a heating outage in unit EH-014. The source adapter sends a
signed webhook event for property LIE-001.
Sample webhook body:
{
"event_id": "EMAIL-12044",
"event_type": "email",
"property_id": "LIE-001",
"source_path": "data/emails/2026-04/20260425_143200_EMAIL-12044.eml",
"payload": {}
}Assumed raw email content:
From: anna.mueller@example.test
To: pm@example.test
Date: Sat, 25 Apr 2026 14:32:00 +0200
Subject: Heizung kalt in EH-014
Hallo,
in EH-014 ist die Heizung seit heute Mittag kalt. Bitte schicken Sie den
Heizungsdienst. Die Wohnung ist in HAUS-12.
Viele Gruesse
Anna Mueller
| Time | Step | Component | What happened | Data read/written |
|---|---|---|---|---|
| 2026-04-25T14:32:00.000+02:00 | source event | Mail adapter | New tenant email is detected and mapped to event_id=EMAIL-12044. |
Reads mailbox; prepares webhook JSON. |
| 2026-04-25T14:32:00.180+02:00 | webhook received | FastAPI webhook | POST /api/v1/webhook/ingest receives raw bytes. |
Reads request body. |
| 2026-04-25T14:32:00.184+02:00 | verify signature | _verify_hmac |
Computes HMAC SHA-256 using APP_WEBHOOK_HMAC_SECRET; compares with x-propcontext-signature. |
No files written. |
| 2026-04-25T14:32:00.188+02:00 | parse event | _parse_event |
JSON is parsed into IngestEvent. |
In-memory event object. |
| 2026-04-25T14:32:00.194+02:00 | claim event | IdempotencyStore.claim |
EMAIL-12044 is inserted as pending. |
Writes output/idempotency.duckdb. |
| 2026-04-25T14:32:00.205+02:00 | dispatch | get_event_handler |
event_type=email routes to EmailHandler. |
In-memory handler selection. |
| 2026-04-25T14:32:00.215+02:00 | normalize | EmailHandler and normalize_eml |
Source .eml is parsed with Python email parser. |
Reads data/emails/2026-04/20260425_143200_EMAIL-12044.eml. |
| 2026-04-25T14:32:00.265+02:00 | write normalized source | write_normalized_markdown |
Normalized markdown is written with source, sha256, parser, parsed time, mime, language. | Writes normalize/eml/2026-04/EMAIL-12044.md. |
| 2026-04-25T14:32:00.310+02:00 | classify prompt | classify_document |
Classifier sees sender, subject, and first 500 chars. | Sends LLM request using settings.haiku_model. |
| 2026-04-25T14:32:00.980+02:00 | classify result | Classification |
Email is considered signal: category task_update/heating, priority high, confidence 0.93. |
No files written. |
| 2026-04-25T14:32:01.005+02:00 | open master data | _open_stammdaten |
stammdaten.duckdb is opened; if empty, JSON master data is loaded. |
Reads data/stammdaten/stammdaten.json; writes output/stammdaten.duckdb if needed. |
| 2026-04-25T14:32:01.035+02:00 | resolve entities | resolve_context |
Sender email maps to tenant, explicit EH-014 and HAUS-12 are validated. Related owner/unit/building chain is collected. |
In-memory ResolutionResult. |
| 2026-04-25T14:32:01.065+02:00 | open section index | open_wiki_chunks |
The wiki section cache is opened. If the property wiki exists but has no rows, it is reindexed. | Reads/writes output/wiki_chunks.duckdb. |
| 2026-04-25T14:32:01.090+02:00 | locate sections | locate_sections |
Finds candidate sections by entity refs first, then full-text query fallback. | Reads wiki_chunks rows. |
| 2026-04-25T14:32:01.130+02:00 | extraction prompt | extract_patch_plan |
Sends normalized email, resolved IDs, source IDs, and located section bodies to extractor. | Sends LLM request using settings.sonnet_model. |
| 2026-04-25T14:32:03.420+02:00 | patch plan returned | canonicalize_patch_plan |
LLM output is normalized: wiki-prefixed paths become property-relative paths, content becomes text or row. |
In-memory PatchPlan. |
| 2026-04-25T14:32:03.455+02:00 | conflict scan | scan_patch_plan_conflicts |
Existing keyed lines are checked for status flip, date drift, or amount delta. No conflict found. | Reads target markdown files. |
| 2026-04-25T14:32:03.480+02:00 | vocabulary validation | validate_keyed_values |
Controlled values are checked. Invalid ops would be moved to _pending_review.md. |
Reads schema/VOCABULARY.md. |
| 2026-04-25T14:32:03.505+02:00 | patch open issue | upsert_bullet |
Inserts or replaces the keyed EH-014 issue line in HAUS-12/index.md. |
Writes wiki/LIE-001/02_buildings/HAUS-12/index.md. |
| 2026-04-25T14:32:03.535+02:00 | patch recent event | prepend_row |
Adds newest event row at top of the Recent Events table. |
Writes wiki/LIE-001/02_buildings/HAUS-12/index.md. |
| 2026-04-25T14:32:03.565+02:00 | patch unit history | prepend_row |
Adds the event to the unit dossier history. | Writes wiki/LIE-001/02_buildings/HAUS-12/units/EH-014.md. |
| 2026-04-25T14:32:03.595+02:00 | patch tenant contact | prepend_row |
Adds the email to tenant contact history. | Writes wiki/LIE-001/03_people/mieter/MIE-014.md. |
| 2026-04-25T14:32:03.625+02:00 | patch provenance | upsert_footnote |
Adds source footnotes in every file that references EMAIL-12044. |
Writes ## Provenance sections only. |
| 2026-04-25T14:32:03.650+02:00 | patch state | update_state |
Updates last_event_id, increments counters, and sets last_patched. |
Writes wiki/LIE-001/_state.json. |
| 2026-04-25T14:32:03.675+02:00 | append feedback | _append_feedback |
Records the ingest summary and op counts. | Writes wiki/LIE-001/_hermes_feedback.jsonl. |
| 2026-04-25T14:32:03.720+02:00 | commit | _git_commit |
Stages all wiki changes and creates one audit commit. | Runs git add -A; commit message ingest(EMAIL-12044): heating outage reported for EH-014. |
| 2026-04-25T14:32:03.820+02:00 | reindex touched files | _reindex_touched_files |
Parses touched markdown sections and refreshes section rows. | Writes output/wiki_chunks.duckdb. |
| 2026-04-25T14:32:03.855+02:00 | mark done | IdempotencyStore.mark_done |
Event status becomes done. |
Writes output/idempotency.duckdb. |
| 2026-04-25T14:32:03.870+02:00 | response | IngestResponse |
API returns status=applied, applied/deferred op counts, commit SHA, idempotent=false. |
HTTP JSON response. |
| 2026-04-25T14:32:04.000+02:00 | read side available | WikiService |
External agents can fetch updated markdown. | GET /api/v1/properties/LIE-001/buildings/HAUS-12. |
The first persistent artifact is not the wiki patch. It is the normalized source markdown. This gives the patch provenance a stable, readable source file.
Example normalize/eml/2026-04/EMAIL-12044.md:
---
source: "data/emails/2026-04/20260425_143200_EMAIL-12044.eml"
sha256: "9c0d...sample"
parser: "python-email"
parsed_at: "2026-04-25T12:32:00.265000+00:00"
mime: "message/rfc822"
lang: "de"
---
# Email EMAIL-12044
| Field | Value |
|---|---|
| ID | EMAIL-12044 |
| Subject | Heizung kalt in EH-014 |
| From | Anna Mueller <anna.mueller@example.test> |
| To | pm@example.test |
| Date | Sat, 25 Apr 2026 14:32:00 +0200 |
| Message-ID | <EMAIL-12044@example.test> |
## Body
Hallo,
in EH-014 ist die Heizung seit heute Mittag kalt. Bitte schicken Sie den
Heizungsdienst. Die Wohnung ist in HAUS-12.
Viele Gruesse
Anna MuellerExample in-memory resolution:
{
"property_id": "LIE-001",
"entities": [
{"id": "MIE-014", "role": "mieter", "source": "email"},
{"id": "EH-014", "role": "einheit", "source": "email"},
{"id": "EIG-009", "role": "eigentuemer", "source": "email"},
{"id": "HAUS-12", "role": "gebaeude", "source": "email"}
],
"mentioned_ids": ["EH-014", "HAUS-12"],
"source_ids": ["EMAIL-12044"],
"unresolved_ids": []
}Example located sections from wiki_chunks.duckdb:
[
{
"file": "02_buildings/HAUS-12/index.md",
"section": "Open Issues",
"entity_refs": ["HAUS-12", "EH-014"],
"score": 10.0
},
{
"file": "02_buildings/HAUS-12/index.md",
"section": "Recent Events",
"entity_refs": ["HAUS-12"],
"score": 10.0
},
{
"file": "02_buildings/HAUS-12/units/EH-014.md",
"section": "History",
"entity_refs": ["EH-014", "MIE-014", "EIG-009"],
"score": 10.0
},
{
"file": "03_people/mieter/MIE-014.md",
"section": "Contact History",
"entity_refs": ["MIE-014", "EH-014"],
"score": 10.0
}
]This is the canonical patch plan after extraction and path normalization. The actual patcher applies these operations deterministically; it does not ask an LLM to edit markdown.
{
"event_id": "EMAIL-12044",
"property_id": "LIE-001",
"event_type": "email",
"summary": "heating outage reported for EH-014",
"source_ids": ["EMAIL-12044"],
"ops": [
{
"op": "upsert_bullet",
"file": "02_buildings/HAUS-12/index.md",
"section": "Open Issues",
"key": "EH-014",
"text": "- π΄ **EH-014:** Heating outage reported by MIE-014 on 2026-04-25; heating service requested [^EMAIL-12044]"
},
{
"op": "prepend_row",
"file": "02_buildings/HAUS-12/index.md",
"section": "Recent Events",
"row": ["2026-04-25 14:32", "HAUS-12", "email", "Heating outage EH-014", "[^EMAIL-12044]"],
"header": ["Date", "Scope", "Type", "Summary", "Source"]
},
{
"op": "prepend_row",
"file": "02_buildings/HAUS-12/units/EH-014.md",
"section": "History",
"row": ["2026-04-25", "email", "Tenant reported no heating", "[^EMAIL-12044]"],
"header": ["Date", "Type", "Summary", "Source"]
},
{
"op": "prepend_row",
"file": "03_people/mieter/MIE-014.md",
"section": "Contact History",
"row": ["2026-04-25", "email", "Reported heating outage in EH-014", "[^EMAIL-12044]"],
"header": ["Date", "Type", "Summary", "Source"]
},
{
"op": "upsert_footnote",
"file": "02_buildings/HAUS-12/index.md",
"key": "EMAIL-12044",
"text": "normalize/eml/2026-04/EMAIL-12044.md"
},
{
"op": "upsert_footnote",
"file": "02_buildings/HAUS-12/units/EH-014.md",
"key": "EMAIL-12044",
"text": "normalize/eml/2026-04/EMAIL-12044.md"
},
{
"op": "upsert_footnote",
"file": "03_people/mieter/MIE-014.md",
"key": "EMAIL-12044",
"text": "normalize/eml/2026-04/EMAIL-12044.md"
},
{
"op": "update_state",
"file": "_state.json",
"updates": {"last_event_id": "EMAIL-12044"},
"counters": {"open_issues": 1}
}
],
"review_items": [],
"complexity_score": 5,
"skill_candidate": false
}Only managed sections before # Human Notes are changed. Human notes and
unkeyed human bullets are preserved.
Before wiki/LIE-001/02_buildings/HAUS-12/index.md:
## Open Issues
<!-- agent-managed: keyed bullets, format `- π΄ **EH-XX:** ... [^source]` -->
## Recent Events
<!-- agent-managed: ring buffer max=50 -->
## Provenance
<!-- agent-managed: footnote definitions only -->
# Human Notes
Tenant called twice last winter about radiator noise. Keep this note.After wiki/LIE-001/02_buildings/HAUS-12/index.md:
## Open Issues
<!-- agent-managed: keyed bullets, format `- π΄ **EH-XX:** ... [^source]` -->
- π΄ **EH-014:** Heating outage reported by MIE-014 on 2026-04-25; heating service requested [^EMAIL-12044]
## Recent Events
<!-- agent-managed: ring buffer max=50 -->
| Date | Scope | Type | Summary | Source |
| --- | --- | --- | --- | --- |
| 2026-04-25 14:32 | HAUS-12 | email | Heating outage EH-014 | [^EMAIL-12044] |
## Provenance
<!-- agent-managed: footnote definitions only -->
[^EMAIL-12044]: normalize/eml/2026-04/EMAIL-12044.md
# Human Notes
Tenant called twice last winter about radiator noise. Keep this note.After wiki/LIE-001/02_buildings/HAUS-12/units/EH-014.md:
## History
<!-- agent-managed: prepend rows, ring buffer -->
| Date | Type | Summary | Source |
| --- | --- | --- | --- |
| 2026-04-25 | email | Tenant reported no heating | [^EMAIL-12044] |
## Provenance
<!-- agent-managed: footnote definitions only -->
[^EMAIL-12044]: normalize/eml/2026-04/EMAIL-12044.md
# Human NotesAfter wiki/LIE-001/03_people/mieter/MIE-014.md:
## Contact History
<!-- agent-managed: ring buffer -->
| Date | Type | Summary | Source |
| --- | --- | --- | --- |
| 2026-04-25 | email | Reported heating outage in EH-014 | [^EMAIL-12044] |
## Provenance
<!-- agent-managed: footnote definitions only -->
[^EMAIL-12044]: normalize/eml/2026-04/EMAIL-12044.md
# Human NotesAfter wiki/LIE-001/_state.json:
{
"schema_version": 1,
"property_id": "LIE-001",
"bootstrapped_at": "2026-04-25T10:00:00+00:00",
"last_patched": "2026-04-25T12:32:03.650000+00:00",
"last_event_id": "EMAIL-12044",
"counts": {
"buildings": 3,
"units": 52,
"owners": 35,
"tenants": 26,
"dienstleister": 16,
"open_issues": 1
}
}After wiki/LIE-001/_hermes_feedback.jsonl:
{"kind":"ingest","event_id":"EMAIL-12044","property_id":"LIE-001","summary":"heating outage reported for EH-014","applied_ops":8,"deferred_ops":0}The current code writes a compact feedback line with event ID, property ID,
summary, applied op count, and deferred op count. The richer JSONL shape in
schema/HERMES_LOOP.md is the planned self-improvement substrate.
Example response:
{
"event_id": "EMAIL-12044",
"status": "applied",
"applied_ops": 8,
"deferred_ops": 0,
"commit_sha": "a3f2c19b6d8e4f0a9b1c2d3e4f5a6b7c8d9e0f1a",
"idempotent": false
}If the same webhook arrives again:
IdempotencyStore.claim("EMAIL-12044")sees statusdone.- The supervisor is not called.
- The API returns:
{
"event_id": "EMAIL-12044",
"status": "duplicate",
"applied_ops": 0,
"deferred_ops": 0,
"commit_sha": null,
"idempotent": true
}There is also patch-level replay protection: if _hermes_feedback.jsonl
already contains the event ID, apply_patch_plan returns an idempotent result
without rewriting markdown. This is useful if the API-level DuckDB state is
lost or rebuilt.
If EH-014 already had a contradictory keyed line, the conflict scanner would
drop only the risky op and keep safe ops.
Example existing line:
- π’ **EH-014:** Heating repaired on 2026-04-24 [^EMAIL-12012]Incoming line:
- π΄ **EH-014:** Heating outage reported by MIE-014 on 2026-04-25 [^EMAIL-12044]Possible result:
## Open Conflicts
### conflict: EH-014
- file: `02_buildings/HAUS-12/index.md`
- section: `Open Issues`
- reason: status flip requires human approval
- existing: `- π’ **EH-014:** Heating repaired on 2026-04-24 [^EMAIL-12012]`
- incoming: `- π΄ **EH-014:** Heating outage reported by MIE-014 on 2026-04-25 [^EMAIL-12044]`The webhook would still complete with status=applied if other non-conflicting
ops were applied, but deferred_ops would be greater than zero.
After the commit and reindex, the markdown is available through the read API:
GET /api/v1/properties/LIE-001
GET /api/v1/properties/LIE-001/buildings/HAUS-12
For this scenario, a building-focused agent would fetch:
wiki/LIE-001/02_buildings/HAUS-12/index.md
It now contains the new open issue, the recent event, and a footnote pointing
back to normalize/eml/2026-04/EMAIL-12044.md.
- Bootstrap must run before live ingestion so
wiki/LIE-001/...files exist and contain the expected sections. wiki_dirmust be a git repository with local user config, because each applied event creates a commit.wiki_chunks.duckdbis derived cache. It can be rebuilt from markdown.normalize/files are provenance artifacts and should not be regenerated in a way that changes source IDs.# Human Notesis the hard boundary. The patcher works only on managed sections above it.- Current code writes property context to
wiki/<property_id>/index.md, not tooutput/building.md. If the deliverable must be literally namedbuilding.md, add an export or compatibility layer rather than changing the canonical wiki layout.