wedge(analyze): remediate C1/I1/I2/U1/U2 — rebuild-by-partition-switch, contracts synced to as-built, runbook

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m2 (AI Agent) 2026-07-02 04:26:22 +02:00
parent c52f968f38
commit 9ec83bf7f2
4 changed files with 88 additions and 3 deletions

View file

@ -16,9 +16,13 @@ per-surface code.
from __future__ import annotations
import os
import httpx
PARTITION = "market:catalog"
# Overridable so a full catalog rebuild can land in a fresh partition and readers
# switch by config (constitution II rebuildability; see docs/runbook.md).
PARTITION = os.environ.get("M2_MARKET_CATALOG_PARTITION", "market:catalog")
class CatalogClient:

60
docs/runbook.md Normal file
View file

@ -0,0 +1,60 @@
# m2-market operational runbook
## Catalog rebuild (constitution II rebuildability)
The live memory-api is store/search only — no delete, no partition drop — so
`m2-market-indexer` reindex is an **idempotent fill** of the current partition, not a
rebuild. A true rebuild (needed after listing updates/delists or index corruption) is a
**fresh-partition switch**:
```bash
# 1. Fill a fresh partition from the registry (source of truth)
MEMORY_API_PARTITION=market:catalog-v2 \
MEMORY_API_URL=... MEMORY_API_KEY=... FORGEJO_URL=... FORGEJO_TOKEN=... \
LISTING_SCHEMA_PATH=$REPO/schemas/listing.schema.json \
python3 -m m2_market_indexer.reindex
# 2. Switch every reader (CLI/Store/Scout) by config — no code change
export M2_MARKET_CATALOG_PARTITION=market:catalog-v2 # or config management
# 3. The old partition is abandoned in place (memory-api cannot delete it);
# it costs storage only. Fold the suffix rotation back when memory-api
# grows delete support (fedlearn T13 follow-up).
```
Verification: `m2-market search <known listing>` returns the same results against the
new partition (quickstart.md Scenario E).
## memory-api endpoint discovery
The live memory-api container IP changes across Coolify redeploys (known split-brain,
two stacks). Resolve the current one:
```bash
MC=$(docker ps --format '{{.Names}}' | grep memory-api | head -1)
docker inspect $MC | jq -r '.[0].NetworkSettings.Networks | to_entries[0].value.IPAddress'
```
Public route: `https://memory.machinemachine.ai`. Writes need `X-API-Key` (the key lives
in the memory-api container env as MEMORY_API_KEY); reads are currently open.
## Ledger
- Public: `https://ledger.machinemachine.ai` (Cloudflare TLS → Traefik http router — the
domain is registered `http://` in Coolify on purpose; re-adding `https://` recreates
the redirect loop).
- Deploy: push to `m2/m2-market` master → Forgejo webhook (hook id 14) → Coolify deploy.
Webhook delivery can lag ~40 s. Manual: `GET /api/v1/deploy?uuid=<app>` on Coolify.
- Keys: `/home/m2/.m2-ledger-keys` (host, 0600). Service key reads/installs; admin key
grants/snapshots.
- Daily snapshot: host cron 03:10 → `POST /snapshot` → commits
`m2/market-registry:audit/YYYY-MM-DD.json`. Balance disputes: recompute from the tx
log (`GET /tx`), which is append-only truth.
- Refund semantics: compensating refunds may overdraw a seller (debt position, settled
at manual payout reconciliation) — data-model.md §Transaction.
## Publish/status flow (known gap, analyze finding I2)
`m2-market publish` opens the PR with `status: "draft"`; nothing flips it on merge yet.
Until automated: after merging a listing PR, commit `status: "published"` on main (the
indexer only indexes published listings).

View file

@ -39,3 +39,22 @@ Response items: `{listing_id, score, listing: <payload>}` ordered by hybrid scor
Install + proposal events land as memory records in `market:evidence` partition keyed by
listing_id; a periodic indexer pass folds counts into `stats` in the registry (PR-less
stats commit or scheduled batch PR — implementation's choice, registry remains truth).
## AS-BUILT AMENDMENT (2026-07-02, analyze finding I1)
The live memory-api (agent.memory.system) exposes `/memory/store` + `/memory/search`
only — no partition drop, no delete, no upsert-by-id, and its server-side tenant filter
is a scalar `tenant_id` (not an array match). The implementation therefore deviates:
- Partition = the `agent_id` namespace; documents are memories with the listing in
`metadata.listing` and `metadata.listing_id`.
- Writer upsert = search-first for `metadata.listing_id`, store only if absent
(idempotent fill). Listing UPDATES and delist removal require memory-api delete —
until then, delisted/updated listings are filtered/handled at read time.
- Reader tenant filter `tenant_visibility ⊇ {caller tenant | "*"}` is enforced in the
ONE shared reader client (`cli/src/m2_market/catalog.py`), which every surface uses.
- Full rebuild = fresh-partition switch (`MEMORY_API_PARTITION` writer-side,
`M2_MARKET_CATALOG_PARTITION` reader-side) — docs/runbook.md.
The original section above is retained as the target contract for when memory-api
grows partition CRUD (fedlearn T13 follow-up).

View file

@ -60,8 +60,10 @@ pytest ledger/tests/test_properties.py # balance==fold(log); grants a
## Scenario E — catalog rebuildability (constitution II)
```bash
m2-market reindex # drop + rebuild market:catalog
m2-market search "branded pdf report" # same results as before reindex
# memory-api has no delete: rebuild = fresh-partition switch (docs/runbook.md)
MEMORY_API_PARTITION=market:catalog-e2e python3 -m m2_market_indexer.reindex
M2_MARKET_CATALOG_PARTITION=market:catalog-e2e \
m2-market search "branded pdf report" # same results as the live partition
```
## Scenario F — M2 Store on canary (Story 4) [after row 8]