m2-market/specs/001-market-first-wedge/data-model.md
m2 (AI Agent) 5d651705ff wedge: adapt catalog clients to REAL memory-api (store/search, agent_id partitions)
- CatalogClient: POST /memory/search, results via metadata.listing; tenant
  visibility filter enforced in the one shared reader client (memory-api
  tenant_id is scalar; deviation recorded in the module docstring)
- indexer MemoryClient: upsert = exists-check + store; drop/delete are WARN
  no-ops (API has no delete) — reindex is an idempotent fill for now
- refund-overdraw decided: compensating refunds may push a seller negative
  (debt, manual reconciliation); data-model amended, property machine updated
- probe doc stored in market:probe partition during API discovery (harmless)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 04:09:33 +02:00

115 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Data Model — 001-market-first-wedge (Phase 1)
Entities from spec.md §Key Entities, concretized. Schemas frozen in `schemas/` are the
normative source; this file is the narrative view.
## Solution (`schemas/solution.schema.json`, v1 frozen)
The installable bundle. Manifest-core fields kept field-compatible with the fedlearn
submission/manifest design (coordination item R3).
| Field | Type | Notes |
|---|---|---|
| `schema_version` | const `m2.solution.v1` | frozen |
| `solution_id` | string `sol_<slug>` | unique across registry |
| `name`, `summary`, `description` | string | presentation |
| `intent` | string | what outcome this delivers |
| `behavior` | object | prompts / skills / playbooks refs (paths in payload) |
| `tools` | array | MCP/API/connector requirements |
| `runtime` | object | required surfaces: hermes / desktop / browser / openclaw |
| `memory_schema` | object? | partitions/keys the solution reads/writes |
| `permissions` | array | declarative permission requests (shown as diff pre-install) |
| `deployment` | object | `recipe.yaml` ref + entrypoint + verify command |
| `applicability` | object | `image_classes[]`, `roles[]`, `tool_requirements[]` (fedlearn-compatible) |
| `tenant_scope` | string | `m2-core` (shared) or tenant id; drives firewall checks |
| `evidence` | array | provenance refs: `{source, excerpt?, machine?, session?, tokens?, wall_time?}` — ≥1 REQUIRED |
| `content_hash` | string sha256 | bundle integrity |
| `price` | object | `{amount: int>0, currency: "m2cr", model: "fixed"}``model` enum extensible (job/metered later, headroom per operator steer) |
| `seller` | string operator_id | |
| `license` | object | `{terms: string, major_version_coverage: true}` |
| `revenue_split` | object | `{platform_pct: number}` (default from ledger config) |
**Validation**: evidence non-empty; price.amount ≥ 1; tenant_scope=tenant-id requires
`owner_initiated: true` + `scrub_status.double_scrubbed: true`; content_hash matches bundle.
## Listing (`schemas/listing.schema.json`, v1 frozen)
Catalog-facing record; truth in `m2/market-registry`, derived copy in `market:catalog`.
| Field | Type | Notes |
|---|---|---|
| `schema_version` | const `m2.listing.v1` | |
| `listing_id` | string `lst_<slug>` | |
| `solution_id` + `solution_version` | refs | what's being sold |
| `inventory_type` | enum `solution|capability|resource|service` | only `solution` transacted in v1 |
| `name`, `summary`, `category`, `keywords[]`, `icon?` | | cargstore-catalog compatible |
| `price` | object | denormalized from solution at listing time |
| `seller` | operator_id | |
| `evidence_summary` | string | rendered from solution evidence |
| `tenant_visibility` | array | tenant ids or `["*"]`; catalog queries filter on this |
| `stats` | object | `{installs: int, rating: number?, proposals_shown: int, proposals_accepted: int}` — conversion signals (FR-012), updated by telemetry, start zeroed |
| `status` | enum `draft|in_review|published|delisted` | delisted keeps licenses valid |
| `install_ref` | string | registry release tag `<listing_id>-v<semver>` |
**State transitions**: `draft → in_review` (PR opened) `→ published` (PR merged, no veto
label) `→ delisted` (delist commit; grants unaffected). Veto label on PR → back to `draft`.
## Transaction (m2-ledger, SQLite `tx` table — append-only)
| Column | Type | Notes |
|---|---|---|
| `id` | integer PK autoincrement | |
| `ts` | text ISO-8601 UTC | set by service |
| `from_id` | text | operator_id or `platform` or `mint` |
| `to_id` | text | operator_id or `platform` |
| `amount` | integer > 0 | whole credits in v1 |
| `reason` | enum `install|payout|grant|route|earn|refund` | `refund` added for FR-005 compensation |
| `ref` | text | listing_id / grant note / install id |
**Invariants**: rows never UPDATEd or DELETEd; `balance(op) = Σ to_id=op Σ from_id=op`;
service refuses tx that would take a non-`mint` balance negative — EXCEPT `refund`
reversals, which are compensating entries and may overdraw a seller/platform that already
spent the proceeds (a debt position, settled at manual payout reconciliation; found by the
T013 property state machine, decided 2026-07-02). An install produces 23
rows atomically (one SQLite transaction): buyer→seller (net), buyer→platform (cut), and on
apply-failure a compensating seller/platform→buyer `refund` pair referencing the same
install id.
## LicenseGrant (m2-ledger, `grant` table)
| Column | Type | Notes |
|---|---|---|
| `grant_id` | text `gr_<uuid>` | |
| `operator_id` | text | buyer |
| `listing_id` | text | |
| `solution_version_major` | int | license covers this major (edge case: updates free within major) |
| `tx_id` | int FK → tx | the debit that paid for it; NOT NULL (no grant without payment — SC-003) |
| `ts` | text | |
## Wallet (derived — no table)
Balance is always computed from `tx`. A `balance_cache` may exist for performance but is
rebuildable and never authoritative (constitution V). Daily snapshot = full balances dump
committed to `m2/market-registry:audit/YYYY-MM-DD.json`.
## Operator
Provisioned mapping, not a service: `operator_id` from fleet.json tenancy; CLI config
(`~/.m2-market/config.toml`) carries `operator_id` + API keys per machine. Ledger admin
endpoint issues starter grants against any operator_id.
## Proposal (Scout v0 — local JSONL + telemetry event)
| Field | Notes |
|---|---|
| `proposal_id`, `ts`, `desktop`, `listing_id`, `confidence` | |
| `outcome` | `shown|dismissed|accepted|suppressed_rate|suppressed_optout` |
Outcomes are appended locally and shipped (summarized, on-box) to memory as listing
evidence; `stats.proposals_*` on the listing updated via the indexer telemetry path.
## InstallState (per machine, `~/.m2-market/state.json`)
Mirrors fedlearn state.json design: `{installs: {listing_id: {version, grant_id, ts,
changeset_hash, status: applied|failed|rolled_back}}}` — the idempotency + re-apply guard
for the local adapter, and the file m2core_sync will merge into when fedlearn lands.