Rail-independence is the plan's spine: fedlearn rails verified NOT landed (no m2/m2-core, no capture/curator CLIs), so the wedge builds against frozen interfaces — m2/market-registry as registry of record, ApplyAdapter seam with local adapter now / m2core-sync stub, schemas frozen here with a fedlearn coordination note. Constitution check passed (1 justified deviation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.7 KiB
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 |
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 |
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 |
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. 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 |
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.