Workflow helpers — skills.*
These two modules are the helper layer the sv-* workflow uses to scaffold,
fork, version and ground a study. They are not part of the simulation engine —
they operate on the study directory — but they are worth knowing if you
drive the pipeline yourself instead of through the skills, or if you script
batch experiments over many studies.
Both are stdlib-only and safe to import without a configured LLM.
skills.sv_workspace — study directories, forks & versions
Scaffolding (scaffold, study_paths), the catalog that sv-init routes
against (discover_studies, catalog_view), forking (fork_study — which
rewrites study_id throughout the copied artifacts, clears reference, and
deliberately carries no code and no run outputs), the capability registry view
(load_capabilities, capability_view), and the version machinery behind
/sv-iterate (init_manifest, create_version, resolve_warm_start).
skills.sv_workspace
Shared helper for the SocioVerse-beta workflow skills.
Scaffolds a standardized study workspace and provides the strict-contract load/save
helpers (thin wrappers over socioverse.validation) the skills must use at every boundary.
scaffold
scaffold(studies_root: str | Path, study_id: str) -> Path
Create the standardized study workspace and return its path.
Source code in skills/sv_workspace.py
| def scaffold(studies_root: str | Path, study_id: str) -> Path:
"""Create the standardized study workspace and return its path."""
root = Path(studies_root) / study_id
for sub in STUDY_SUBDIRS:
(root / sub).mkdir(parents=True, exist_ok=True)
return root
|
write_roster
write_roster(path: str | Path, rows) -> Path
Write the materialized initial roster (population/roster.jsonl): one JSON line per agent,
the t=0 panel row (agent_id, step, state, action_kind, action_payload). This is the durable
'instantiated agents' artifact sv-build-population produces so the dashboard's agent inspector
can show every initialized agent BEFORE any run — the same shape the live panel_live.jsonl
uses, so the dashboard reads it with no special casing. rows = TrajectoryRecords (or dicts
with those keys). Lives under population/, so version snapshots keep it.
Source code in skills/sv_workspace.py
| def write_roster(path: str | Path, rows) -> Path:
"""Write the materialized initial roster (population/roster.jsonl): one JSON line per agent,
the t=0 panel row (agent_id, step, state, action_kind, action_payload). This is the durable
'instantiated agents' artifact sv-build-population produces so the dashboard's agent inspector
can show every initialized agent BEFORE any run — the same shape the live panel_live.jsonl
uses, so the dashboard reads it with no special casing. ``rows`` = TrajectoryRecords (or dicts
with those keys). Lives under population/, so version snapshots keep it."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
def _get(r, k, default=None):
return getattr(r, k, default) if not isinstance(r, dict) else r.get(k, default)
with p.open("w", encoding="utf-8") as f:
for r in rows:
f.write(json.dumps({
"agent_id": _get(r, "agent_id"),
"step": _get(r, "step", 0),
"state": _get(r, "state", {}),
"action_kind": _get(r, "action_kind"),
"action_payload": _get(r, "action_payload", {}),
}, ensure_ascii=False) + "\n")
return p
|
save_study_yaml
save_study_yaml(path: str | Path, study_spec) -> Path
StudySpec is written as JSON-in-.yaml (valid YAML, simple + re-loadable).
Source code in skills/sv_workspace.py
| def save_study_yaml(path: str | Path, study_spec) -> Path:
"""StudySpec is written as JSON-in-.yaml (valid YAML, simple + re-loadable)."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(study_spec.model_dump_json(indent=2), encoding="utf-8")
return p
|
fork_study
fork_study(
studies_root: str | Path,
source_study_id: str,
new_study_id: str,
*,
status: str = "draft",
) -> Path
Path-A reuse: copy an existing study into a NEW study_id so the source stays intact.
Beta's reuse rule is never edit a matched or reference study in place — fork it. Any
study whose study.yaml carries reference: true (the bundled templates), and any
already-adapted study, is a read-only baseline; a new query becomes its own study. This copies the source's
authored artifacts (study.yaml + the resources/environment/population/simulation JSON
that exist, plus grounding/grounding.json — the fork inherits the source's real-world
anchors and refreshes only the entries it changes), rewrites the fork's identity
(study_id across study.yaml AND every copied artifact that embeds it —
resources/environment/population/simulation — plus status / created_by), forces
reference: false (a working fork is never a read-only template), and
DROPS stale run outputs (trajectory/*, reports/*) so the fork re-runs clean. No code
is copied or written: the fork's bundles keep pointing at the source's registered
provider_refs (that is what makes this the "no new code" path). Returns the new study
root. Refuses to overwrite an existing study_id.
Source code in skills/sv_workspace.py
| def fork_study(
studies_root: str | Path,
source_study_id: str,
new_study_id: str,
*,
status: str = "draft",
) -> Path:
"""Path-A reuse: copy an existing study into a NEW ``study_id`` so the source stays intact.
Beta's reuse rule is **never edit a matched or reference study in place — fork it.** Any
study whose ``study.yaml`` carries ``reference: true`` (the bundled templates), and any
already-adapted study, is a read-only baseline; a new query becomes its own study. This copies the source's
authored artifacts (``study.yaml`` + the resources/environment/population/simulation JSON
that exist, plus ``grounding/grounding.json`` — the fork inherits the source's real-world
anchors and refreshes only the entries it changes), rewrites the fork's identity
(``study_id`` across study.yaml AND every copied artifact that embeds it —
resources/environment/population/simulation — plus ``status`` / ``created_by``), forces
``reference: false`` (a working fork is never a read-only template), and
DROPS stale run outputs (``trajectory/*``, ``reports/*``) so the fork re-runs clean. No code
is copied or written: the fork's bundles keep pointing at the source's registered
``provider_refs`` (that is what makes this the "no new code" path). Returns the new study
root. Refuses to overwrite an existing ``study_id``.
"""
src = Path(studies_root) / source_study_id
if not (src / "study.yaml").exists():
raise FileNotFoundError(f"no study to fork at {src}")
dst = Path(studies_root) / new_study_id
if dst.exists():
raise FileExistsError(f"refusing to overwrite existing study {dst}")
scaffold(studies_root, new_study_id)
src_paths, dst_paths = study_paths(src), study_paths(dst)
for key in ("study", "resources", "environment", "population", "simulation", "grounding"):
s = src_paths[key]
if s.exists():
dst_paths[key].parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(s, dst_paths[key])
spec = load_study_yaml(dst_paths["study"])
spec.study_id = new_study_id
spec.status = status
spec.created_by = f"sv-init (fork of {source_study_id})"
spec.reference = False # a working fork is NEVER a read-only template — else it masquerades as
# one (blocks in-place edits, shows a REF badge, can be picked as a build template)
save_study_yaml(dst_paths["study"], spec)
# Rewrite the copied artifacts' embedded study_id too. resources/environment/population/simulation
# each carry `study_id`; a stale SOURCE id mis-tags the fork's trajectory store (DuckDB `study_id`),
# binding the fork's results onto the source study in the dashboard. fork_study historically only
# rewrote study.yaml, so every caller had to hand-patch these (the recurring fork id-drift bug).
for key in ("resources", "environment", "population", "simulation"):
p = dst_paths[key]
if not p.exists():
continue
try:
obj = json.loads(p.read_text())
except (json.JSONDecodeError, OSError):
continue
if isinstance(obj, dict) and obj.get("study_id") not in (None, new_study_id):
obj["study_id"] = new_study_id
p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")
return dst
|
current_version
current_version(study_dir: str | Path) -> str
The version id the live dir currently carries (implicit v1 pre-manifest).
Source code in skills/sv_workspace.py
| def current_version(study_dir: str | Path) -> str:
"""The version id the live dir currently carries (implicit ``v1`` pre-manifest)."""
man = load_manifest(study_dir)
return (man or {}).get("current") or "v1"
|
init_manifest
init_manifest(
study_dir: str | Path, note: str = "initial build"
) -> dict
Register the live dir as v1. sv-init calls this at scaffold/fork time so the
dashboard shows a default version from day one; pre-versioning studies get it
lazily on their first /sv-iterate. No-op when a manifest already exists.
Source code in skills/sv_workspace.py
| def init_manifest(study_dir: str | Path, note: str = "initial build") -> dict:
"""Register the live dir as ``v1``. sv-init calls this at scaffold/fork time so the
dashboard shows a default version from day one; pre-versioning studies get it
lazily on their first /sv-iterate. No-op when a manifest already exists."""
existing = load_manifest(study_dir)
if existing:
return existing
manifest = {
"current": "v1",
"versions": [{"id": "v1", "parent": None, "note": note,
"created_at": time.time(), "snapshot": None, "warm_start": None}],
}
_write_manifest(study_dir, manifest)
return manifest
|
version_trajectory_db
version_trajectory_db(
study_dir: str | Path, version_id: str
) -> Path
Path to an archived version's trajectory store — what warm-start replays from.
create_version snapshots the live dir into versions/<id>/ and study.duckdb is NOT
in SNAPSHOT_EXCLUDE, so a finished version's panel is preserved here for the next version to
inherit. (Only defined for versions that have a snapshot; the live/current version's store is
at trajectory/study.duckdb.)
Source code in skills/sv_workspace.py
| def version_trajectory_db(study_dir: str | Path, version_id: str) -> Path:
"""Path to an archived version's trajectory store — what warm-start replays from.
``create_version`` snapshots the live dir into ``versions/<id>/`` and ``study.duckdb`` is NOT
in SNAPSHOT_EXCLUDE, so a finished version's panel is preserved here for the next version to
inherit. (Only defined for versions that have a snapshot; the live/current version's store is
at ``trajectory/study.duckdb``.)"""
return Path(study_dir) / VERSIONS_DIR / version_id / "trajectory" / "study.duckdb"
|
create_version
create_version(
study_dir: str | Path,
note: str,
base: str | None = None,
warm_start: dict | None = None,
resume_from_stage: str | None = None,
) -> str
Iterate the study to a NEW version, preserving the current one as a snapshot.
This is the version-iterate path of /sv-iterate (the in-place path simply skips
this call). Order is crash-safe: (1) freeze live → versions/v<current>/;
(2) if base names an older version, restore its snapshot into the live dir
(copy2 keeps mtimes, so untouched artifacts read as carried-from-parent);
(3) drop the live run outputs (kept in the snapshot) so the new version starts at
"built, not yet run"; (4) append the new entry + move current (atomic, last —
a half-done copy is invisible to the dashboard until the manifest says otherwise).
note is the one-line human description shown in the dashboard's version box.
base defaults to the current version; passing an older id branches from it
(cross-version iteration). warm_start (optional) records the inherit-steps provenance
on the NEW version entry — {"source": <parent v>, "resume_from": K} — set by /sv-iterate
when the user opts to inherit the parent's already-run steps (the parent's snapshot is what
sv-run then replays). resume_from_stage (optional) records the earliest sv-* stage this
iteration re-enters (e.g. "sv-build-environment"); the dashboard reads it so the new
version's progress pointer parks on that stage — rather than reading the parent's carried
artifacts as this version's finished work — until Claude actually re-authors it. Returns the
new version id. The caller (skill contract) must not invoke this while a run is writing the
trajectory store.
Source code in skills/sv_workspace.py
| def create_version(study_dir: str | Path, note: str, base: str | None = None,
warm_start: dict | None = None, resume_from_stage: str | None = None) -> str:
"""Iterate the study to a NEW version, preserving the current one as a snapshot.
This is the **version-iterate** path of /sv-iterate (the in-place path simply skips
this call). Order is crash-safe: (1) freeze live → ``versions/v<current>/``;
(2) if ``base`` names an older version, restore its snapshot into the live dir
(``copy2`` keeps mtimes, so untouched artifacts read as carried-from-parent);
(3) drop the live run outputs (kept in the snapshot) so the new version starts at
"built, not yet run"; (4) append the new entry + move ``current`` (atomic, last —
a half-done copy is invisible to the dashboard until the manifest says otherwise).
``note`` is the one-line human description shown in the dashboard's version box.
``base`` defaults to the current version; passing an older id branches from it
(cross-version iteration). ``warm_start`` (optional) records the inherit-steps provenance
on the NEW version entry — ``{"source": <parent v>, "resume_from": K}`` — set by /sv-iterate
when the user opts to inherit the parent's already-run steps (the parent's snapshot is what
`sv-run` then replays). ``resume_from_stage`` (optional) records the earliest sv-* stage this
iteration re-enters (e.g. ``"sv-build-environment"``); the dashboard reads it so the new
version's progress pointer parks on that stage — rather than reading the parent's carried
artifacts as this version's finished work — until Claude actually re-authors it. Returns the
new version id. The caller (skill contract) must not invoke this while a run is writing the
trajectory store.
"""
d = Path(study_dir)
if not (d / "study.yaml").exists():
raise FileNotFoundError(f"no study at {d}")
manifest = load_manifest(d) or init_manifest(d)
cur = manifest["current"]
entries = {v["id"]: v for v in manifest["versions"]}
base = base or cur
if base not in entries:
raise ValueError(f"unknown base version {base!r}; have {sorted(entries)}")
# Branching from an older version is a big context switch → default the re-entry to sv-init so
# the dashboard's review gate (see compute_stages) makes Claude re-review EVERY stage from the
# start. A same-line new version leaves resume_from_stage to the caller (the earliest affected
# stage); if that too is omitted, compute_stages fails safe to the first carried stage, never run.
if resume_from_stage is None and base != cur:
resume_from_stage = "sv-init"
# 1) freeze the live dir as the current version's archive
entries[cur]["snapshot"] = _snapshot_live(d, cur)
# 2) branching from an older version: make live == that snapshot (minus outputs)
if base != cur:
base_snap = d / (entries[base].get("snapshot") or "")
if not base_snap.is_dir():
raise FileNotFoundError(f"base version {base!r} has no snapshot at {base_snap}")
for child in d.iterdir():
if child.name in RESTORE_KEEP:
continue
shutil.rmtree(child) if child.is_dir() else child.unlink()
for child in base_snap.iterdir():
if child.name in OUTPUT_SUBDIRS:
continue # outputs stay archived; the new version re-runs
dst = d / child.name
if child.is_dir():
shutil.copytree(child, dst)
else:
shutil.copy2(child, dst)
# 3) clear live run outputs (preserved in the snapshot) + restore the empty layout
for sub in OUTPUT_SUBDIRS:
if (d / sub).exists():
shutil.rmtree(d / sub)
for sub in STUDY_SUBDIRS:
(d / sub).mkdir(parents=True, exist_ok=True)
# 4) register the new version and point `current` at it (atomic, last)
nums = [int(v["id"][1:]) for v in manifest["versions"]
if v["id"].startswith("v") and v["id"][1:].isdigit()]
new_id = f"v{(max(nums) if nums else len(manifest['versions'])) + 1}"
manifest["versions"].append({"id": new_id, "parent": base, "note": note,
"created_at": time.time(), "snapshot": None,
"warm_start": warm_start, # {source, resume_from} | None
"resume_from_stage": resume_from_stage}) # earliest re-entered stage | None
manifest["current"] = new_id
_write_manifest(d, manifest)
return new_id
|
resolve_warm_start
resolve_warm_start(study_dir: str | Path) -> dict | None
Turn the current version's manifest warm_start provenance into runnable engine params.
/sv-iterate records {"source": <parent v>, "resume_from": K} on the new version entry;
sv-run calls this to build a WarmStartSpec — it resolves source to the parent's
archived store (versions/<source>/trajectory/study.duckdb) and returns
{source_version, source_trajectory, resume_from}, or None when there is no warm-start
or the parent snapshot is missing (→ fall back to a cold run from step 0). Keeps the manifest
the single source of truth (sv-run doesn't re-derive the resume point).
Source code in skills/sv_workspace.py
| def resolve_warm_start(study_dir: str | Path) -> dict | None:
"""Turn the current version's manifest ``warm_start`` provenance into runnable engine params.
/sv-iterate records ``{"source": <parent v>, "resume_from": K}`` on the new version entry;
sv-run calls this to build a ``WarmStartSpec`` — it resolves ``source`` to the parent's
archived store (``versions/<source>/trajectory/study.duckdb``) and returns
``{source_version, source_trajectory, resume_from}``, or ``None`` when there is no warm-start
or the parent snapshot is missing (→ fall back to a cold run from step 0). Keeps the manifest
the single source of truth (sv-run doesn't re-derive the resume point)."""
man = load_manifest(study_dir)
if not man:
return None
cur = man.get("current")
entry = next((v for v in man.get("versions", []) if v.get("id") == cur), None)
ws = (entry or {}).get("warm_start")
if not ws or not ws.get("source"):
return None
db = version_trajectory_db(study_dir, ws["source"])
if not db.exists():
return None
return {"source_version": ws["source"], "source_trajectory": str(db),
"resume_from": int(ws.get("resume_from", 0))}
|
discover_studies
discover_studies(
studies_root: str | Path = "studies",
) -> list
The runtime 'catalog': glob studies/*/study.yaml -> list[StudySpec].
sv-init Step 0 calls this to see what has already been adapted, then matches a new
query against the discovery fields (domain / tags / legacy_simulator / metrics /
adjustable_params) to choose Path A (reuse) vs Path B (build new). Decentralised by
design — each study carries its own card in its own study.yaml, so collaborators add
studies without editing a shared index (no merge conflicts). Malformed/partial
study.yaml files are skipped rather than raising.
Source code in skills/sv_workspace.py
| def discover_studies(studies_root: str | Path = "studies") -> list:
"""The runtime 'catalog': glob ``studies/*/study.yaml`` -> list[StudySpec].
sv-init Step 0 calls this to see what has already been adapted, then matches a new
query against the discovery fields (domain / tags / legacy_simulator / metrics /
adjustable_params) to choose Path A (reuse) vs Path B (build new). Decentralised by
design — each study carries its own card in its own study.yaml, so collaborators add
studies without editing a shared index (no merge conflicts). Malformed/partial
study.yaml files are skipped rather than raising.
"""
out = []
for p in sorted(Path(studies_root).glob("*/study.yaml")):
try:
out.append(load_study_yaml(p))
except Exception:
continue
return out
|
catalog_view
catalog_view(
studies_root: str | Path = "studies",
) -> list[dict]
Compact, matching-relevant projection of discover_studies() for display/routing.
Source code in skills/sv_workspace.py
| def catalog_view(studies_root: str | Path = "studies") -> list[dict]:
"""Compact, matching-relevant projection of discover_studies() for display/routing."""
fields = ("study_id", "title", "domain", "tags", "study_type", "metrics",
"legacy_simulator", "provider_refs", "adjustable_params", "status",
"demonstrates", "teaches", "reference")
return [{f: getattr(s, f) for f in fields} for s in discover_studies(studies_root)]
|
load_capabilities
load_capabilities(
path: str | Path | None = None,
) -> list[dict]
Raw registry entries (list of dicts); [] when the registry is absent or empty.
Lazy-imports yaml so environments without PyYAML can still use every other helper.
Source code in skills/sv_workspace.py
| def load_capabilities(path: str | Path | None = None) -> list[dict]:
"""Raw registry entries (list of dicts); [] when the registry is absent or empty.
Lazy-imports yaml so environments without PyYAML can still use every other helper."""
import yaml
p = Path(path) if path is not None else CAPABILITIES_PATH
if not p.exists():
return []
data = yaml.safe_load(p.read_text(encoding="utf-8")) or []
return [e for e in data if isinstance(e, dict) and e.get("name")]
|
capability_view
capability_view(
stage: str | None = None,
path: str | Path | None = None,
mcp_json: str | Path | None = None,
) -> list[dict]
catalog_view's sibling for external SERVICES — what each build skill's generic
"capability check" hook lists.
stage filters to the entries anchored at that skill (their stages field);
None returns all. Each entry is returned verbatim plus available /
availability_note resolved from local config. Adding, removing, or changing a
service is an edit to the registry only — the skills' hook text never changes.
Source code in skills/sv_workspace.py
| def capability_view(stage: str | None = None, path: str | Path | None = None,
mcp_json: str | Path | None = None) -> list[dict]:
"""``catalog_view``'s sibling for external SERVICES — what each build skill's generic
"capability check" hook lists.
``stage`` filters to the entries anchored at that skill (their ``stages`` field);
``None`` returns all. Each entry is returned verbatim plus ``available`` /
``availability_note`` resolved from local config. Adding, removing, or changing a
service is an edit to the registry only — the skills' hook text never changes.
"""
mj = Path(mcp_json) if mcp_json is not None else MCP_JSON_PATH
out = []
for e in load_capabilities(path):
if stage and stage not in (e.get("stages") or []):
continue
avail, note = _capability_available(e, mj)
out.append({**e, "available": avail, "availability_note": note})
return out
|
skills.sv_grounding — the provenance sidecar
Read/merge access to studies/<id>/grounding/grounding.json. merge upserts
facts and assumptions by id, so every build stage can extend the ledger
without clobbering an earlier stage's entries, and writes atomically.
Deliberately not a Pydantic handoff: the ledger is append-friendly documentation
for review, not a runtime-validated contract. See
Grounding & Provenance.
skills.sv_grounding
Grounding sidecar for SocioVerse-beta studies (stdlib only — no socioverse import).
studies/<id>/grounding/grounding.json records the study's real-world anchors:
facts with provenance, implementation_refs (how similar phenomena are modeled),
and declared assumptions. Deliberately NOT a pydantic handoff artifact — a flat,
merge-friendly JSON the sv-* stages append to ("ground before you invent") and the
dashboard renders read-only. Downloaded reference tables (small, MB-scale) live next
to it under grounding/data/ and are pointed at by source.local_path.
Document shape (convention, not schema):
{"study_id": "...", "query": "...", "updated_at": "...", "method_notes": "...",
"implementation_refs": [{"title", "url", "takeaway", "accessed"}],
"facts": [{"id", "claim", "value", "unit", "as_of",
"basis": "sourced" | "proxy" | "assumed",
"source": {"title", "url",
"via": "event_service" | "web_search" | "provider" | "user",
"accessed", "local_path"?},
"applies_to": ["environment.provider_args.base", ...], "note"}],
"assumptions": [{"id", "claim", "rationale"}]}
method_notes="stylized" + empty facts records the decision that a study has no
real-world anchors (abstract/toy models) — distinct from grounding merely missing.
load
load(study_dir: str | Path) -> dict | None
Parsed grounding.json, or None when missing/corrupt (callers treat as empty).
Source code in skills/sv_grounding.py
| def load(study_dir: str | Path) -> dict | None:
"""Parsed grounding.json, or None when missing/corrupt (callers treat as empty)."""
p = grounding_path(study_dir)
if not p.exists():
return None
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return None
|
merge
merge(
study_dir: str | Path,
*,
facts=(),
implementation_refs=(),
assumptions=(),
method_notes: str | None = None,
query: str | None = None,
) -> Path
Merge entries into grounding.json (creating it, and grounding/, if absent).
Facts/assumptions upsert by id (order preserved; missing ids assigned f<N>/
a<N> past collisions); refs dedupe by url; method_notes/query overwrite
only when not None. updated_at bumps on every merge. Write is atomic (tmp +
rename, like sv_workspace._write_manifest) — the dashboard poller must never read
a torn file. Returns the file path.
Source code in skills/sv_grounding.py
| def merge(study_dir: str | Path, *, facts=(), implementation_refs=(), assumptions=(),
method_notes: str | None = None, query: str | None = None) -> Path:
"""Merge entries into grounding.json (creating it, and ``grounding/``, if absent).
Facts/assumptions upsert by ``id`` (order preserved; missing ids assigned ``f<N>``/
``a<N>`` past collisions); refs dedupe by url; ``method_notes``/``query`` overwrite
only when not None. ``updated_at`` bumps on every merge. Write is atomic (tmp +
rename, like sv_workspace._write_manifest) — the dashboard poller must never read
a torn file. Returns the file path.
"""
doc = load(study_dir) or _skeleton(study_dir)
doc["facts"] = _merge_by_id(doc.get("facts") or [], facts, "f")
doc["assumptions"] = _merge_by_id(doc.get("assumptions") or [], assumptions, "a")
doc["implementation_refs"] = _merge_refs(doc.get("implementation_refs") or [],
implementation_refs)
if method_notes is not None:
doc["method_notes"] = method_notes
if query is not None:
doc["query"] = query
doc["updated_at"] = _now_iso()
p = grounding_path(study_dir)
p.parent.mkdir(parents=True, exist_ok=True) # tolerate pre-grounding scaffolds
tmp = p.with_name(p.name + ".tmp")
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(p)
return p
|
summary
summary(study_dir: str | Path) -> str
One-liner for sv_emit narratives / stage reports.
'12 facts: 8 sourced / 2 proxy / 2 assumed · 3 refs · 2 assumptions'
'stylized model, no real-world anchors' (method_notes=="stylized", no facts)
'no grounding recorded' (file missing/corrupt)
Source code in skills/sv_grounding.py
| def summary(study_dir: str | Path) -> str:
"""One-liner for sv_emit narratives / stage reports.
'12 facts: 8 sourced / 2 proxy / 2 assumed · 3 refs · 2 assumptions'
'stylized model, no real-world anchors' (method_notes=="stylized", no facts)
'no grounding recorded' (file missing/corrupt)
"""
doc = load(study_dir)
if doc is None:
return "no grounding recorded"
facts = doc.get("facts") or []
if not facts and doc.get("method_notes") == "stylized":
return "stylized model, no real-world anchors"
counts = {b: 0 for b in BASES}
for f in facts:
if f.get("basis") in counts:
counts[f["basis"]] += 1
parts = [f"{len(facts)} facts: " + " / ".join(f"{counts[b]} {b}" for b in BASES)]
if doc.get("implementation_refs"):
parts.append(f"{len(doc['implementation_refs'])} refs")
if doc.get("assumptions"):
parts.append(f"{len(doc['assumptions'])} assumptions")
return " · ".join(parts)
|