- 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>
115 lines
5.9 KiB
Markdown
115 lines
5.9 KiB
Markdown
# 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 2–3
|
||
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.
|