Code / src/modelmap/atlas/mapcard.py
src/modelmap/atlas/mapcard.py
132 lines
# =============================================================================
# Project : modelmap
# File : src/modelmap/atlas/mapcard.py
# Purpose : Map card schema v0 — machine-readable provenance + confidence
# Author : Simon-Pierre Boucher
# Contact : contact@spboucher.ai
# Website : https://modelmap.io
# Created : 2026-08-12
# Modified : 2026-08-12
# Platform : macOS / Apple Silicon (arm64)
# License : All rights reserved (research code)
# =============================================================================
"""Map card v0 (gap G23, ranked #1 in Phase 4).
A map card is the datasheet of a published map. `tools/publish.py` refuses to
export any atlas entry whose card does not validate. Fields follow the Phase 2
conclusions: pinned intervention protocol, ablation-scheme coverage, featurizer
class, seed replication, per-dataset agreement — plus the charter's provenance
block (commit, config, model hash, hardware, dates).
The schema will evolve (v1+) with the first real maps; version is embedded so
old cards remain interpretable.
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
SCHEMA_VERSION = "0.1"
CONFIDENCE_LEVELS = {
0: "anecdotal",
1: "correlational",
2: "method-robust",
3: "causal",
}
# Fields that must be non-empty for ANY publishable card.
REQUIRED_ALWAYS = (
"map_id", "map_type", "model_id", "model_hash", "quantization",
"commit", "config", "created", "hardware_manifest",
"confidence_level", "regenerate_command",
)
# Additional requirements per confidence level (charter §10 taxonomy).
REQUIRED_LEVEL_1 = ("seeds", "prompt_sets", "controls")
REQUIRED_LEVEL_2 = REQUIRED_LEVEL_1 + ("methods_in_agreement",)
REQUIRED_LEVEL_3 = REQUIRED_LEVEL_2 + ("interventions",)
@dataclass
class MapCard:
"""v0 map card. All list/dict fields default empty; validate() enforces."""
map_id: str = "" # atlas/<model>/<type>/<version>
map_type: str = "" # probes | geometry | features | circuits | ...
model_id: str = "" # e.g. Qwen/Qwen3-0.6B
model_hash: str = "" # checkpoint content hash
quantization: str = "" # fp16 | q8 | q4 | q2 | mixed:<desc>
commit: str = "" # modelmap git commit that generated the map
config: str = "" # path or inline JSON of the run config
created: str = "" # YYYY-MM-DD
hardware_manifest: dict = field(default_factory=dict)
confidence_level: int = 0
regenerate_command: str = "" # exact command that rebuilds the map
seeds: list = field(default_factory=list) # ≥3 for Level 1+
prompt_sets: list = field(default_factory=list) # ≥2 for Level 1+ (name+checksum)
controls: list = field(default_factory=list) # e.g. shuffled-labels, random-init
methods_in_agreement: list = field(default_factory=list) # ≥2 for Level 2+
interventions: list = field(default_factory=list) # ≥1 for Level 3
replication_rate: float | None = None # map-to-map agreement under reseeding
per_dataset_agreement: float | None = None
ablation_schemes: list = field(default_factory=list) # curves published, e.g. zero/mean/resample
featurizer_class: str = "" # natural-basis | linear | orthogonal | DAS-optimized
intervention_protocol: str = "" # pinned corruption + metric (Phase 2 rule)
negative_result: bool = False
notes: str = ""
author: str = "Simon-Pierre Boucher"
contact: str = "contact@spboucher.ai"
website: str = "https://modelmap.io"
schema_version: str = SCHEMA_VERSION
# ------------------------------------------------------------------ api
def validate(self) -> list[str]:
"""Return a list of violations; empty list means publishable."""
errs: list[str] = []
for f in REQUIRED_ALWAYS:
v = getattr(self, f)
if v in ("", None) or v == {}:
errs.append(f"missing required field: {f}")
lvl = self.confidence_level
if lvl not in CONFIDENCE_LEVELS:
errs.append(f"confidence_level must be 0-3, got {lvl!r}")
return errs
extra = {1: REQUIRED_LEVEL_1, 2: REQUIRED_LEVEL_2, 3: REQUIRED_LEVEL_3}.get(lvl, ())
for f in extra:
if not getattr(self, f):
errs.append(f"Level {lvl} requires non-empty: {f}")
if lvl >= 1:
if len(self.seeds) < 3:
errs.append("Level 1+ requires >=3 seeds")
if len(self.prompt_sets) < 2:
errs.append("Level 1+ requires >=2 prompt sets")
if self.replication_rate is None:
errs.append("Level 1+ requires replication_rate")
if lvl >= 2 and len(self.methods_in_agreement) < 2:
errs.append("Level 2 requires >=2 independent methods in agreement")
if lvl >= 3 and not self.featurizer_class:
errs.append("Level 3 requires featurizer_class disclosure")
return errs
def to_json(self) -> str:
return json.dumps(asdict(self), indent=2, ensure_ascii=False) + "\n"
@classmethod
def from_json(cls, text: str) -> MapCard:
data = json.loads(text)
known = {f for f in cls.__dataclass_fields__} # tolerate future fields
return cls(**{k: v for k, v in data.items() if k in known})
def save(self, path: Path) -> None:
path.write_text(self.to_json())
@classmethod
def load(cls, path: Path) -> MapCard:
return cls.from_json(path.read_text())