diff --git a/docs/architecture/corpus-coverage-ledger.md b/docs/architecture/corpus-coverage-ledger.md new file mode 100644 index 0000000..d948320 --- /dev/null +++ b/docs/architecture/corpus-coverage-ledger.md @@ -0,0 +1,71 @@ +# Corpus Coverage Ledger + +The corpus coverage ledger is the project-level index for accepted and planned +Aetheria training fixtures. + +Its job is not to store story content. Its job is to keep the corpus honest: +which factions, movements, flashpoints, timeline lanes, tonal modes, collision +axes, review states, and item/economy hooks are represented, and which are still +missing. + +## Live Files + +- Ledger: `state/corpus-coverage.json` +- Schema reference: `schemas/corpus-coverage.schema.json` +- Validator/status tool: `tools/validate_corpus_coverage.py` +- Command: `npm run coverage:status` + +## Entry Contract + +Each fixture entry records: + +- `fixture_id`: stable fixture handle. +- `status`: `planned`, `draft`, `accepted_as_draft`, `accepted`, + `needs_revision`, `rejected`, or `superseded`. +- `timeline_lane`: `historical_grounded`, `transition_grounded`, or + `future_branch`. +- `canon_status`: single-history, branch-local, promoted branch, template, or + draft status. +- `story_type`: founding-era, day-in-the-life, flashpoint, movement, + future-branch, or trade-life story. +- `primary_subjects` and `secondary_subjects`: factions, movements, + institutions, species/body groups, or other represented subjects. +- `collision_axes`: the pressures the fixture exercises, such as law, labor, + care, class, logistics, species/body affordance, trade, memory, personhood, + technology, legitimacy, or infrastructure. +- `tonal_modes`: prose modes the fixture contributes to the corpus. +- `training_targets`: organs this fixture can train or review. +- `artifacts`: paths to fixture files. +- `review`: status of schema, narrative, lore, spatial, visual, and IF review. +- `economy_hooks`: item, assembly, trade-good, supply-chain, or tech hooks. +- `open_lore_gaps`: blocking or useful gaps discovered during generation. + +## Acceptance Rule + +An accepted or accepted-as-draft fixture must have the core artifact refs: + +- lore digest +- agent state +- coordinator artifact +- Ink file +- training sidecar + +It must also carry review fields for schema, narrative, lore, spatial, visual, +and IF artifact review. A fixture may still be accepted-as-draft with review +caveats, but the caveats should be visible instead of hidden under the nice rug. + +## Report Shape + +`npm run coverage:status` validates the ledger and prints: + +- total entries and accepted count +- progress toward the 100-150 broad target +- status, timeline lane, story type, collision-axis, tonal-mode, and subject + counts +- major-power founding/day-in-life candidates when present +- basic gap signals, such as missing future-branch coverage or no day-in-life + stories yet + +This is deliberately a coarse instrument. It should tell us where to point the +next fixture without pretending to be a corpus scientist in a lab coat made of +spreadsheet tabs. diff --git a/notes/documentation-inventory.md b/notes/documentation-inventory.md index 278d6f8..3747055 100644 --- a/notes/documentation-inventory.md +++ b/notes/documentation-inventory.md @@ -38,6 +38,8 @@ not slowly melt into one cursed binder. format. - `docs/architecture/aetheria-lore-grounding-architecture.md`: Aetheria historical, future-branch, and procedural grounding boundaries. +- `docs/architecture/corpus-coverage-ledger.md`: corpus coverage ledger + contract and status-report shape. - `docs/architecture/technology-item-manifest-plan.md`: item, assembly, supply-chain, trade-good, and blueprint data target. - `docs/architecture/training-plan.md`: trainable organs, datasets, gates, and @@ -76,6 +78,8 @@ not slowly melt into one cursed binder. - `state/evidence.jsonl`: distilled current evidence ledger. - `state/evidence.archive.jsonl`: archival evidence ledger. - `state/branches.json`: branch ledger. +- `state/corpus-coverage.json`: machine-readable coverage ledger for accepted, + draft, planned, rejected, and superseded Aetheria fixtures. - `state/scratch.md`: disposable one-subgoal scratch surface. ## Maintenance Rule diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index b8bbe4a..a7d5d46 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -56,6 +56,8 @@ The active path is a source-grounded branching-scene data loop: - Responder packet schema: `schemas/responder-packet.schema.json` - Responder output schema: `schemas/responder-output.schema.json` - Prompt folder: `prompts/` +- Corpus coverage ledger: `state/corpus-coverage.json` +- Corpus coverage command: `npm run coverage:status` - Sandboxed responder prompt template: `prompts/sandboxed-responder-packet.md` - Research sanity responder prompt template: `prompts/research-sanity-responder.md` - Narrative quality reviewer prompt: `prompts/narrative-quality-reviewer.md` @@ -135,8 +137,9 @@ steering surfaces unless the user explicitly asks for archaeology. ## Current Next Action -Add the corpus coverage ledger seam or choose the next Aetheria fixture from the -100-150 broad coverage target. +Use the corpus coverage ledger to choose the next Aetheria fixture from the +100-150 broad coverage target. Current coverage begins with Pallas Species +Strikes as the first accepted-as-draft row. ## Warnings diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index da11b66..81c8645 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -18,6 +18,7 @@ validated artifact seams for producing reviewed training data. - `state/map.yaml`: canonical mission, boundaries, live architecture, next action. - `state/evidence.jsonl`: distilled belief-changing evidence only. - `state/evidence.archive.jsonl`: older evidence preserved for archaeology. +- `state/corpus-coverage.json`: accepted/planned fixture coverage ledger. - `notes/fresh-workspace-handoff.md`: compact re-entry packet. - `notes/ghostlight-implementation-plan.md`: near-term implementation sequence. - `docs/architecture/`: detailed contracts and rationale. @@ -79,6 +80,7 @@ the room. - Ink branch contract: `docs/architecture/ink-branching-scenes.md` - Illustrated IF visual pipeline: `docs/architecture/illustrated-if-visual-pipeline.md` - Training stages and corpus gates: `docs/architecture/training-plan.md` +- Corpus coverage ledger: `docs/architecture/corpus-coverage-ledger.md` ## Current Live Example @@ -118,7 +120,7 @@ paths as active state surfaces. - Full scene/event loop implementation - Culture prior engine - Automatic promotion of branch outcomes into canonical state -- Corpus coverage ledger +- Corpus coverage ledger expansion beyond the initial Pallas row ## Current North Star @@ -129,3 +131,6 @@ receipts, branch compiler artifacts, IF review findings, visual scene plans, and clear branch consequences. The samples should also train tonal control: a fixture needs an intentional prose mode and enough ordinary life for consequence to matter. + +Use `npm run coverage:status` before choosing new fixtures so the corpus grows +by coverage need instead of proximity to the nearest interesting disaster. diff --git a/notes/ghostlight-implementation-plan.md b/notes/ghostlight-implementation-plan.md index bfe5441..46da861 100644 --- a/notes/ghostlight-implementation-plan.md +++ b/notes/ghostlight-implementation-plan.md @@ -42,7 +42,7 @@ not carry live next-action lists. - Preserve the lessons it established: ordinary-life onboarding, branch-and-fold discipline, material state variables, source-backed lore elaboration, visual segmentation, scene-set need for illustrated replay, and review before acceptance. 4. Generalize the loop. - - Add the corpus coverage ledger seam so future fixtures can be tracked by faction, movement, flashpoint, tonal mode, training target, review status, and cultural collisions. + - Use the corpus coverage ledger so future fixtures can be tracked by faction, movement, flashpoint, tonal mode, training target, review status, and cultural collisions. - Select the next Aetheria fixture from the 100-150 broad coverage target. - Add more historical grounded fixtures from AetheriaLore. - Add future-branch Elysium fixtures with branch lineage and constraint labels. diff --git a/package.json b/package.json index 100b299..12d2e5c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "state:status": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\ghostlight_state.py status", "state:prepare-compaction": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\ghostlight_prepare_compaction.py", "agent-state:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_agent_state.py", - "schema:validate": "npm run agent-state:validate && npm run projection:validate && npm run projected-context:validate && npm run lore-grounding:validate && npm run coordinator:validate && npm run responder-packets:validate && npm run responder-outputs:validate && npm run ink:validate", + "schema:validate": "npm run agent-state:validate && npm run projection:validate && npm run projected-context:validate && npm run lore-grounding:validate && npm run coordinator:validate && npm run responder-packets:validate && npm run responder-outputs:validate && npm run ink:validate && npm run coverage:status", "projection:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_projection_examples.py", "projected-context:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_projected_contexts.py", "lore-grounding:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_lore_grounding.py", @@ -15,7 +15,8 @@ "responder-packets:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_responder_packets.py", "responder-outputs:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_responder_outputs.py", "ink:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_ink_examples.py", - "images:count": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\scripts\\count_visual_images.py" + "images:count": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\scripts\\count_visual_images.py", + "coverage:status": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_corpus_coverage.py" }, "devDependencies": { "inkjs": "^2.4.0" diff --git a/schemas/corpus-coverage.schema.json b/schemas/corpus-coverage.schema.json new file mode 100644 index 0000000..63eb804 --- /dev/null +++ b/schemas/corpus-coverage.schema.json @@ -0,0 +1,127 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ghostlight.corpus_coverage.schema.json", + "title": "Ghostlight Corpus Coverage Ledger", + "type": "object", + "required": [ + "schema_version", + "coverage_id", + "coverage_targets", + "entries" + ], + "properties": { + "schema_version": { + "const": "ghostlight.corpus_coverage.v0" + }, + "coverage_id": { + "type": "string", + "minLength": 1 + }, + "coverage_targets": { + "type": "object", + "required": [ + "major_powers", + "minor_powers", + "movements", + "pre_elysium_flashpoints", + "defunct_absorbed_powers", + "practical_first_broad_target", + "major_power_requirement", + "collision_requirement" + ], + "properties": { + "major_powers": { "type": "integer", "minimum": 0 }, + "minor_powers": { "type": "integer", "minimum": 0 }, + "movements": { "type": "integer", "minimum": 0 }, + "pre_elysium_flashpoints": { "type": "integer", "minimum": 0 }, + "defunct_absorbed_powers": { "type": "integer", "minimum": 0 }, + "practical_first_broad_target": { + "type": "object", + "required": ["minimum", "maximum"], + "properties": { + "minimum": { "type": "integer", "minimum": 0 }, + "maximum": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + "major_power_requirement": { "type": "string", "minLength": 1 }, + "collision_requirement": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "required": [ + "fixture_id", + "status", + "timeline_lane", + "story_type", + "primary_subjects", + "secondary_subjects", + "collision_axes", + "tonal_modes", + "training_targets", + "artifacts", + "review", + "economy_hooks", + "open_lore_gaps", + "notes" + ], + "properties": { + "fixture_id": { "type": "string", "minLength": 1 }, + "status": { + "enum": ["planned", "draft", "accepted_as_draft", "accepted", "needs_revision", "rejected", "superseded"] + }, + "timeline_lane": { + "enum": ["historical_grounded", "transition_grounded", "future_branch"] + }, + "canon_status": { + "enum": ["single_history", "branch_local", "promoted_branch", "template", "draft"] + }, + "story_type": { + "enum": [ + "founding_era_story", + "day_in_the_life_story", + "flashpoint_story", + "movement_story", + "future_branch_story", + "trade_life_story" + ] + }, + "era": { "type": "string" }, + "location": { "type": "string" }, + "flashpoint": { "type": "string" }, + "primary_subjects": { "$ref": "#/$defs/nonemptyStringArray" }, + "secondary_subjects": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "collision_axes": { "$ref": "#/$defs/nonemptyStringArray" }, + "tonal_modes": { "$ref": "#/$defs/nonemptyStringArray" }, + "training_targets": { "$ref": "#/$defs/nonemptyStringArray" }, + "artifacts": { + "type": "object", + "additionalProperties": { "type": "string", "minLength": 1 } + }, + "review": { + "type": "object", + "additionalProperties": { + "enum": ["not_run", "accepted", "accepted_as_draft", "needs_revision", "rejected", "blocked"] + } + }, + "economy_hooks": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "open_lore_gaps": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "notes": { "type": "string" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "$defs": { + "nonemptyStringArray": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + } + } +} diff --git a/state/corpus-coverage.json b/state/corpus-coverage.json new file mode 100644 index 0000000..1bc2a84 --- /dev/null +++ b/state/corpus-coverage.json @@ -0,0 +1,92 @@ +{ + "schema_version": "ghostlight.corpus_coverage.v0", + "coverage_id": "ghostlight-aetheria-corpus-coverage-v0", + "coverage_targets": { + "major_powers": 12, + "minor_powers": 21, + "movements": 23, + "pre_elysium_flashpoints": 24, + "defunct_absorbed_powers": 5, + "practical_first_broad_target": { + "minimum": 100, + "maximum": 150 + }, + "major_power_requirement": "Each major power needs at least one founding-era story and one day-in-the-life story.", + "collision_requirement": "Every accepted coverage fixture should involve multiple cultures, factions, movements, institutions, body assumptions, or infrastructure assumptions colliding." + }, + "entries": [ + { + "fixture_id": "pallas-species-strikes-awakened-labor-v0", + "status": "accepted_as_draft", + "timeline_lane": "historical_grounded", + "canon_status": "single_history", + "story_type": "flashpoint_story", + "era": "Pre-Elysium", + "location": "Pallas, AU-controlled Bloom industrial spoke manifold service ring", + "flashpoint": "Pallas Species Strikes", + "primary_subjects": [ + "Awakened Labor Front", + "Aeronautics Unlimited" + ], + "secondary_subjects": [ + "Baseline League", + "Cephalopod Syndicate", + "BioElevate", + "VitaForge", + "BioDrone Standard" + ], + "collision_axes": [ + "labor", + "species_body", + "liability", + "infrastructure", + "personhood", + "class", + "safety" + ], + "tonal_modes": [ + "industrial realism", + "humane institutional pressure", + "restrained dry wit", + "ordinary work texture" + ], + "training_targets": [ + "coordinator", + "branch_compiler", + "if_artifact_reviewer", + "narrative_reviewer", + "lore_grounding_reviewer", + "spatial_geometric_reviewer", + "visual_scene_continuity_reviewer", + "technology_item_manifest" + ], + "artifacts": { + "lore_digest": "examples/lore-grounding/pallas-species-strikes.awakened-labor.v0.json", + "agent_state": "examples/agent-state.pallas-species-strikes.v0.json", + "coordinator": "examples/coordinator/pallas-species-strikes.branch-and-fold.v0.json", + "ink": "examples/ink/pallas-species-strikes.branch-and-fold.v0.ink", + "training_sidecar": "examples/ink/pallas-species-strikes.branch-and-fold.v0.training.json", + "visual_plan": "examples/visual/pallas-species-strikes.branch-and-fold.v0.visual.json", + "clean_run": "experiments/pallas-species-strikes/pallas-species-strikes.branch-and-fold-clean-run.v0.md", + "visual_review_receipt": "experiments/pallas-species-strikes/pallas-species-strikes.visual-scene-review.v1.json" + }, + "review": { + "schema": "accepted", + "narrative": "accepted_as_draft", + "lore": "accepted_as_draft", + "spatial": "accepted_as_draft", + "visual": "accepted_as_draft", + "if_artifact": "accepted_as_draft" + }, + "economy_hooks": [ + "cephalopod_zero_g_maintenance_harness", + "biodrone_limiter_interface", + "cavity_habitat_seal_assembly", + "strike_safe_life_support_bypass_kit", + "industrial_spoke_manifold_service_ring" + ], + "open_lore_gaps": [], + "notes": "Current accepted-as-draft scaffold. Use as a shape reference for source grounding, branch-and-fold, review, visual planning, and item-hook capture; do not treat as raw responder gold." + } + ] +} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index f56aad8..200bdf2 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -45,3 +45,4 @@ {"ts": "2026-05-04T10:03:43+00:00", "type": "review", "status": "ok", "note": "Lore and spatial reviewer pass exposed BioElevate/VitaForge provenance drift and Pallas visual geometry issues; fixture now uses supplier_liability, VitaForge BioDrone Standard provenance, narrowed BioElevate uplift-adaptation liability, explicit Ilya stair route, triptych final callback, and lockout-rigging solidarity imagery. AetheriaLore gained economy item hooks plus industrial spoke manifold ring backfill."} {"ts": "2026-05-04T11:20:03+00:00", "type": "visual-doctrine", "status": "ok", "note": "Generated environment images are accepted only as concept/material reference for illustrated IF. Multi-angle or branch-consistent replay needs a durable scene-set/blockout/layout source with named cameras and staging slots; imagegen should paint over camera-specific visual input rather than preserve room topology from memory."} {"ts": "2026-05-04T12:31:39+00:00", "type": "maintenance", "status": "ok", "note": "Pruned obsolete prototype fixture receipts, removed stale runner scripts and image-provider experiment tooling from the live workspace, moved old experiment notes out of active evidence, and kept Pallas Species Strikes as the current accepted-as-draft fixture."} +{"ts": "2026-05-04T12:52:20+00:00", "type": "implementation", "status": "ok", "note": "Added corpus coverage ledger seam: state/corpus-coverage.json, schema reference, validator/status command, state:status coverage summary, and initial Pallas Species Strikes coverage row. Coverage status now reports 1/100-150 accepted fixtures and gap signals for future-branch, founding-era, and day-in-life coverage."} diff --git a/state/map.yaml b/state/map.yaml index e227ab7..d6a1b9c 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -68,6 +68,9 @@ live_architecture: branch_compiler: - The branch compiler turns fixture briefs, lore digests, scene/world state, actor-local branch candidates, reviewed consequence packets, fold plans, and optional visual replay requirements into playable Ink plus training sidecars, separate visual plan artifacts when illustrated replay is in scope, and compiler notes. - The branch compiler owns playable structure, click-through segmentation, conditionals, variables, callbacks, fold materialization, visual scene ids, and image prompt handles; it stores visual prompt data in `.visual.json` artifacts rather than bloating training annotations; it does not own hidden character truth, participant appraisal, or durable social mutation. + coverage_ledger: + - `state/corpus-coverage.json` tracks accepted, draft, planned, rejected, and superseded fixtures by faction, movement, flashpoint, timeline lane, tonal mode, collision axis, training target, artifact refs, review status, and economy hooks. + - `npm run coverage:status` validates the ledger and reports coarse corpus gaps before choosing the next fixture. training_targets: - Coordinator/story runtime - Memory and lore retrieval @@ -109,6 +112,7 @@ coverage_targets: current_artifacts: canonical_docs: - notes/documentation-inventory.md + - docs/architecture/corpus-coverage-ledger.md - docs/architecture/prompt-projection-contract.md - docs/architecture/projected-local-context.md - docs/architecture/sequential-agent-runtime.md @@ -123,6 +127,7 @@ current_artifacts: - schemas/agent-state.schema.json - schemas/projection-example.schema.json - schemas/lore-grounding-digest.schema.json + - schemas/corpus-coverage.schema.json - schemas/projected-local-context.schema.json - schemas/coordinator-artifact.schema.json - schemas/responder-packet.schema.json @@ -136,6 +141,7 @@ current_artifacts: - prompts/spatial-geometric-cohesion-reviewer.md - prompts/visual-scene-continuity-reviewer.md live_examples: + - state/corpus-coverage.json - examples/agent-state.call-of-the-void.json - examples/projections/call-of-the-void.scene-broken-taxi-oz.jsonl - examples/projections/call-of-the-void.scene-broken-taxi-oz.pretty.json @@ -161,4 +167,4 @@ data_flow: current_status: summary: Pallas Species Strikes is the current accepted-as-draft branch-and-fold IF fixture. The live workspace has been pruned to remove old prototype fixture receipts, stale runner tools, and model-runner experiment debris after their lessons were distilled into the architecture docs, state, evidence, and git history. The next build step is corpus-scale coverage rather than further archaeology. - next_action: Add the corpus coverage ledger seam or select the next Aetheria fixture from the 100-150 broad coverage target. + next_action: Use the corpus coverage ledger to select the next Aetheria fixture from the 100-150 broad coverage target. diff --git a/tools/ghostlight_state.py b/tools/ghostlight_state.py index 493670c..3f6df8e 100644 --- a/tools/ghostlight_state.py +++ b/tools/ghostlight_state.py @@ -12,10 +12,11 @@ STATE_DIR = ROOT / "state" MAP_PATH = STATE_DIR / "map.yaml" BRANCHES_PATH = STATE_DIR / "branches.json" EVIDENCE_PATH = STATE_DIR / "evidence.jsonl" +COVERAGE_PATH = STATE_DIR / "corpus-coverage.json" def read_text(path: Path) -> str: - return path.read_text(encoding="utf-8") + return path.read_text(encoding="utf-8-sig") def write_text(path: Path, content: str) -> None: @@ -64,6 +65,24 @@ def extract_active_subgoals() -> list[str]: return results +def load_coverage_summary() -> str | None: + if not COVERAGE_PATH.exists(): + return None + data = json.loads(read_text(COVERAGE_PATH)) + entries = data.get("entries", []) + accepted = [ + entry + for entry in entries + if entry.get("status") in {"accepted", "accepted_as_draft"} + ] + target = data.get("coverage_targets", {}).get("practical_first_broad_target", {}) + minimum = target.get("minimum") + maximum = target.get("maximum") + if isinstance(minimum, int) and isinstance(maximum, int): + return f"{len(accepted)} / {minimum}-{maximum} accepted fixtures" + return f"{len(accepted)} accepted fixtures" + + def cmd_status(_: argparse.Namespace) -> int: branches = load_branches().get("branches", []) active = [branch for branch in branches if branch.get("status") == "active"] @@ -75,6 +94,9 @@ def cmd_status(_: argparse.Namespace) -> int: print(f"Summary: {summary}") print(f"Next action: {next_action}") print(f"Active branches: {len(active)} / {len(branches)}") + coverage = load_coverage_summary() + if coverage: + print(f"Coverage: {coverage}") if subgoals: print("Active subgoals:") for item in subgoals: @@ -177,4 +199,3 @@ def main() -> int: if __name__ == "__main__": raise SystemExit(main()) - diff --git a/tools/validate_corpus_coverage.py b/tools/validate_corpus_coverage.py new file mode 100644 index 0000000..877fdb0 --- /dev/null +++ b/tools/validate_corpus_coverage.py @@ -0,0 +1,259 @@ +"""Validate and summarize the Ghostlight corpus coverage ledger.""" + +from __future__ import annotations + +import argparse +import json +from collections import Counter, defaultdict +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_LEDGER = ROOT / "state" / "corpus-coverage.json" +VALID_SCHEMA_VERSION = "ghostlight.corpus_coverage.v0" +VALID_STATUSES = {"planned", "draft", "accepted_as_draft", "accepted", "needs_revision", "rejected", "superseded"} +ACCEPTED_STATUSES = {"accepted", "accepted_as_draft"} +VALID_TIMELINE_LANES = {"historical_grounded", "transition_grounded", "future_branch"} +VALID_CANON_STATUSES = {"single_history", "branch_local", "promoted_branch", "template", "draft"} +VALID_STORY_TYPES = { + "founding_era_story", + "day_in_the_life_story", + "flashpoint_story", + "movement_story", + "future_branch_story", + "trade_life_story", +} +VALID_REVIEW_STATUSES = {"not_run", "accepted", "accepted_as_draft", "needs_revision", "rejected", "blocked"} +REQUIRED_REVIEW_KEYS = {"schema", "narrative", "lore", "spatial", "visual", "if_artifact"} +REQUIRED_ARTIFACTS_FOR_ACCEPTED = {"lore_digest", "agent_state", "coordinator", "ink", "training_sidecar"} + + +class ValidationError(Exception): + pass + + +def load_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def require_string(value: Any, path: str) -> None: + require(isinstance(value, str) and value.strip(), f"{path} must be a non-empty string") + + +def require_string_array(value: Any, path: str, *, nonempty: bool = False) -> None: + require(isinstance(value, list), f"{path} must be an array") + require(not nonempty or bool(value), f"{path} must not be empty") + for index, item in enumerate(value): + require_string(item, f"{path}[{index}]") + + +def require_keys(obj: dict[str, Any], keys: set[str], path: str) -> None: + missing = sorted(keys - set(obj)) + require(not missing, f"{path} missing required keys: {', '.join(missing)}") + + +def validate_targets(targets: Any, path: str) -> None: + require(isinstance(targets, dict), f"{path} must be an object") + required = { + "major_powers", + "minor_powers", + "movements", + "pre_elysium_flashpoints", + "defunct_absorbed_powers", + "practical_first_broad_target", + "major_power_requirement", + "collision_requirement", + } + require_keys(targets, required, path) + for key in ["major_powers", "minor_powers", "movements", "pre_elysium_flashpoints", "defunct_absorbed_powers"]: + require(isinstance(targets[key], int) and targets[key] >= 0, f"{path}.{key} must be a non-negative integer") + broad = targets["practical_first_broad_target"] + require(isinstance(broad, dict), f"{path}.practical_first_broad_target must be an object") + require_keys(broad, {"minimum", "maximum"}, f"{path}.practical_first_broad_target") + require(isinstance(broad["minimum"], int) and broad["minimum"] >= 0, f"{path}.practical_first_broad_target.minimum must be a non-negative integer") + require(isinstance(broad["maximum"], int) and broad["maximum"] >= broad["minimum"], f"{path}.practical_first_broad_target.maximum must be >= minimum") + require_string(targets["major_power_requirement"], f"{path}.major_power_requirement") + require_string(targets["collision_requirement"], f"{path}.collision_requirement") + + +def validate_entry(entry: Any, path: str) -> list[str]: + require(isinstance(entry, dict), f"{path} must be an object") + required = { + "fixture_id", + "status", + "timeline_lane", + "story_type", + "primary_subjects", + "secondary_subjects", + "collision_axes", + "tonal_modes", + "training_targets", + "artifacts", + "review", + "economy_hooks", + "open_lore_gaps", + "notes", + } + require_keys(entry, required, path) + require_string(entry["fixture_id"], f"{path}.fixture_id") + require(entry["status"] in VALID_STATUSES, f"{path}.status is invalid") + require(entry["timeline_lane"] in VALID_TIMELINE_LANES, f"{path}.timeline_lane is invalid") + if "canon_status" in entry: + require(entry["canon_status"] in VALID_CANON_STATUSES, f"{path}.canon_status is invalid") + require(entry["story_type"] in VALID_STORY_TYPES, f"{path}.story_type is invalid") + for optional in ["era", "location", "flashpoint", "notes"]: + if optional in entry: + require(isinstance(entry[optional], str), f"{path}.{optional} must be a string") + require_string_array(entry["primary_subjects"], f"{path}.primary_subjects", nonempty=True) + require_string_array(entry["secondary_subjects"], f"{path}.secondary_subjects") + require_string_array(entry["collision_axes"], f"{path}.collision_axes", nonempty=True) + require_string_array(entry["tonal_modes"], f"{path}.tonal_modes", nonempty=True) + require_string_array(entry["training_targets"], f"{path}.training_targets", nonempty=True) + require_string_array(entry["economy_hooks"], f"{path}.economy_hooks") + require_string_array(entry["open_lore_gaps"], f"{path}.open_lore_gaps") + + artifacts = entry["artifacts"] + require(isinstance(artifacts, dict), f"{path}.artifacts must be an object") + for key, ref in artifacts.items(): + require_string(key, f"{path}.artifacts key") + require_string(ref, f"{path}.artifacts.{key}") + require((ROOT / ref).exists(), f"{path}.artifacts.{key} does not exist: {ref}") + if entry["status"] in ACCEPTED_STATUSES: + missing_artifacts = sorted(REQUIRED_ARTIFACTS_FOR_ACCEPTED - set(artifacts)) + require(not missing_artifacts, f"{path}.artifacts missing accepted fixture refs: {', '.join(missing_artifacts)}") + + review = entry["review"] + require(isinstance(review, dict), f"{path}.review must be an object") + for key, value in review.items(): + require_string(key, f"{path}.review key") + require(value in VALID_REVIEW_STATUSES, f"{path}.review.{key} is invalid") + if entry["status"] in ACCEPTED_STATUSES: + missing_review = sorted(REQUIRED_REVIEW_KEYS - set(review)) + require(not missing_review, f"{path}.review missing accepted fixture review keys: {', '.join(missing_review)}") + + warnings: list[str] = [] + if entry["status"] in ACCEPTED_STATUSES: + weak_reviews = sorted(key for key, value in review.items() if value in {"not_run", "needs_revision", "rejected", "blocked"}) + if weak_reviews: + warnings.append(f"{entry['fixture_id']}: accepted fixture has weak review fields: {', '.join(weak_reviews)}") + if len(set(entry["primary_subjects"] + entry["secondary_subjects"])) < 2: + warnings.append(f"{entry['fixture_id']}: accepted fixture has fewer than two represented subjects") + if entry["story_type"] == "future_branch_story" and entry.get("canon_status") != "branch_local": + warnings.append(f"{entry['fixture_id']}: future_branch_story should usually be canon_status branch_local") + return warnings + + +def validate_ledger(document: Any) -> tuple[list[dict[str, Any]], list[str]]: + require(isinstance(document, dict), "ledger must be an object") + require(document.get("schema_version") == VALID_SCHEMA_VERSION, "ledger schema_version is wrong") + require_string(document.get("coverage_id"), "ledger.coverage_id") + validate_targets(document.get("coverage_targets"), "ledger.coverage_targets") + entries = document.get("entries") + require(isinstance(entries, list), "ledger.entries must be an array") + seen: set[str] = set() + warnings: list[str] = [] + for index, entry in enumerate(entries): + entry_warnings = validate_entry(entry, f"ledger.entries[{index}]") + fixture_id = entry["fixture_id"] + require(fixture_id not in seen, f"ledger.entries[{index}].fixture_id is duplicated: {fixture_id}") + seen.add(fixture_id) + warnings.extend(entry_warnings) + return entries, warnings + + +def summarize(document: dict[str, Any], entries: list[dict[str, Any]], warnings: list[str]) -> str: + targets = document["coverage_targets"] + status_counts = Counter(entry["status"] for entry in entries) + lane_counts = Counter(entry["timeline_lane"] for entry in entries) + story_counts = Counter(entry["story_type"] for entry in entries) + collision_counts = Counter(axis for entry in entries for axis in entry["collision_axes"]) + tonal_counts = Counter(mode for entry in entries for mode in entry["tonal_modes"]) + subject_counts = Counter(subject for entry in entries for subject in entry["primary_subjects"] + entry["secondary_subjects"]) + accepted_entries = [entry for entry in entries if entry["status"] in ACCEPTED_STATUSES] + accepted_count = len(accepted_entries) + target_min = targets["practical_first_broad_target"]["minimum"] + target_max = targets["practical_first_broad_target"]["maximum"] + + lines = ["Ghostlight corpus coverage", ""] + lines.append(f"Ledger: {document['coverage_id']}") + lines.append(f"Entries: {len(entries)} total, {accepted_count} accepted/accepted-as-draft") + lines.append(f"Practical first broad target: {target_min}-{target_max} accepted fixtures") + if target_min: + lines.append(f"Progress to minimum: {accepted_count}/{target_min} ({accepted_count / target_min:.1%})") + lines.append("") + + def append_counter(title: str, counter: Counter[str]) -> None: + lines.append(title) + if counter: + for key, count in counter.most_common(): + lines.append(f"- {key}: {count}") + else: + lines.append("- none") + lines.append("") + + append_counter("Status", status_counts) + append_counter("Timeline lanes", lane_counts) + append_counter("Story types", story_counts) + append_counter("Top collision axes", collision_counts) + append_counter("Tonal modes", tonal_counts) + append_counter("Subjects represented", subject_counts) + + major_story_types: dict[str, set[str]] = defaultdict(set) + for entry in entries: + if entry["story_type"] in {"founding_era_story", "day_in_the_life_story"}: + for subject in entry["primary_subjects"]: + major_story_types[subject].add(entry["story_type"]) + if major_story_types: + lines.append("Major-power story coverage candidates") + for subject, covered in sorted(major_story_types.items()): + missing = sorted({"founding_era_story", "day_in_the_life_story"} - covered) + if missing: + lines.append(f"- {subject}: missing {', '.join(missing)}") + else: + lines.append(f"- {subject}: founding and day-in-life covered") + lines.append("") + + lines.append("Gap signal") + if accepted_count == 0: + lines.append("- No accepted fixtures yet.") + if accepted_count < target_min: + lines.append(f"- Need {target_min - accepted_count} more accepted fixtures to reach the minimum broad target.") + if not any(entry["timeline_lane"] == "future_branch" for entry in entries): + lines.append("- No future_branch fixture coverage yet.") + if not any(entry["story_type"] == "founding_era_story" for entry in entries): + lines.append("- No founding-era story coverage yet.") + if not any(entry["story_type"] == "day_in_the_life_story" for entry in entries): + lines.append("- No day-in-the-life story coverage yet.") + if not any(entry["economy_hooks"] for entry in entries): + lines.append("- No economy hooks captured yet.") + if warnings: + lines.append("") + lines.append("Warnings") + for warning in warnings: + lines.append(f"- {warning}") + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate and summarize the Ghostlight corpus coverage ledger.") + parser.add_argument("path", nargs="?", type=Path, default=DEFAULT_LEDGER, help="Coverage ledger JSON path.") + parser.add_argument("--json", action="store_true", help="Print the validated ledger JSON instead of a text summary.") + args = parser.parse_args() + + document = load_json(args.path) + entries, warnings = validate_ledger(document) + if args.json: + print(json.dumps(document, indent=2)) + else: + print(summarize(document, entries, warnings)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())