diff --git a/cli/src/m2_market/catalog.py b/cli/src/m2_market/catalog.py index deed796..8e3bce8 100644 --- a/cli/src/m2_market/catalog.py +++ b/cli/src/m2_market/catalog.py @@ -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: diff --git a/docs/runbook.md b/docs/runbook.md new file mode 100644 index 0000000..0f111fa --- /dev/null +++ b/docs/runbook.md @@ -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 ` 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=` 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). diff --git a/specs/001-market-first-wedge/contracts/catalog-index.md b/specs/001-market-first-wedge/contracts/catalog-index.md index 1efc61f..6e06991 100644 --- a/specs/001-market-first-wedge/contracts/catalog-index.md +++ b/specs/001-market-first-wedge/contracts/catalog-index.md @@ -39,3 +39,22 @@ Response items: `{listing_id, score, listing: }` 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). diff --git a/specs/001-market-first-wedge/quickstart.md b/specs/001-market-first-wedge/quickstart.md index 5aac56c..b07441f 100644 --- a/specs/001-market-first-wedge/quickstart.md +++ b/specs/001-market-first-wedge/quickstart.md @@ -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]