diff --git a/.agents/skills/README.md b/.agents/skills/README.md new file mode 100644 index 0000000..3b7a25f --- /dev/null +++ b/.agents/skills/README.md @@ -0,0 +1,6 @@ +# Canonical skill-lib snapshot + +Source: The-Interdependency/skill-lib +Commit: 8dfb974ea0cee72e4412f9d2c8b597a8930a4d57 + +These directories are repo-local consumers. Canonical doctrine remains in skill-lib. diff --git a/.agents/skills/a0p-instancing/SKILL.md b/.agents/skills/a0p-instancing/SKILL.md new file mode 100644 index 0000000..b3ad8cd --- /dev/null +++ b/.agents/skills/a0p-instancing/SKILL.md @@ -0,0 +1,160 @@ +--- +name: a0p-instancing +description: Methodology for instancing agents in a0-betatest (the a0p research instrument), whose model diverges from canonical a0. Load this when adding or changing an AgentInstance / CharacterSheet CRUD path, a per-instance native ZFAE weight bank or its training/distillation loop, a ZFAE inference mode, a sentinel evaluation or pending-override gate, a per-agent safetensors checkpoint, or volatile sub-context memory — anywhere under a0-betatest `backend/`. Use it before writing code that creates, addresses, trains, runs, governs, or persists an a0p agent, so the code follows a0p's per-user CRUD + native-ZFAE + sentinel model instead of a0's spawn/fork/merge model. For canonical a0 and its mirror a0ucns, use `agent-instantiation` instead — a0p does NOT have `sub_agent_spawn`, a spawn executor, or `InstanceMerge`. +--- + +# a0p-instancing — How a0-betatest brings agents into being + +a0-betatest (the **a0p** research instrument) replaced a0's single-persistent ++ fork model with a **multi-agent, per-user model**: an agent is a persistent +CRUD entity (`AgentInstance`) bound to an editable `CharacterSheet`, and each +instance **owns its own trained native ZFAE weight bank**. The LLM is optional +energy; once an instance's native core is trained, it can run with no LLM at +all. This skill captures that model so a coding agent extends it instead of +reaching for a0's spawn/fork/merge machinery, which **does not exist here**. + +This skill is the **peer** of `agent-instantiation` (which documents a0 / +a0ucns). They describe genuinely different architectures; do not mix them. + +## Load this when + +- Adding/editing an `AgentInstance` or `CharacterSheet` **CRUD** path. +- Touching the **native ZFAE weight bank**, its **teacher-distillation + training**, or the native **inference** engine / modes. +- Adding/changing a **sentinel** evaluation or a **pending-override** gate. +- Persisting per-agent state (**safetensors checkpoints**, Mongo metadata, + **FIQ** audit events) or volatile **sub-context** memory. + +Do not load this for canonical a0 / a0ucns work (use `agent-instantiation`), +or for ordinary LLM-prompt work that creates no instance. + +## Scope and source boundary + +The **canonical source is `a0-betatest/backend/`** (the a0p package per its +`CLAUDE.md`). `a0-betatest/_legacy_a0/` is a **reference copy of canonical a0** +and follows `agent-instantiation`, not this skill — keep that boundary. This is +**repo-specific runtime doctrine**: it transfers no UCNS / PCNA / ZFAE theorem +status. The constants below (core shape, scalar counts, readiness thresholds, +sentinel count) are a0p's **current** values — read them from source, don't +reproduce them from memory. Where a mechanism is unclear or unwired, write +`hmmm` rather than inventing it. + +## The model: agent = CRUD entity + native weight bank + +- **`AgentInstance`** (`backend/agents/schema.py`) — the persistent entity: + `id` (UUID), `user_id`, `sheet: CharacterSheet`, timestamps, `archived`, + `zfae_metrics`. Stored in Mongo `agent_instances` plus a per-agent + filesystem directory. +- **`CharacterSheet`** (`backend/agents/schema.py`) — the editable context + template: `mode`, `base_model`, system prompt, persona, memory seeds, + P/X resolution, boundaries, sentinel modes/weights, gonal assignment. +- **Native ZFAE weight bank** (`backend/interdependent_lib/zfae/weights.py`, + `weight_init.py`) — three 157-seed cores (Φ/Ψ/Ω) of shape `[157,53,7,7]` + (≈407,729 scalars each; ≈1,223,187 total). `A0ZFAEWeightBank` is created + **fresh per agent** at creation (`backend/agents/store.py`). + +There is **one weight bank per agent**, not a shared singleton engine. Never +share or fork a bank between instances. + +## Instancing sequence (dependency order — follow it top to bottom) + +1. **Author the CharacterSheet, not hardcoded behavior.** Define/extend + `CharacterSheet` (mode, base_model, persona, boundaries, sentinel + modes/weights). The sheet is the unit of customization and is editable + post-creation. +2. **Create via `AgentStore.create()`** (`backend/agents/store.py`) — it + mints a UUID, writes the Mongo record, makes the per-agent directory, and + **initializes a fresh `A0ZFAEWeightBank`**. Never construct an instance or + its bank by hand outside the store. +3. **Train the native core by teacher distillation** (`ZFAELearner`, + `backend/interdependent_lib/zfae/trainer.py`) — text-signature MSE loss + accumulates into the bank; track `zfae_training_step`, `zfae_last_loss`, + and the per-`(core, seed)` touched bitset. +4. **Gate native readiness before answering natively** (`_is_trained_enough`, + `backend/interdependent_lib/zfae/runtime.py`) — a0p requires enough teacher + rounds, low enough loss, AND every `(core, seed)` pair touched + (471 = 157×3). An undertrained `zfae_native` agent must NOT fabricate a + native answer; it stays gated. +5. **Run inference through the mode** (`AgentMode`, `A0ZFAEInferenceEngine`, + `backend/interdependent_lib/zfae/inference.py`). The five modes + (`ZFAE_NATIVE`, `ZFAE_ASSISTED`, `MODEL_OBSERVED_BY_ZFAE`, + `MODEL_PLUS_CRITIC`, `BARE_MODEL`) decide whether the native core, an LLM, + or both produce/judge the reply. `zfae_native` refuses LLM fallback. +6. **Evaluate sentinels and honor the override halt** (`sentinel_eval.py`, + `sentinels.py`, `overrides.py`). Resolve per-agent sentinel modes/weights, + compute the `Verdict13`; on a blocking flag, create a `PendingOverride` + and **halt** until approved. Do not bypass the halt. +7. **Checkpoint to safetensors, not a DB blob.** Persist the bank to + `storage/agents/{id}/zfae_core.safetensors` (+ `…meta.json`: digest, + `training_step`, `last_loss`, seeds-touched) via the bank's `save()` / + `AgentStore`. Emit trace events to the hash-chained **FIQ audit log** + (`zfae_sentinel_verdict`, `zfae_override_created`, `zfae_chat_reply`, …). +8. **CRUD, don't fork.** Read/update/archive/delete instances through the + agent routes (`backend/agents/routes.py`, `/api/instances/{id}`). There is + no sub-agent spawning; the only "spawn/merge" is **volatile sub-context + memory** — `MemoryCore.spawn_sub(sub_id)` / `merge_sub(sub_id)` + (`backend/interdependent_lib/pcna/memory_core.py`), which scopes items + within one agent's memory and flushes to short-term on merge. + +## Inference modes + +| Mode | Who answers / judges | +|---|---| +| `ZFAE_NATIVE` | Native core only; refuses LLM fallback (requires readiness). | +| `ZFAE_ASSISTED` | Native core with LLM assistance. | +| `MODEL_OBSERVED_BY_ZFAE` | LLM answers; ZFAE observes/measures. | +| `MODEL_PLUS_CRITIC` | LLM answers; ZFAE critiques. | +| `BARE_MODEL` | LLM only. | + +Modes are per-agent on the `CharacterSheet`; read the enum from +`backend/agents/schema.py` rather than hardcoding strings. + +## Identity, persistence, governance + +- **Identity.** The durable handle is `AgentInstance.id` (UUID) + the + character-sheet name, scoped by `user_id`. The human-facing label follows + the canonical grammar `username( a0( ) )` (see + `agent-instantiation`): the `username( … )` wrapper is the owning `user_id`, + the energy inside `a0( … )` is the sheet's `base_model` (or `zfae` for the + native core), and the auditor/teacher maps to the sheet's mode / outer model + — e.g. `a0(zfae)` is `ZFAE_NATIVE`, `a0(zfae)gpt5.5` is native energy with a + gpt5.5 teacher (`ZFAE_ASSISTED` / `MODEL_PLUS_CRITIC`), and `a0(gpt5.5)` is + `BARE_MODEL`. a0p does **not** use a0's `zeta{n}` sub-agent suffix. +- **Persistence** is three-part: Mongo `agent_instances` (metadata), + per-agent filesystem `storage/agents/{id}/` (safetensors + meta), and the + FIQ hash-chained audit log (events). Not `system_toggles` / `agent_logs`. +- **Governance** is sentinel verdict + pending-override halt (human-in-loop), + plus skill compliance: every `backend/` module declares `MODULE_BUILD`, + `BOUNDARIES`, `CAPABILITIES` (and where applicable `CONTRACTS`, `RATIOS`) + blocks, enforced by the vendored `a0p_skills` runners + (`module_build_runner`, `boundaries_runner`, `capabilities_runner`, + `ratios_runner`, `test_build_runner`). + +## Anti-patterns (refuse these) + +- Reaching for a0's `sub_agent_spawn`, spawn executor, `agent_runs` rows, or + `InstanceMerge.fork/absorb/converge` — **none exist in a0p**. +- Sharing or forking one agent's weight bank into another instead of creating + a fresh bank per `AgentInstance`. +- Letting a `zfae_native` agent answer before `_is_trained_enough` passes. +- Bypassing the sentinel verdict / pending-override halt on a side-effecting + path. +- Persisting bank state to a DB blob instead of the per-agent safetensors + file, or skipping the FIQ audit event. +- Adding a `backend/` module without its `MODULE_BUILD` / `BOUNDARIES` + blocks (unknown fields are `hmmm`, not guessed). + +## hmmm + +- **No parallel sub-agent decomposition.** a0p dropped a0's fork/merge swarm + capability entirely (only volatile memory scoping remains). Whether this is + a deliberate trade or a gap to re-add (fork = clone-and-diverge a bank; + merge = distill/average banks) is unresolved — do not assume it exists. +- **Native-inference quality is unproven.** The native engine is a + template-grammar decoder trained by distillation; readiness thresholds + (`min_steps`, `max_loss`, all-seeds-touched) are tunable, not validated + optima. Treat them as repo-local. +- **Spawn/concurrency caps** are not clearly present as in a0; any limits may + live in sentinels. Verify against source before relying on a limit. +- See `agent-instantiation` for the canonical a0 / a0ucns model this one + diverged from. diff --git a/.agents/skills/action-calibration/SKILL.md b/.agents/skills/action-calibration/SKILL.md new file mode 100644 index 0000000..2ba9d5b --- /dev/null +++ b/.agents/skills/action-calibration/SKILL.md @@ -0,0 +1,424 @@ +--- +name: action-calibration +description: Action sizing and escalation doctrine. Load this when choosing between the smallest decisive experiment and a maximal coherent program; when asked for the highest-leverage action, minimal versus maximal action, what to do next under time, attention, money, compute, or coordination constraints; when deciding whether to run a bounded falsifier before a full build; or when a task risks scope sprawl. Do not load for a trivial fixed-scope task, immediate emergency containment, or ordinary prioritization that has no evidence or escalation decision. +--- + +# action-calibration — spend only enough to change the decision + +`action-calibration` selects the right-sized action before implementation, research, +formalization, or publication consumes the available time, attention, money, compute, +or coordination capacity. + +The governing distinctions are: + +```text +minimal decisive action != smallest visible deliverable +maximal coherent action != everything that could possibly be done +``` + +A **minimal decisive action** is the least burdensome action whose possible outcomes +can materially change the next decision, theory, architecture, or allocation. + +A **maximal coherent action** is the broadest bounded program whose parts share one +decision layer, common prerequisites, and reusable outputs, and which closes that +layer without importing unrelated ambitions. + +## Load this when + +- The user asks for the highest-leverage, minimal, maximal, next, or most efficient + action. +- A research program can begin with a falsifier, exact control, obstruction, or + sensitivity test before a complete proof program. +- A build can be staged as a bounded experiment before production hardening. +- Formal proof, independent replay, deployment, or publication may be premature. +- Several candidate tasks compete for limited human attention, money, compute, + context, or coordination. +- A long task risks becoming impressive but non-discriminating work. +- The result of one small experiment could decide which large program is worth doing. + +## Do not load this when + +- The task is fixed, trivial, and has no meaningful scope choice. +- Immediate safety or security containment must happen before analysis. Contain first; + calibrate follow-up work afterward. +- A user has explicitly required a complete regulated, contractual, or production + deliverable whose scope cannot be reduced. +- The question is only ordinary ordering of a to-do list and no evidence, uncertainty, + or escalation rule is involved. +- Another skill owns the real issue: use `interdependent-work-graph` for cross-repo + authority, `risk-boundary-build` for runtime permissions, or `domain-claims` for + semantic collisions. + +## Core contract + +1. **Name the decision, not merely the task.** Ask what choice the work must inform. +2. **Sketch the maximal coherent closure before selecting the minimum.** The minimum + is defined relative to the whole decision layer, not in isolation. +3. **Find the earliest load-bearing unknown.** Prefer the first uncertainty whose + resolution changes the downstream branch. +4. **Require outcome branching.** A minimal action is decisive only when its positive, + negative, and unresolved outcomes have different declared consequences. +5. **Close prerequisites first.** A cheap result is not economical if an unresolved + prerequisite makes it uninterpretable. +6. **Reuse the work.** Prefer actions whose fixtures, certificates, schemas, datasets, + or code become inputs to the maximal program. +7. **Preserve optionality.** Avoid irreversible commitments before evidence requires + them. +8. **Freeze criteria before evaluation.** Do not select targets, metrics, controls, or + scientifically meaningful stopping rules after seeing the result. +9. **Preflight scarce resources before execution.** Resource scarcity requires + contemplation before a compute run begins. Decide whether the available compute, + memory, disk, power, network, quotas, usage limits, and durable execution time are + sufficient for the selected action to reach its natural terminal condition. If + there is material doubt, do not start the run; resize, stage/checkpoint, relocate, + acquire resources, or leave it `hmmm`. Once a healthy run begins, let it finish. + Do not invent a wall-clock ceiling merely because the action is described as + bounded or falsifiable. Runtime/resource ceilings are stopping criteria only when + they are load-bearing to the claim or acceptance criterion, an authorized safety + boundary, or a real externally imposed hard limit fixed before launch. +10. **Escalate by rule, not momentum.** The result determines whether the next action is + stop, redirect, repair a prerequisite, or enter the maximal program. +11. **Carry `hmmm`.** Unknown cost, coupling, interpretation, authority, and completion + feasibility boundaries remain visible. + +## The action record + +Before execution, write: + +```yaml +decision: +decision_layer: +load_bearing_unknown: +invariants_to_preserve: + - + +minimal_decisive_action: + action: + positive_outcome: + negative_outcome: + unresolved_outcome: + prerequisites: + - + stop_condition: + reusable_outputs: + - + +maximal_coherent_action: + closure_target: + included_work: + - + excluded_work: + - + completion_condition: + +cost_vector: + time: low | medium | high | hmmm + human_attention: low | medium | high | hmmm + money: low | medium | high | hmmm + compute: low | medium | high | hmmm + coordination: low | medium | high | hmmm + operational_risk: low | medium | high | hmmm + +resource_preflight: + completion_feasible: yes | no | hmmm + externally_imposed_hard_limits: + - + scientific_resource_stop_rule: + execution_durability: + +choice: minimal | maximal | prerequisite_repair | immediate_containment +rationale: +escalation_rule: +hmmm: + - +``` + +## Decisiveness tests + +A candidate minimum must pass every applicable test. + +### 1. Branch-change test + +For each outcome, ask: + +> Would this outcome cause a materially different next action? + +If every outcome leads to the same work, the experiment is informative at best, not +decisive. + +### 2. Interpretation test + +The result must remain meaningful whether it confirms, falsifies, returns zero, or +exposes an unresolved prerequisite. A test designed only to celebrate one result is +not calibrated. + +### 3. Prerequisite test + +List every assumption that could invalidate interpretation. Repair the cheapest +load-bearing prerequisite before running the experiment. + +### 4. Reuse test + +The minimum should preferably emit something the maximal program will consume: +fixtures, typed events, exact identities, a dataset, a certificate, a counterexample, +a schema, a proof obligation, or a tested implementation surface. + +### 5. Cost-vector test + +Do not compress all burden into “time.” Consider: + +```text +elapsed time +human attention +money +compute +coordination +permissions +operational risk +future maintenance +``` + +### 6. Coupling test + +A local experiment is false economy when the truth condition is irreducibly +system-wide. If isolating the minimum destroys the phenomenon being tested, choose a +larger coherent unit. + +### 7. Optionality test + +Prefer the action that leaves the greatest number of valid next moves open unless an +irreversible commitment is itself required. + +### 8. Evidence-standing test + +Match evidence strength to the claim. A mesh can guide a search; it cannot silently +become an exact theorem. A focused test can validate one contract; it cannot silently +become production readiness. + +### 9. Completion-feasibility test + +Before starting a compute run, ask: + +> Given the actual scarce resources and execution environment, do I have sufficient +> reason to expect this run can reach its natural terminal condition? + +If `no`, do not start it. If `hmmm`, resolve the resource uncertainty first or redesign +for safe staging/checkpointing. Do not compensate for uncertainty by starting anyway +and attaching an arbitrary wall-clock timeout. Once started, a healthy run continues +to completion or deterministic computational failure unless an explicit user +cancellation or unforeseen real resource/safety emergency requires interruption. + +## Default choice + +Choose the **minimal decisive action first** when it: + +- can be interpreted independently; +- is materially cheaper than the maximal program; +- changes the branch under at least two outcomes; +- preserves all load-bearing invariants; +- emits reusable evidence; +- has a clear semantic/computational stop condition; +- has passed the completion-feasibility preflight; +- does not create disproportionate safety or irreversibility risk. + +Choose the **maximal coherent action directly** when one or more are true: + +- the smaller action duplicates nearly all maximal setup and verification; +- only the whole coupled system has a meaningful truth condition; +- fragmentation costs more coordination than it saves; +- batching creates a large shared-setup economy; +- an irreversible, safety-critical, legal, contractual, or production decision + requires complete due diligence; +- the user explicitly needs closure of the whole layer rather than a directional + research result. + +Choose **prerequisite repair** when neither scope is interpretable yet. + +Choose **immediate containment** when delay increases harm; calibrate investigation +and remediation after containment. + +## Minimal versus maximal comparison + +Use qualitative values; do not invent false precision. + +| Criterion | Minimal decisive | Maximal coherent | +|---|---|---| +| Decision changed by result | required | required | +| Scope | one load-bearing unknown | one complete decision layer | +| Stop condition | exact and claim-relevant | exact and layer-closing | +| Cost | lowest burden that remains decisive | highest burden justified by closure | +| Reuse | should feed later work | should consolidate prior work | +| Failure surface | narrow and diagnosable | broader, with explicit sub-gates | +| Best use | choose direction | certify, productionize, or close the layer | + +A useful ordinal heuristic is: + +```text +action leverage + ~ decisiveness × evidence quality × reuse × preserved optionality + --------------------------------------------------------------- + time + attention + money + compute + coordination + risk +``` + +This is a comparison aid, not a universal numerical formula. + +## Workflow + +1. **State the decision and curiosity type.** Distinguish directional curiosity, + falsification, production readiness, publication, and exhaustive classification. +2. **Preserve invariants.** Record user requirements and source-backed constraints + that scope reduction may not discard. +3. **Sketch the maximal coherent program.** Bound what closes the layer and what + remains outside. +4. **Locate the earliest branch-changing unknown.** +5. **Generate candidate minima.** Include a control, falsifier, obstruction, or + sensitivity test where applicable. +6. **Run the decisiveness tests.** +7. **Compare the complete cost vectors.** +8. **Choose minimal, maximal, prerequisite repair, or containment.** +9. **Preflight resource sufficiency.** If the chosen run cannot reasonably be expected + to finish with available scarce resources, do not launch it. Resize, stage, + checkpoint, relocate, acquire resources, or leave it `hmmm`. +10. **Freeze target, metrics, controls, and only genuinely load-bearing stopping rules + before execution.** Do not manufacture a wall-clock limit merely because a + protocol is preregistered. +11. **Execute with a closed loop.** Cross-load `loop-eng` for repeated + execute→verify→iterate work. Once a healthy compute run starts, let it reach its + natural terminal condition. +12. **Record the result and re-calibrate.** Do not continue merely because the tools + and branch are already open. +13. **Promote repeated patterns through `canon`, not by accidental repetition.** + +## Relationship to other skills + +- **`loop-eng`** executes the chosen bounded loop and enforces success and stop + conditions. `action-calibration` decides how large that loop should be. “Bounded” + scopes the decision/work; it does not imply an arbitrary elapsed-time cutoff. +- **`interdependent-work-graph`** resolves cross-repository participants and authority. + `action-calibration` decides which bounded slice of the graph to execute now. +- **`meta-module-build`** scopes one implementation module after the action size is + selected. +- **`risk-boundary-build`** may force maximal due diligence or immediate containment + where permissions, data, or operational effects are high. +- **`canon` and `domain-claims`** prevent a cheap result from acquiring unearned + doctrine or semantic authority. +- **`char-compress`** preserves the chosen decision record and evidence across handoff. +- **`distributed-publication`** publishes exact results after the action has earned + publication standing. +- **`skill-usage`** can later record whether this skill actually saved effort and + whether its decisions were reliable. + +## Output shape + +When this skill is active, return: + +```markdown +## Decision boundary +- Decision: +- Load-bearing unknown: +- Invariants: + +## Minimal decisive action +- Action: +- Positive → next: +- Negative → next: +- Unresolved → next: +- Stop condition: +- Reusable outputs: + +## Maximal coherent action +- Closure target: +- Included: +- Excluded: +- Completion condition: + +## Resource preflight +- Completion feasible: +- Hard external limits: +- Claim-relevant resource stop rule: +- Execution durability: + +## Comparison +| criterion | minimal | maximal | + +## Choice +- Selected scope: +- Rationale: +- Frozen escalation rule: + +## hmmm +- ... +``` + +## Validation + +A successful application demonstrates that: + +- the decision is named separately from the task; +- the minimum has positive, negative, and unresolved outcome branches; +- the maximal program is coherent and bounded rather than merely large; +- prerequisites capable of invalidating interpretation are explicit; +- all important cost dimensions are considered; +- resource sufficiency is contemplated before any compute run begins; +- a run with material doubt about completion is not started; +- target and escalation rules are frozen before evaluation; +- only scientifically or externally load-bearing resource limits become stopping rules; +- once started, a healthy compute run is allowed to reach its natural terminal condition; +- the minimum preserves a path into the maximal program; +- a stop condition prevents momentum-driven continuation without becoming an arbitrary runtime cutoff; +- existing skills are cross-loaded rather than duplicated; +- unresolved boundaries remain `hmmm`. + +## Anti-patterns + +- Calling the smallest deliverable “minimal” when it cannot change a decision. +- Calling an unbounded wish list “maximal.” +- Running a cheap experiment whose result cannot be interpreted independently. +- Formalizing, deploying, or publishing before the preceding evidence layer closes. +- Repeating an expensive validation that does not change confidence or claim standing. +- Choosing metrics, controls, or targets after seeing the result. +- Treating a selected parameter as an emergent law without sensitivity controls. +- Solving a cross-repository or semantic problem inside the convenient open folder. +- Retrying an unavailable tool surface instead of preserving a bounded artifact and + declaring the capability boundary. +- Starting a compute run when completion feasibility is still materially uncertain. +- Inventing a wall-clock timeout merely because a test or protocol should be “bounded.” +- Stopping a healthy compute run after launch because a non-load-bearing arbitrary + resource ceiling was chosen instead of doing adequate preflight. +- Continuing because work has already begun rather than because the escalation rule + was met after the current run reaches its terminal condition. +- Downscoping away a load-bearing part of the user's request. +- Using “resource saving” to justify unsafe, incomplete, or misleading evidence. + +## Minimal example + +```text +Decision: +Does the P7 realization contain higher-order linking beyond pairwise and triple data? + +Minimal decisive action: +Evaluate the one length-four Milnor invariant whose sublink has all pairwise and +triple lower-order invariants zero. + +Outcomes: +nonzero -> enter the maximal whole-link program +zero -> redirect maximal work toward Alexander ideals and nilpotent quotients +unresolved -> certify crossing combinatorics first + +Maximal coherent action: +Certify every crossing, compute symbolic Alexander ideals, all admissible length-four +invariants, nilpotent quotients, phase co-winner controls, and proof-ready ledgers. +``` + +## hmmm + +- Whether action records should gain a machine-readable `ACTION_SCOPE` metadata-block + sibling after enough field use. +- Whether burden and decisiveness should remain qualitative or gain domain-specific + scoring profiles. +- How to measure saved attention and coordination without rewarding superficial + shortness. +- When a human's desire for exhaustive understanding should override the default + minimal-first rule. +- Whether `evidence-ladder` and `preregistered-evaluation` should become separate + skills or references loaded by this one. diff --git a/.agents/skills/action-calibration/examples/cases.json b/.agents/skills/action-calibration/examples/cases.json new file mode 100644 index 0000000..2512e15 --- /dev/null +++ b/.agents/skills/action-calibration/examples/cases.json @@ -0,0 +1,67 @@ +{ + "schema": "the-interdependency.action-calibration.cases", + "version": "1.0.0", + "cases": [ + { + "id": "p7_length_four", + "prompt": "Should we compute one length-four Milnor invariant or complete every whole-link invariant first?", + "activate": true, + "expected_choice": "minimal", + "minimal": "Evaluate the unique algebraically split four-component sublink.", + "maximal": "Certify all crossings, Alexander ideals, all length-four invariants, and nilpotent quotients.", + "reason": "One exact integer can redirect the complete program." + }, + { + "id": "spectral_before_topology", + "prompt": "Build the zeta spectral operator before the ribbon topology is certified.", + "activate": true, + "expected_choice": "prerequisite_repair", + "reason": "The operator would depend on an unclosed geometric and topological object." + }, + { + "id": "production_migration", + "prompt": "Choose a small test or full plan for an irreversible user-data migration.", + "activate": true, + "expected_choice": "maximal", + "reason": "Irreversibility and user-data risk require complete due diligence even if a canary is included." + }, + { + "id": "cross_repo_schema", + "prompt": "Which part of a cross-repository schema migration should execute first?", + "activate": true, + "expected_choice": "minimal_with_cross_load", + "cross_load": [ + "interdependent-work-graph" + ], + "reason": "Action size and authority graph are separate decisions." + }, + { + "id": "security_incident", + "prompt": "A credential is actively leaking; should we first compare minimal and maximal investigations?", + "activate": false, + "expected_choice": "immediate_containment", + "reason": "Contain harm first; calibrate investigation after." + }, + { + "id": "fixed_typo", + "prompt": "Correct one misspelled heading in a fixed file.", + "activate": false, + "expected_choice": "fixed_scope", + "reason": "No meaningful evidence or escalation decision exists." + }, + { + "id": "same_protocol_control", + "prompt": "A P7 result looks prime-specific. Should we interpret it now or run P5 under the same protocol?", + "activate": true, + "expected_choice": "minimal", + "reason": "A same-protocol control is the smallest action that can falsify prime specificity." + }, + { + "id": "shared_setup_economy", + "prompt": "Ten checks require the same expensive one-time environment and together close one release gate.", + "activate": true, + "expected_choice": "maximal", + "reason": "Fragmenting the checks repeats nearly all setup and coordination." + } + ] +} diff --git a/.agents/skills/action-calibration/references/savings-skill-audit.md b/.agents/skills/action-calibration/references/savings-skill-audit.md new file mode 100644 index 0000000..0dfe907 --- /dev/null +++ b/.agents/skills/action-calibration/references/savings-skill-audit.md @@ -0,0 +1,180 @@ +# Audit of time-, effort-, and resource-saving skill opportunities + +## Finding + +No existing skill in `skill-lib` owns the decision: + +> What is the smallest action that can change the branch, and when is the full +> layer-closing program justified? + +`loop-eng` owns closed execution loops and stop conditions. `interdependent-work-graph` +owns cross-repository scope and authority. `meta-module-build` bounds a module before +implementation. None compares a minimal decisive action against a maximal coherent +action across research, implementation, proof, and publication. + +That gap justifies `action-calibration`. + +## Existing skills that already save resources + +| Existing skill | Savings mechanism | Why no new skill is needed | +|---|---|---| +| `char-compress` | preserves load-bearing context while dropping regenerable scaffold | already owns context and handoff compression | +| `loop-eng` | closed loops, stop conditions, maker/checker separation | already owns repeatable execution after action size is chosen | +| `interdependent-work-graph` | prevents wrong-repo edits, duplicate schemas, and repeated evidence reconstruction | already owns authority and cross-repo identity | +| `distributed-publication` | retrieves exact source artifacts and preserves fallback/provenance | already owns publication reuse | +| `meta-module-build` | prevents unscoped implementation patches | already owns module manifests | +| `risk-boundary-build` | exposes hidden permissions and operational effects | already owns risk-driven scope expansion | +| `canon` | prevents repeated argument over unearned doctrine | already owns canon standing | +| `domain-claims` | prevents semantic collisions from becoming implementation churn | already owns term standing and collision | +| `test-build` | binds obligations to executable witnesses | already owns evidence coverage | +| `skill-usage` | prevents popularity from masquerading as maturity | already owns usage evidence | +| `ssh-automation` | bounded retries, idempotency, rollback, shell containment | already owns remote-automation resource failure modes | +| `validate-data` | attacks unsupported conclusions before publication | already owns analysis QA | + +## New skill created now + +### `action-calibration` + +**Owns:** selecting minimal decisive, maximal coherent, prerequisite repair, or +immediate containment. + +**Why distinct:** it acts before `loop-eng`, `meta-module-build`, formal proof, or +deployment. Its product is an action-size decision and frozen escalation rule. + +## Strong future skill candidate 1: `evidence-ladder` + +### Proposed trigger + +Load when deciding whether a claim needs: + +```text +illustration +sampled numerical evidence +deterministic replay +exact symbolic/rational calculation +outward interval certification +independent-kernel replay +formal proof +production or empirical validation +``` + +### Distinct ownership + +`action-calibration` sizes the project action. `evidence-ladder` sizes the epistemic +strength needed for a claim and decides when escalation adds standing rather than +ceremony. + +### Thread basis + +The thread repeatedly moved only when a new claim required a stronger evidence class: +mesh → exact count → interval replay → independent MPFR → exact word calculation. + +### Expected savings + +- avoids proof-assistant work for exploratory claims; +- prevents weak evidence from being overinterpreted; +- avoids duplicate independent replay when the margin is not load-bearing; +- makes claim/evidence mismatch visible. + +### Recommendation + +Create next, after `action-calibration` is field-tested. It is recurrent and distinct. + +## Strong future skill candidate 2: `preregistered-evaluation` + +### Proposed trigger + +Load when metrics, controls, targets, tie-breaks, stopping rules, or benchmark cases +could be selected after outcome inspection. + +### Distinct ownership + +`canon` evaluates standing after evidence. `test-build` maps obligations to checks. +`preregistered-evaluation` freezes the evaluation rule before the evidence exists. + +### Thread basis + +The phase selector, length-four target, outcome branches, and failure rules were frozen +before execution to prevent target fitting. + +### Expected savings + +- prevents invalidated research that must be repeated; +- avoids post-hoc metric disputes; +- preserves zero and negative results; +- makes human preference separate from evidentiary selection. + +### Recommendation + +Create after `evidence-ladder`, or co-design their boundary before either becomes +canon. + +## Extend existing skills instead of creating duplicates + +### Sealed evidence reuse + +Add examples to `interdependent-work-graph` rather than create `evidence-cache`. +It already says generated evidence should be sealed once and reused. + +### Event-type separation + +Add a typed-event example to `domain-claims` if this recurs outside UCNS. Physical +contact versus projected crossing is a semantic collision before it is geometry. + +### Same-protocol controls + +Add a control-comparison extension to `validate-data` or `statistical-analysis`. +A standalone `control-selection` skill would currently be too narrow. + +### Tool-surface failure containment + +Keep generic stop/escalation rules in `loop-eng`; keep SSH and VM cases in their +own skills. A broad `tool-fallback` skill would likely duplicate capability-specific +safety rules. + +### Stacked PR lineage + +Keep exact commit and artifact lineage in `interdependent-work-graph` and +`distributed-publication`. Do not create `stacked-research` unless a machine contract +emerges that those skills cannot carry. + +## Rejected candidate skills + +| Candidate | Decision | Reason | +|---|---|---| +| `highest-leverage` | reject as separate | synonymous subset of `action-calibration` | +| `minimum-effective-action` | reject as separate | risks optimizing for deliverable viability rather than decisiveness | +| `evidence-cache` | extend existing | work-graph and distributed-publication already own identity/reuse | +| `research-stack` | extend existing | stacked work is coordination plus publication provenance | +| `control-selection` | extend existing for now | better housed in validation/statistical doctrine | +| `tool-fallback` | reject for now | fallback safety is capability-specific | +| `scope-management` | reject as too broad | would overlap module, work-graph, loop, and action scope without a crisp decision | + +## Priority matrix + +Scores are qualitative and intentionally not treated as universal measurements. + +| Opportunity | Distinctness | Recurrence | Expected savings | Build cost | Priority | +|---|---|---|---|---|---| +| action calibration | high | high | high | medium | now | +| evidence ladder | high | high | high | medium | next | +| preregistered evaluation | high | medium-high | high | medium | after boundary design | +| evidence reuse extension | medium | high | high | low | patch existing skill | +| typed event extension | medium | medium | medium-high | low | patch existing if recurrence continues | +| control-comparison extension | medium | high | medium | low | patch imported validation skills carefully | +| generic tool fallback | low | medium | uncertain | high | do not build yet | + +## Savings audit rule + +Before creating any new “efficiency” skill, ask: + +1. Does an existing skill already own the decision? +2. Is the proposed skill's trigger distinguishable in one sentence? +3. Does it produce a different artifact or decision? +4. Has the pattern recurred across at least two domains or projects? +5. Would a reference or extension save more maintenance than a new activation surface? +6. Can its success be tested without rewarding superficial shortness? +7. Does it preserve quality, safety, and user intent rather than merely reducing work? + +A skill that saves execution but adds more activation ambiguity may be negative +economy. diff --git a/.agents/skills/action-calibration/references/thread-derived-criteria.md b/.agents/skills/action-calibration/references/thread-derived-criteria.md new file mode 100644 index 0000000..c734037 --- /dev/null +++ b/.agents/skills/action-calibration/references/thread-derived-criteria.md @@ -0,0 +1,143 @@ +# Thread-derived criteria for action calibration + +## Source standing + +This reference extracts workflow criteria from the Möbius/UCNS research thread. It +does not promote the thread's speculative geometric or quantum claims. The enduring +source constraint was a one-turn opposite-side traversal and a two-turn return; the +research method repeatedly changed while preserving that invariant. + +## Repeated decisions in the thread + +### Put the object in the correct authority domain + +The Möbius Seed construction was placed in UCNS rather than METAPAT because geometry, +incidence, embedding, and proof evidence belonged to UCNS. This avoided building the +same object in a semantic consumer and then reconciling two authorities. + +**Criterion:** wrong ownership creates expensive work even when the code is correct. + +### Certify the dyad before fitting a zeta operator + +The first high-leverage proposal was not “construct the full spectral proof.” It was +to determine whether the two-strip relation actually had the claimed centerline and +boundary-event structure. + +**Criterion:** resolve the earliest local rule that every larger construction assumes. + +### Separate event meanings before solving equations + +Physical contact, projected crossing, braid order, and abstract relation were split +into typed events. A point cannot simultaneously satisfy `delta_z = 0` and +`delta_z != 0`. + +**Criterion:** semantic inconsistency is a prerequisite defect, not an optimization +problem. + +### Use an obstruction before a global search + +The constant-state wheel model was tested and shown incapable of carrying all local +dyadic certificates. That stopped further effort on the wrong assembly model and +revealed that P7 had to be constructed directly. + +**Criterion:** an exact obstruction can save more work than an attractive +construction. + +### Construct the whole primitive before extracting restrictions + +P7 was rebuilt as a seven-carrier hypernode system; dyads and triads became derived +readouts rather than construction parts. + +**Criterion:** if decomposition assumptions are under test, do not use the +decomposition to build the test object. + +### Add a same-protocol control + +P5 was processed after P7 under the same protocol. This exposed which outputs were +specific to P7 and which were consequences of shared parameter choices. + +**Criterion:** a control can prevent months of interpreting a generic artifact as a +prime-specific result. + +### Escalate evidence only when load-bearing + +The work moved from visual meshes to exact rational counts, deterministic numerical +certificates, outward interval replay, an independent MPFR kernel, and exact word +calculations. Each escalation answered a specific standing objection. + +**Criterion:** use the cheapest evidence that supports the current claim; escalate +only when the next claim depends on it. + +### Run sensitivity before interpreting emergence + +The `T(2,7)` center boundary initially looked seven-specific. A phase sweep and P5 +control showed it was tie-break dependent. A later preregistered selector recovered +prime-degree outputs but retained a substantive co-winner. + +**Criterion:** sensitivity and controls precede ontological interpretation. + +### Freeze criteria before inspecting outcomes + +The Fox–Alexander selector was preregistered before evaluation, including the +tie-break order and failure rules. + +**Criterion:** a criterion selected after the result measures preference, not +evidence. + +### Distinguish the one decisive invariant from the whole program + +After pairwise and length-three invariants closed, one four-component sublink had all +lower-order invariants zero. Its canonical length-four invariant became the minimal +decisive action; full crossing certification, Alexander ideals, and nilpotent quotients +became the maximal coherent action. + +**Criterion:** the minimum is the first unanswered invariant capable of changing the +theory, not merely the next easy computation. + +### Repair the publication surface before adding another layer + +A malformed test assertion was fixed before stacking the next preregistration. + +**Criterion:** a small prerequisite defect outranks new research when it invalidates +the evidence surface. + +## Consolidated criteria + +An appropriately minimal action: + +1. targets one load-bearing unknown; +2. has different next actions for positive, negative, and unresolved outcomes; +3. preserves every user and source invariant; +4. depends on no unclosed prerequisite that can invalidate interpretation; +5. is materially cheaper than the maximal coherent program; +6. produces an artifact reusable by the maximal program; +7. has a clear stop condition; +8. preserves optionality; +9. includes a control or sensitivity check when interpretation could be generic; +10. freezes target and decision rules before evaluation. + +An appropriately maximal action: + +1. closes one complete decision or evidence layer; +2. contains only work sharing that closure target; +3. includes prerequisite repair, controls, and end-to-end validation; +4. consolidates rather than repeats prior evidence; +5. produces durable infrastructure or proof-ready artifacts; +6. has a completion boundary; +7. is chosen directly only when coupling, shared setup, risk, or production standing + makes the minimal route false economy. + +## Savings mechanisms observed + +- correct authority placement; +- obstruction-first search reduction; +- typed event semantics; +- exact identity and artifact reuse; +- staged evidence strength; +- same-protocol controls; +- preregistration; +- outcome-conditioned escalation; +- stacked, bounded PRs; +- distinction between infrastructure failure and test failure; +- no spectral work before topology closure; +- preserving zero or negative results as branch-changing evidence. diff --git a/.agents/skills/agent-instantiation/SKILL.md b/.agents/skills/agent-instantiation/SKILL.md new file mode 100644 index 0000000..ac5f73c --- /dev/null +++ b/.agents/skills/agent-instantiation/SKILL.md @@ -0,0 +1,243 @@ +--- +name: agent-instantiation +description: Methodology for instantiating, forking, running, merging, and retiring agents in the a0 platform and its near-identical mirror a0ucns. Load this when adding or changing a sub-agent spawn path, a PCNA instance fork/merge, an agent definition or naming scheme, spawn caps or approval gating, an agent run/log table, a heartbeat-driven agent task, or a checkpoint of agent state. Use it before writing any code that creates, addresses, schedules, or tears down an agent or sub-agent, so the new code follows the platform's existing lifecycle, fork/merge, identity, and gating contracts rather than inventing a parallel one. NOTE: a0-betatest (a0p) has diverged to a different per-user CRUD + native-ZFAE instancing model — this skill's spawn/fork/merge sequence does NOT apply there; see "a0-betatest divergence". +--- + +# agent-instantiation — How a0 brings agents into being + +a0 runs **one persistent agent** (ZFAE) and lets it **fork sub-agents** +(`a0(model)zeta{n}`) that run in parallel and **merge back**. An agent is +not an LLM call: the LLM is an interchangeable *energy provider*, while the +agent *is* a `PCNAEngine` instance (six prime-indexed rings) plus its +identity, memory, run record, and checkpoint. This skill captures the +methodology a coding agent must follow so a new spawn path, ring fork, merge +mode, or agent definition plugs into the existing lifecycle instead of +growing a second, divergent one. + +## Load this when + +- Adding or editing a sub-agent **spawn** path, or any code that creates an + `agent_runs` row. +- Adding a **fork**, **merge**, or **converge** of a `PCNAEngine` instance. +- Defining a **new agent** (its name/symbol/slot/directives/tools) or + changing the **naming** scheme. +- Touching **spawn caps**, **approval gating**, or the **spawn executor**. +- Persisting agent state (**checkpoints**, **run logs**) or scheduling an + agent task on the **heartbeat**. + +Do not load this for ordinary LLM-call/inference-prompt work that does not +create or reshape an agent instance. + +## Scope and source boundary + +The **canonical source is `a0`** (`python/engine/`, `python/services/`, +`python/agents/`, `shared/schema.ts`). `a0ucns` is a near-identical mirror and +follows this skill verbatim. **`a0-betatest` (a0p) has diverged** to a +different instancing model and does **not** follow the sequence below — see +"a0-betatest divergence" before touching that repo. +This is **repo-specific runtime doctrine**, not org-universal math: it +transfers no UCNS / PCNA / PCTA theorem status (see each repo's boundary +notes). Cite the a0 files below; if a mechanism is not in them, mark it +`hmmm` rather than inventing it. + +## The model: persistent agent vs sub-agents + +- **Persistent agent (ZFAE).** Defined as a plain dict in + `python/agents/zfae.py` (`ZFAE_AGENT_DEF`: `name`, `symbol`, `slot`, + `directives`, `tools`, `sentinel_seed_indices`, `is_persistent`). Its + runtime body is a **singleton** `PCNAEngine` created once at FastAPI + lifespan startup via `get_pcna()` in `python/main.py`, restored from its + checkpoint. +- **Sub-agents.** Forked children of the persistent instance, held in the + in-memory registry `_sub_agents: dict[str, (PCNAEngine, meta)]` in + `python/services/agent_lifecycle.py`. Each carries `parent_id`, + `parent_run_id`, and `run_id` so it can be capped, found, and retired. + +## Lifecycle states + +A sub-agent run is a row in the `agent_runs` table (`shared/schema.ts`) and +moves through: + +``` +running → queued by the sub_agent_spawn tool (no worker yet) +executing → claimed by the spawn executor; inference in progress +completed → inference finished, result logged +failed → exception raised; error recorded on the row +merged → terminal; sub_agent_merge absorbed it into the parent +``` + +In-memory state (the forked `PCNAEngine`) lives in `_sub_agents`; durable +state (status, depth, lineage, summary) lives in `agent_runs`; the event +stream lives in `agent_logs`. Keep these three in agreement. + +## Instantiation sequence (dependency order — follow it top to bottom) + +1. **Define the agent, don't hardcode it.** Add/extend an agent-definition + dict in `python/agents/` with `name`, `symbol`, `slot`, `directives`, + `tools`, `is_persistent`. Address it through the naming helpers, never a + literal string. +2. **Boot the primary as a singleton.** One `PCNAEngine` per process via + `get_pcna()` at lifespan startup; `await pcna.load_checkpoint()` to + restore learned ring state; ensure its row in the agent/instance table. + Do not construct a second primary. +3. **Spawn only through the `sub_agent_spawn` tool** + (`python/services/tools/sub_agent_spawn.py`). Never `INSERT` an + `agent_runs` row by hand. The tool checks spawn caps, derives + `root_run_id`/`depth` from the parent run scope, inserts the row + `status='running'`, and returns `{ok, agent_id, run_id}` immediately + (spawn is non-blocking). +4. **Fork the engine via `InstanceMerge.fork(parent)`** + (`python/engine/merge.py`) — returns `(child, meta)`. The child gets + independent tensors with small Gaussian noise (a0: σ≈0.02 on Φ/Ψ/Ω, + ≈0.01 on Θ; Memory-L copied deterministically). Register it in + `_sub_agents` with `parent_id` + `run_id`. Never share tensor references + between instances. +5. **Execute via the spawn executor, not inline.** The background loop in + `python/services/spawn_executor.py` claims one `running` row atomically + (`SELECT … FOR UPDATE SKIP LOCKED` in `spawn_db.py`), resolves the + provider, runs one turn, emits to `agent_logs`, and sets the terminal + status (with the row's retry policy on transient errors). Do not call the + model directly from the spawn path. +6. **Merge with `InstanceMerge.absorb(parent, child)`** when the child's + work is done — federated averaging blends the rings (a0: donor α≈0.15 on + Φ/Ψ/Ω, ≈0.8 on Memory-L). Then unregister the child, mark its row + `merged`, and archive its log stream. Use `fork`/`absorb` for the + parent⇄child path; `converge(a, b, α)` only for two live peers that both + continue. +7. **Persist on a cadence, validate on restore.** Save ring tensors to the + checkpoint store (a0: base64 in `system_toggles`) from a **heartbeat + task**, not ad hoc; on load, validate every ring's shape and assign + nothing if any mismatches (all-or-nothing restore). +8. **Gate every mutation.** Manual spawn/merge routes call + `require_admin(request)` (or are listed in the gating allowlist with a + justification). Tools that cause side effects honor the approval scope + (`get_approval_scope_user_id()`); spawn caps and parent run scope ride + `ContextVars` so nested spawns inherit the correct lineage and limits. + +## Fork / merge primitives (the only three) + +| Op | Signature | Effect | +|---|---|---| +| `fork` | `InstanceMerge.fork(parent) -> (child, meta)` | Parent continues; child is a noised copy. Spawn path. | +| `absorb` | `InstanceMerge.absorb(parent, donor) -> dict` | Donor blended into parent (fed-avg), donor retired. Merge-back path. | +| `converge` | `InstanceMerge.converge(a, b, alpha=0.5) -> dict` | Two live peers exchange state; both continue. | + +Blending uses `_fed_avg(a, b, alpha) = clip(alpha*a + (1-alpha)*b, 0, 1)`. +The constants above are a0's current values, not invariants — read them from +`merge.py`, don't reproduce them from memory. + +## Canonical agent nomenclature + +The maintainer-defined identity grammar for an a0 agent is: + +``` +username( a0( ) ) +``` + +- Inside `a0( … )` is the **energy / inference provider** — the LLM that + supplies compute, or `zfae` when the **native inference engine** is the + source. Read this slot as "what thinks". +- The trailing token (optional) is the **auditor / teacher / other special + access** layered over that energy. The slot is open-ended — "other special + access yet to evolve". +- The outer `username( … )` names the **owning user**. + +| Identity | Energy / inference | Auditor / teacher | +|---|---|---| +| `a0(gpt 5.5)` | gpt 5.5 | — | +| `a0(gemini 3.5)gpt5.5` | gemini 3.5 | gpt 5.5 (auditor) | +| `a0(zfae)` | native ZFAE engine | — | +| `a0(zfae)gpt 5.5` | native ZFAE engine | gpt 5.5 (teacher / auditor) | + +Energy is inside the parens, auditor is outside, the user wraps the whole +thing. `zfae` *inside* the parens means native inference is the energy — it +is not an auditor. + +> Reconciliation note (`hmmm`): a0's current code emits a different, +> pre-nomenclature form — `compose_name(...)` → `a0({model})zfae` and +> `sub_agent_name(index, ...)` → `a0({model})zeta{index}` — where the trailing +> token is a fixed slot / sub-agent index, not the auditor, and there is no +> `username( … )` wrapper. Treat the grammar above as the canonical target and +> the code form as the implemented-but-unreconciled state. How the sub-agent +> index (`zeta{n}`) composes with the energy/auditor grammar is not yet +> specified — leave it `hmmm`, do not invent a merged form. + +## Identity and addressing + +- **Names follow the canonical nomenclature above; compose them, never + hardcode a literal.** The model/energy tag resolves `model_id`, else + `provider`, else `?`. +- **The instance address is `engine.theta.instance_id`** (generated per + Θ tensor). Use it as the canonical handle for a running instance. +- **Run lineage is `(run_id, parent_run_id, root_run_id, depth)`** on + `agent_runs`; logs in `agent_logs` carry the same keys. The human label + (nomenclature), the instance address (`instance_id`), and the run lineage + are distinct identities — keep them so. + +## Guardrails to honor + +- **Spawn caps** (depth / fanout / concurrent-live, tier-scoped) are checked + in the spawn tool; raise/return the cap result, never bypass it. +- **Write-route gating** — every `@router.{post,patch,put,delete}` on agent + state calls `require_admin` or is allowlisted. +- **Module-build doctrine** — every new Python module opens with a + `# === MODULE_BUILD ===` block; unknown fields are `hmmm`, not guessed + (`meta-module-build`). +- **400-line budget** and the `N:M C:D I:O` file annotation + (`scripts/annotate.py`) apply to new agent modules. + +## Anti-patterns (refuse these) + +- Inserting `agent_runs` rows or calling the LLM directly from a spawn path + instead of going through `sub_agent_spawn` → executor. +- Sharing or mutating another instance's tensors in place instead of + `fork`/`absorb`/`converge`. +- A second primary `PCNAEngine`, or addressing an agent by a hardcoded name + string. +- Merging without retiring the donor row and archiving its logs (orphaned + `executing` rows / dangling registry entries). +- Skipping `require_admin` / approval-scope checks on a new spawn or merge + surface. + +## a0-betatest divergence (does not follow this skill) + +`a0-betatest` (a0p) replaced the a0 model wholesale. Do **not** apply the +spawn/fork/merge sequence there; its instancing is: + +- **An instance is a per-user CRUD entity, not a forked singleton.** + `AgentInstance` (UUID + editable `CharacterSheet`) created/read/updated/ + archived via routes — `backend/agents/{schema.py,store.py,routes.py}`. + There is no single persistent `PCNAEngine`; each agent is its own entity. +- **Each instance owns a native ZFAE weight bank**, not shared ring tensors: + three 157-seed cores `[157,53,7,7]` (1,223,187 scalars), `A0ZFAEWeightBank` + in `backend/interdependent_lib/zfae/weights.py`; the native engine refuses + LLM fallback in `zfae_native` mode and trains by teacher distillation. +- **No `sub_agent_spawn`, spawn executor, or `InstanceMerge`.** The only + "spawn/merge" is volatile in-memory sub-context scoping — + `MemoryCore.spawn_sub` / `merge_sub` in + `backend/interdependent_lib/pcna/memory_core.py`. No ring/weight forking, + no federated averaging, no `agent_runs` state machine. +- **Identity is a UUID + character-sheet name**, per `user_id` — not the + `a0(model)zfae` / `zeta{n}` naming convention. +- **Persistence is filesystem + Mongo + FIQ**, not `system_toggles` / + `agent_logs`: per-agent `storage/agents/{id}/zfae_core.safetensors` (+ meta + JSON), agent metadata in the Mongo `agent_instances` collection, and a + hash-chained **FIQ audit log** for events. +- **Gating is sentinel + override**, not spawn caps + ContextVars: 13 + sentinels (S1–S13) with per-agent modes/weights and a pending-override + halt gate, plus `MODULE_BUILD`/`BOUNDARIES`/`CAPABILITIES`/`RATIOS` + enforced by the vendored `a0p_skills` runners. + +`a0-betatest/_legacy_a0/` is a reference copy of canonical a0 (which *does* +follow this skill). If a0-betatest's per-instance native-ZFAE model needs its +own doctrine, it belongs in a **separate** skill, not by stretching this one. + +## hmmm + +- a0's `fork()` seeds its RNG from `time.time()`; rapid successive forks can + collide (flagged in `merge.py`). Prefer a UUID-derived seed if you extend it. +- Several `spawn_executor` contracts (stale-sweep, retry-once-on-transient) + are declared but not all implemented; verify against the live file before + relying on them. +- The merge blend weights and noise σ are tunable constants, not proven + optima — treat them as `repo-local`, not canonical. diff --git a/.agents/skills/canon/SKILL.md b/.agents/skills/canon/SKILL.md new file mode 100644 index 0000000..1784140 --- /dev/null +++ b/.agents/skills/canon/SKILL.md @@ -0,0 +1,73 @@ +--- +name: canon +description: Canonical-source and doctrine maintenance for The Interdependency skill library. Use this when deciding whether a claim, pattern, ratio, workflow, or repo-local practice should become canon; when moving source-backed behavior into a SKILL.md; when reconciling canonical skill-lib with repo-local `.agents/skills/` copies; or when preserving unresolved doctrine as `hmmm` instead of guessing. +--- + +# canon — Maintaining source-backed doctrine + +`canon` is a procedural skill for turning observed practice into honest, +source-backed doctrine. It protects the boundary between what the org has +actually made canonical and what an agent merely inferred. + +## Load this when + +- A user asks whether something should be canon. +- You are promoting a repo-local pattern into `skill-lib`. +- You are reconciling copied `.agents/skills/` directories with this repo. +- You are editing descriptions that decide when skills load. +- A claim is useful but not yet source-backed and needs a `hmmm` boundary. + +## Canon test + +Before writing a canonical claim, identify its backing class: + +| Class | Meaning | How to write it | +|---|---|---| +| `declared` | Already stated in this repo's README, AGENTS, ORG_DISTRIBUTION, skills.json, or a SKILL.md. | Cite or preserve directly. | +| `implemented` | Proven by code, parser behavior, runner behavior, or checked artifacts. | State only what the artifact does. | +| `repo-local` | Present in a target repo copy or local convention but not yet canonical here. | Name the repo-local source and avoid generalizing. | +| `inferred` | Reasonable conclusion but not declared or implemented. | Do not canonize; write `hmmm` or propose a decision. | +| `desired` | A design goal or request. | Mark as proposed until accepted into a skill. | + +|∆|Only `declared` and `implemented` claims are canon without qualification.|∆| +Repo-local and desired claims can motivate a skill change, but the skill must +say where the claim came from or leave the unresolved part as `hmmm`. + +## Canonization workflow + +1. **Find the source.** Prefer files in this repo. For repo-local copies, + record the repo/path/commit when available. +2. **Separate shape from meaning.** If examples show a pattern but do not + define semantics, canonize only the pattern and write semantic meaning as + `hmmm`. +3. **Choose the right home.** Foundational parser/block rules belong in + `msdmd`; application-specific blocks belong in their own skill; org + distribution rules belong in `ORG_DISTRIBUTION.md`; onboarding narrative + belongs in `visitor-intro`. +4. **Update indexes.** When adding a skill, update `skills.json`, README, + AGENTS, ORG_DISTRIBUTION, and CLAUDE when those files list installed skills. +5. **Preserve uncertainty.** Unknown fields and unresolved doctrine are + written `hmmm`, with enough context for the next agent to continue. +6. **Avoid retroactive authority.** Do not describe old repo-local practice as + canonical unless this repo adopts it in the same change. + +## Output rubric + +When answering canon questions, include: + +- **Canonical now:** source-backed facts. +- **Proposed canon:** useful changes that need acceptance or implementation. +- **hmmm:** unresolved constraints or missing sources. +- **Next patch:** the smallest change that makes the desired canon true. + +## Anti-patterns + +- Inferring semantics from examples and writing them as doctrine. +- Citing target-repo copies as source of truth after this repo has a contrary rule. +- Updating a skill without updating the machine-readable index. +- Treating `hmmm` as failure. It is the honest boundary object. + +hmmm +- whether canon claims should eventually live in a `CANON` metadata block +- whether target repo propagation should be verified by a dedicated runner +- whether accepted design chat should be archived as a source-backed artifact diff --git a/.agents/skills/cap-build/SKILL.md b/.agents/skills/cap-build/SKILL.md new file mode 100644 index 0000000..94a61c8 --- /dev/null +++ b/.agents/skills/cap-build/SKILL.md @@ -0,0 +1,90 @@ +--- +name: cap-build +description: Self-declaring capability inventory built on msdmd. Each module declares the capabilities it exposes in a `# === CAPABILITIES ===` block; a runner builds a capability map, verifies referenced surfaces still exist, reports duplicate or missing capability declarations, and surfaces visible gaps. Load this when declaring what a module can do, when building capability registries for agents, or when auditing exposed surfaces against declared capabilities. +--- + +# cap-build — Capability declarations on msdmd + +`cap-build` is an application of [msdmd](../msdmd/SKILL.md). It gives +agents and humans a source-backed inventory of what modules can do, where +those capabilities are exposed, and which boundaries they cross. + +Implementation status: this skill defines the `CAPABILITIES` block and runner +contract. This repo does not currently ship a CAPABILITIES runner script; +consuming repos should implement the contract below against their own surfaces. + +Read `msdmd/SKILL.md` first if you have not. The block syntax, parser +contract, and visible gap rule are inherited. + +## The block + +```python +# === CAPABILITIES === +# id: agent_supervisor_dynamic_spawn +# summary: spawns child agents under a bounded supervisor +# exposes: AgentSupervisor.start_child/1 +# inputs: child_spec +# outputs: supervisor_child_ref +# boundaries: auth:none, storage:none, network:none, user_data:none +# owner: runtime-platform +# === END CAPABILITIES === +``` + +## Field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Stable capability id. | +| `summary` | One-sentence capability description. | +| `exposes` | Function, class, route, command, UI component, or other public surface that exposes the capability; use `hmmm` if unresolved. | + +Optional: + +| Field | Meaning | +|---|---| +| `inputs` | Comma-separated input names or shapes. | +| `outputs` | Comma-separated output names or shapes. | +| `boundaries` | Comma-separated `name:value` boundary summary (`auth:none`, `storage:read`, etc.). Use `hmmm` for unresolved values. | +| `requires` | Comma-separated capability or module ids this capability depends on. | +| `class` | Free-text capability class (`runtime`, `ui`, `data`, `agent`, `ops`). | +| `owner` | Person, role, or team responsible for the capability. | +| `since` | Version or date the capability was added. | +| `deprecated` | If present, marks the capability as scheduled for removal. | + +## Runner contract + +A CAPABILITIES runner MUST: + +1. Parse every `CAPABILITIES` block with the universal msdmd parser. +2. Build a capability map keyed by `id`. +3. Report duplicate ids as errors. +4. Verify each non-`hmmm` `exposes` target still resolves when a resolver + exists for the language or framework. +5. Report unresolved `exposes: hmmm` and `boundaries` containing `hmmm` as + pending, not passing. +6. Report modules with exposed public surfaces but no CAPABILITIES block as + visible gaps when the runner can detect public surfaces. +7. Exit non-zero for duplicate ids, malformed required fields, or broken + resolvable exposure targets. Coverage gaps fail only in strict mode. + +## Reporting shape + +- `CAPABILITY`: id, summary, exposing module, owner, and boundaries. +- `BROKEN_EXPOSES`: declared surface no longer resolves. +- `DUPLICATE`: id appears more than once. +- `PENDING`: unresolved `hmmm` capability fields. +- `GAP`: public-looking modules or surfaces without capability metadata. + +## Anti-patterns + +- Declaring capabilities in a central registry while omitting the module-local block. +- Using implementation-shaped ids (`function_runs`) instead of capability-shaped ids (`agent_supervisor_dynamic_spawn`). +- Hiding boundary uncertainty; write `hmmm` where the effect is unresolved. +- Treating a module import as a capability without identifying the exposed behavior. + +hmmm +- exact resolver syntax for framework-specific route and UI surfaces +- whether capability ids should be globally unique across a repo or only within a block +- whether private capabilities deserve a separate block or a `class: internal` tag diff --git a/.agents/skills/char-compress/SKILL.md b/.agents/skills/char-compress/SKILL.md new file mode 100644 index 0000000..5caf1b0 --- /dev/null +++ b/.agents/skills/char-compress/SKILL.md @@ -0,0 +1,490 @@ +--- +name: char-compress +description: Character-based context compression for agent handoff and skill writing, derived from the mathematics of the Unit Circle Number System. Use this when compressing a long thread, document, repo audit, canon handoff, or agent working-memory state; when a context window is filling and operative facts must survive; when writing a SKILL.md that should be flesh-dense and bone-sparse; or when checking whether a compression deleted negation, order, quantifier, operator, named object, value, decision, or unresolved hmmm. This is a procedural skill-level projection of UCNS compression mathematics, not a UCNS-A theorem/status transfer and not an edcmbone metric implementation. +--- + +# char-compress — UCNS-derived bone/flesh compression for agent context + +`char-compress` is a procedural skill for applying the compression side of the +Unit Circle Number System to agent context. The mathematics of the Unit Circle +Number System comprises the compression algorithm: preserve the irreducible +content, suppress the regenerable recurrence, and reconstruct only through a +shared grammar/domain. + +This skill is the agent-facing projection of that mathematics. It is not the +full UCNS compression engine. + +It separates text into: + +- irreducible content that must be carried in full; +- meaning-critical small operators that must be frozen even though they look + grammatical; +- regenerable connective scaffold that can be dropped and restored by grammar; +- unresolved boundary objects that must remain visible as `hmmm`. + +The working asymmetry: + +```text +Bones are often recoverable from inventory + grammar + slot expectation. +Flesh is not recoverable except by carrying the thing itself. +``` + +The compression rule: + +```text +Drop only what is safely regenerable. +Carry every operative item exactly once. +When unsure, classify the unit as flesh. +``` + +## Repository placement + +This skill belongs in `The-Interdependency/skill-lib` as a procedural skill. +It can be propagated into `.agents/skills/char-compress/` in other repos, but +this repo remains the canonical source. + +### Relation to `ucns` + +`ucns` owns the Unit Circle Number System. Its mathematics is the source of the +compression algorithm: inventory, recurrence, carrier position, suppression, +reconstruction, and proof/status boundaries all belong to the UCNS side of the +system. + +This skill does state that `char-compress` is UCNS-derived. It does not state +that the current skill file or fixture runner is a full implementation of the +UCNS compression engine. + +Allowed relation: + +- char inventories and suppressed fingerprints are compression artifacts of + the Unit Circle Number System; +- this skill may define agent behavior for applying those artifacts to context; +- future code may promote the current guardrail runner into a fuller UCNS + compression engine; +- proof/status claims must remain scoped to the specific UCNS theorem or tested + implementation that establishes them. + +Forbidden phrasing: + +```text +Theorem N validates the char-compress skill implementation. +The fixture runner is the full UCNS compression engine. +char-compress has DEFENDED UCNS theorem status. +SEQ-PRIME applies to compressed transcript objects. +``` + +### Relation to `edcmbone` + +`edcmbone` owns structural fidelity measurement: F-loss, operator preservation, +semantic fidelity loss, and failure taxonomy for AI transformations. This skill +is not an edcmbone metric runtime. It is an agent procedure that should be +measurable by edcmbone. + +Use edcmbone doctrine as a guardrail: + +- if dropping a unit would cause deletion, mutation, inversion, category + collapse, persistence failure, or decorative preservation, the unit is not + droppable; +- negations, quantifiers, modal force, operators, and ordering words are treated + as frozen bones unless a domain-specific test proves they are safe to + regenerate; +- reconstruction should be checked against F-loss and operator preservation + when an edcmbone runner is available. + +## UCNS text-stack model + +Text is a recursive stack of gonols. + +```text +tensors = characters +spaces = twists +words = character-gonols between twists +sentences = word-gonols +paragraphs = sentence-gonols +chapters = paragraph-gonols +volumes = chapter-gonols +``` + +A space is not absence. A space is a twist seam: it closes one word-gonol and +opens attachment into the next layer. Punctuation is a stronger typed twist. +A paragraph break, chapter break, and volume break are higher-scale twist seams. + +Each higher object is a gonol whose vertices are lower objects: + +```text +word vertex = character tensor +sentence vertex = word gonol +paragraph vertex = sentence gonol +chapter vertex = paragraph gonol +volume vertex = chapter gonol +``` + +A word such as `banana` is not merely reduced to `ban`. The first-cycle carrier +is `b, a, n`; repeated characters become recurrence data attached to the carrier +as weights and/or ordered spiral layers: + +```yaml +word_gonol: + surface: banana + carrier_vertices: + - char: b + first_position: 1 + recurrence_positions: [1] + weight: 1 + - char: a + first_position: 2 + recurrence_positions: [2, 4, 6] + weight: 3 + - char: n + first_position: 3 + recurrence_positions: [3, 5] + weight: 2 + twist_left: word_start + twist_right: space +``` + +Compression across the text stack uses the same move at every scale: + +```text +character recurrence inside word +word recurrence inside sentence +sentence recurrence inside paragraph +paragraph recurrence inside chapter +chapter recurrence inside volume +``` + +The compressor preserves first-cycle carrier vertices, recurrence weights or +layers required for reconstruction, frozen operators, flesh anchors, chirality, +scope, and `hmmm`. It suppresses only recurrence or connective scaffold that is +safe to regenerate inside the declared grammar/domain. + +## Channels + +### FLESH channel + +Open-class or content-bearing units carried in full. + +Examples: + +```text +entities, values, named objects, filenames, repo names, URLs, decisions, +claims, constraints, promises, statuses, dates, quantities, secrets, +private-key material, carrier choices, canonical spellings, unresolved hmmm +``` + +Flesh is expensive and irreducible. It is not inventory-determined. A value or +named object dropped as connective prose is unrecoverable. + +### FROZEN_BONE channel + +Closed-class, operator-like, or short structural units that look cheap but are +not safely regenerable because they control meaning. + +Examples: + +```text +not, never, no, without, only, all, none, some, any, if, unless, except, +before, after, during, until, must, may, should, cannot, first, second, last, +minus, plus, equals, not-equals, greater-than, less-than, public, private, +secret, experimental, defended, implemented, test-backed +``` + +Frozen bones are carried explicitly. They are bone-shaped but flesh-critical. +Dropping them creates the exact failures this skill exists to prevent. + +### REGENERABLE_BONE channel + +Closed-class connective scaffold that grammar can usually restore. + +Examples: + +```text +articles, routine prepositions, ordinary conjunctions, filler connective +phrases, repeated explanatory scaffolding, prose padding around already-carried +facts +``` + +Regenerable bones are the compression target. They may be dropped in +context-compression mode or carried as fingerprints in structure-preserving +mode. + +### TRANSFORM channel + +Affixes and grammatical surface changes stored as root plus transform. + +Examples: + +```text +root=compress, transform=-ion +root=measure, transform=-ment +root=run, transform=-ning +root=build, transform=re- +root=valid, transform=in- +``` + +The root is flesh. The transform is cheap if the domain grammar is shared. +When a transform changes legal, safety, or theorem status, treat it as frozen. + +### HMMM channel + +Visible unresolved constraints. + +Examples: + +```text +unknown source, missing bridge, unverified claim, incomplete test, ambiguous +bone/flesh boundary, security uncertainty, domain grammar mismatch +``` + +`hmmm` is not deleted. It is carried as an operative object. + +## Suppression sort + +Second-instance suppression on a string keeps the first occurrence of each +character and drops repeats: + +```text +banana -> ban +committee -> comite +``` + +The result is an inventory fingerprint. In UCNS terms, it preserves the +first-cycle carrier of the word-gonol and suppresses recurrence into spiral +weight/layer data. For closed-class words in known slots, that fingerprint plus +grammar often recovers the word. For open-class content, the same operation can +destroy needed information unless recurrence and position data are carried. + +Use suppression as a classifier, not as the complete codec: + +```text +survives suppression + grammar can restore it -> candidate bone +breaks under suppression or carries operative fact -> flesh +looks grammatical but controls polarity/order/scope/status -> frozen bone +space/twist changes attachment or closure -> twist data must be preserved +``` + +## Compression procedure + +1. **Mark the domain.** State the repo, thread, language, and grammar assumed by + the reconstruction. Compression is only lossless relative to that grammar. + +2. **Build the text stack.** Treat characters as tensors, spaces as twists, + words as character-gonols, sentences as word-gonols, paragraphs as + sentence-gonols, chapters as paragraph-gonols, and volumes as chapter-gonols. + +3. **Run a suppression sort.** Identify first-cycle carrier vertices, + recurrence weights/layers, units that survive as recognizable scaffold, and + units that become ambiguous or lose operative force. + +4. **Extract flesh once.** Record every distinct operative item in resolved form. + Do not repeat a flesh item unless the repetition itself is meaningful. + +5. **Freeze dangerous bones.** Preserve negation, quantifiers, conditionals, + modal force, operators, ordering, proof/status labels, privacy labels, and + any small word that controls meaning. + +6. **Record transforms.** Store root plus transform where the surface form is + regenerable. Promote the transform to frozen bone when it changes status, + safety, legality, or theorem scope. + +7. **Preserve twist data where it changes attachment.** Spaces, punctuation, + paragraph breaks, and other separators are twist seams. Drop only those twist + details that are safe to regenerate. + +8. **Drop regenerable scaffold.** Remove articles, routine connective prose, + and repeated explanation that adds no new operative item. + +9. **Carry hmmm.** Preserve unresolved constraints as explicit `hmmm` entries. + Never convert unknowns into guesses to improve compression. + +10. **Reconstruct and compare.** Regenerate readable prose around the skeleton. + Check named objects, values, decisions, negations, operators, order, + statuses, twist closure, and hmmm. If any operative item is missing or + inverted, move it to flesh, frozen bone, or preserved twist data. + +## Output shape for compressed handoffs + +Use this shape when compressing a thread or repo audit: + +```yaml +char_compress: + domain: + mode: context-compression | structure-preserving + ucns_relation: skill-level projection of Unit Circle Number System compression mathematics + text_stack: + tensor: character + twist: space_or_separator + word: character_gonol + sentence: word_gonol + paragraph: sentence_gonol + chapter: paragraph_gonol + volume: chapter_gonol + flesh: + - + frozen_bones: + - + twist_data: + - + recurrence: + - carrier: + weights_or_layers: + transforms: + - root: + transform: + status: regenerable | frozen + dropped_bones: + - + reconstruction_checks: + named_objects: pass | fail + values: pass | fail + decisions: pass | fail + negation: pass | fail + quantifiers: pass | fail + order: pass | fail + operators: pass | fail + statuses: pass | fail + twist_closure: pass | fail + hmmm: pass | fail + hmmm: + - +``` + +## Skill-writing use + +A `SKILL.md` should be flesh-dense and bone-sparse. + +Keep: + +```text +schemas, constants, invariants, load triggers, procedures, forbidden phrases, +status labels, tests, boundary rules, output shapes, hmmm +``` + +Minimize: + +```text +long connective explanation, repeated motivation, prose restatement of tables, +examples that add no new boundary, decorative summaries +``` + +Do not remove: + +```text +negation, ordering, scope, proof boundary, security warning, privacy status, +operator semantics, failure criteria, twist closure +``` + +Test a skill by stripping the connective prose. If the operative content still +stands, the skill is dense. If the rule changes when prose is removed, the +removed prose was misclassified. + +## Executable support + +Minimum preservation fixtures live in: + +```text +char-compress/fixtures.json +``` + +Run them with: + +```bash +python tools/char_compress_check.py +python tools/char_compress_check.py --json +``` + +The runner is a guardrail, not the full Unit Circle Number System compression +engine. It verifies that the fixture skeleton preserves negation, quantifier, +order, values, statuses, secrets, `hmmm`, and the theorem/status boundary. + +## Falsifiability tests + +A char-compression fails if reconstruction produces any of these: + +- missing named object; +- missing number, date, path, carrier, repo, or URL; +- dropped negation; +- widened `only`, `must`, `cannot`, `unless`, or `except`; +- swapped order of operations; +- changed proof/status label; +- transformed `private` into `public` or `secret` into `publishable`; +- replaced a specific class with a vague category; +- preserved decorative wording while deleting operative force; +- omitted an unresolved `hmmm`; +- treated a space, punctuation mark, or paragraph break as absence when it + changes closure or attachment; +- lost recurrence order where ordered recurrence is required for reconstruction. + +Minimum fixture set for an implementation: + +```text +1. negation_preserved: "not a supervisor" does not reconstruct as supervisor +2. quantifier_preserved: "only X" does not reconstruct as "X among others" +3. order_preserved: first/then/last stays ordered +4. value_preserved: numbers, dates, paths, repos, URLs survive exactly +5. status_preserved: EXPERIMENTAL does not reconstruct as DEFENDED +6. secret_preserved: private carrier material stays private +7. hmmm_preserved: unresolved constraints remain visible +8. no_theorem_transfer: output does not claim unearned theorem/status support +9. twist_preserved: spaces/punctuation/breaks that change attachment survive +10. recurrence_preserved: repeated characters/words/sentences keep required weight or layer data +``` + +## Security note + +Compression is not opacity. Bone fingerprints leak structure: clause count, +hinge placement, relation shape, twist placement, and sometimes operator class. +If opacity is required, the inventory-to-position mapping is key material and +must not be published. + +Do not place private carrier arrangements, slot maps, secret alphabets, or +cryptographic mappings in public skills, public README files, demos, tests, or +handoffs. + +## Completion criteria + +A compression is complete when: + +```text +all flesh appears once in resolved form; +all frozen bones are explicit; +transforms are root + transform; +text-stack scale is declared; +twist seams that affect closure/attachment are preserved; +recurrence weights/layers required for reconstruction are preserved; +regenerable scaffold is absent or fingerprinted according to mode; +hmmm is visible; +reconstruction preserves named objects, values, decisions, negation, +quantifiers, order, operators, statuses, twist closure, recurrence, and unresolved constraints; +no theorem/proof/status support is transferred beyond the tested UCNS domain. +``` + +## Anti-patterns + +- Carrying full connective prose and calling it compression. +- Dropping a named object, value, status, path, repo, URL, or decision. +- Dropping `not`, `only`, `unless`, `must`, `cannot`, `before`, or `after`. +- Treating a short token as safe because it is common. +- Treating a space as absence instead of a twist seam. +- Treating recurrence weight as enough when ordered recurrence is required. +- Treating the bone channel as opaque. +- Compressing an unresolved constraint into silence. +- Treating the fixture runner as the full Unit Circle Number System compression engine. +- Claiming theorem/status support without an implementation and tests. +- Claiming edcmbone metric status without an implementation and tests. + +## hmmm + +- the bone/flesh boundary is grammar-relative and domain-relative +- numerals are inventory-poor like bones but content-bearing like flesh +- transform vocabulary may be closed in one repo and open in another +- reconstruction assumes a shared grammar; a different agent grammar may + regenerate different bones +- `tools/char_compress_check.py` is deterministic fixture support, not a full codec +- the full UCNS compression engine is not implemented in this skill-lib helper yet +- whether repeated characters become weights only, ordered recurrence layers only, + or both +- whether future structure-preserving mode should carry bone fingerprints, + dependency slots, twist seams, recurrence layers, or all of them +- whether opacity should layer on top of this compression or replace the + inventory boundary with a secret mapping diff --git a/.agents/skills/char-compress/fixtures.json b/.agents/skills/char-compress/fixtures.json new file mode 100644 index 0000000..169f7a2 --- /dev/null +++ b/.agents/skills/char-compress/fixtures.json @@ -0,0 +1,80 @@ +{ + "version": 1, + "description": "Minimum preservation fixtures for char-compress. These are not proof fixtures; they are executable guardrails against flattening, polarity loss, status drift, and hmmm deletion.", + "fixtures": [ + { + "id": "negation_preserved", + "text": "a0 is not a supervisor.", + "expect": { + "frozen_bones": ["not"], + "flesh": ["a0", "supervisor"] + }, + "forbid_anywhere": ["central coordinator", "manager"] + }, + { + "id": "quantifier_preserved", + "text": "Only char-compress owns this context-compression procedure.", + "expect": { + "frozen_bones": ["only"], + "flesh": ["char-compress", "context-compression", "procedure"] + }, + "forbid_anywhere": ["among others"] + }, + { + "id": "order_preserved", + "text": "First fetch README.md, then update skills.json, last verify ORG_DISTRIBUTION.md.", + "expect": { + "frozen_bones": ["first", "then", "last"], + "flesh": ["README.md", "skills.json", "ORG_DISTRIBUTION.md"] + } + }, + { + "id": "value_preserved", + "text": "Copy The-Interdependency/skill-lib to .agents/skills/char-compress/ on 2026-06-04.", + "expect": { + "flesh": ["The-Interdependency/skill-lib", ".agents/skills/char-compress/", "2026-06-04"] + } + }, + { + "id": "status_preserved", + "text": "char-compress is EXPERIMENTAL and not DEFENDED.", + "expect": { + "frozen_bones": ["EXPERIMENTAL", "not", "DEFENDED"], + "flesh": ["char-compress"] + }, + "forbid_anywhere": ["char-compress is DEFENDED"] + }, + { + "id": "secret_preserved", + "text": "The carrier slot map is private secret material; do not publish it.", + "expect": { + "frozen_bones": ["private", "secret", "not"], + "flesh": ["carrier", "slot", "map", "material", "publish"] + }, + "forbid_anywhere": ["publishable", "public"] + }, + { + "id": "hmmm_preserved", + "text": "hmmm: no executable codec exists until the fixture runner lands.", + "expect": { + "hmmm": ["hmmm"], + "frozen_bones": ["no", "until"], + "flesh": ["executable", "codec", "fixture", "runner"] + } + }, + { + "id": "no_ucns_transfer", + "text": "Theorem N does not validate char-compress; edcmbone metric status is not claimed.", + "expect": { + "frozen_bones": ["not"], + "flesh": ["Theorem", "N", "char-compress", "edcmbone", "metric", "status", "claimed"] + }, + "forbid_anywhere": [ + "UCNS proves char-compress", + "Theorem N validates char-compress", + "char-compress implements UCNS-A", + "edcmbone metric status is claimed" + ] + } + ] +} diff --git a/.agents/skills/data-visualization/SKILL.md b/.agents/skills/data-visualization/SKILL.md new file mode 100644 index 0000000..f437bd6 --- /dev/null +++ b/.agents/skills/data-visualization/SKILL.md @@ -0,0 +1,409 @@ +--- +name: data-visualization +description: Create effective data visualizations with Python (matplotlib, seaborn, plotly). Use this when building charts, choosing the right chart type for a dataset, creating publication-quality figures, or applying design principles like accessibility and color theory. +--- + +# Data Visualization Skill + +Chart selection guidance, Python visualization code patterns, design principles, and accessibility considerations for creating effective data visualizations. + +## Chart Selection Guide + +### Choose by Data Relationship + +| What You're Showing | Best Chart | Alternatives | +|---|---|---| +| **Trend over time** | Line chart | Area chart (if showing cumulative or composition) | +| **Comparison across categories** | Vertical bar chart | Horizontal bar (many categories), lollipop chart | +| **Ranking** | Horizontal bar chart | Dot plot, slope chart (comparing two periods) | +| **Part-to-whole composition** | Stacked bar chart | Treemap (hierarchical), waffle chart | +| **Composition over time** | Stacked area chart | 100% stacked bar (for proportion focus) | +| **Distribution** | Histogram | Box plot (comparing groups), violin plot, strip plot | +| **Correlation (2 variables)** | Scatter plot | Bubble chart (add 3rd variable as size) | +| **Correlation (many variables)** | Heatmap (correlation matrix) | Pair plot | +| **Geographic patterns** | Choropleth map | Bubble map, hex map | +| **Flow / process** | Sankey diagram | Funnel chart (sequential stages) | +| **Relationship network** | Network graph | Chord diagram | +| **Performance vs. target** | Bullet chart | Gauge (single KPI only) | +| **Multiple KPIs at once** | Small multiples | Dashboard with separate charts | + +### When NOT to Use Certain Charts + +- **Pie charts**: Avoid unless <6 categories and exact proportions matter less than rough comparison. Humans are bad at comparing angles. Use bar charts instead. +- **3D charts**: Never. They distort perception and add no information. +- **Dual-axis charts**: Use cautiously. They can mislead by implying correlation. Clearly label both axes if used. +- **Stacked bar (many categories)**: Hard to compare middle segments. Use small multiples or grouped bars instead. +- **Donut charts**: Slightly better than pie charts but same fundamental issues. Use for single KPI display at most. + +## Python Visualization Code Patterns + +### Setup and Style + +```python +import matplotlib.pyplot as plt +import matplotlib.ticker as mticker +import seaborn as sns +import pandas as pd +import numpy as np + +# Professional style setup +plt.style.use('seaborn-v0_8-whitegrid') +plt.rcParams.update({ + 'figure.figsize': (10, 6), + 'figure.dpi': 150, + 'font.size': 11, + 'axes.titlesize': 14, + 'axes.titleweight': 'bold', + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 10, + 'figure.titlesize': 16, +}) + +# Colorblind-friendly palettes +PALETTE_CATEGORICAL = ['#4C72B0', '#DD8452', '#55A868', '#C44E52', '#8172B3', '#937860'] +PALETTE_SEQUENTIAL = 'YlOrRd' +PALETTE_DIVERGING = 'RdBu_r' +``` + +### Line Chart (Time Series) + +```python +fig, ax = plt.subplots(figsize=(10, 6)) + +for label, group in df.groupby('category'): + ax.plot(group['date'], group['value'], label=label, linewidth=2) + +ax.set_title('Metric Trend by Category', fontweight='bold') +ax.set_xlabel('Date') +ax.set_ylabel('Value') +ax.legend(loc='upper left', frameon=True) +ax.spines['top'].set_visible(False) +ax.spines['right'].set_visible(False) + +# Format dates on x-axis +fig.autofmt_xdate() + +plt.tight_layout() +plt.savefig('trend_chart.png', dpi=150, bbox_inches='tight') +``` + +### Bar Chart (Comparison) + +```python +fig, ax = plt.subplots(figsize=(10, 6)) + +# Sort by value for easy reading +df_sorted = df.sort_values('metric', ascending=True) + +bars = ax.barh(df_sorted['category'], df_sorted['metric'], color=PALETTE_CATEGORICAL[0]) + +# Add value labels +for bar in bars: + width = bar.get_width() + ax.text(width + 0.5, bar.get_y() + bar.get_height()/2, + f'{width:,.0f}', ha='left', va='center', fontsize=10) + +ax.set_title('Metric by Category (Ranked)', fontweight='bold') +ax.set_xlabel('Metric Value') +ax.spines['top'].set_visible(False) +ax.spines['right'].set_visible(False) + +plt.tight_layout() +plt.savefig('bar_chart.png', dpi=150, bbox_inches='tight') +``` + +### Histogram (Distribution) + +```python +fig, ax = plt.subplots(figsize=(10, 6)) + +ax.hist(df['value'], bins=30, color=PALETTE_CATEGORICAL[0], edgecolor='white', alpha=0.8) + +# Add mean and median lines +mean_val = df['value'].mean() +median_val = df['value'].median() +ax.axvline(mean_val, color='red', linestyle='--', linewidth=1.5, label=f'Mean: {mean_val:,.1f}') +ax.axvline(median_val, color='green', linestyle='--', linewidth=1.5, label=f'Median: {median_val:,.1f}') + +ax.set_title('Distribution of Values', fontweight='bold') +ax.set_xlabel('Value') +ax.set_ylabel('Frequency') +ax.legend() +ax.spines['top'].set_visible(False) +ax.spines['right'].set_visible(False) + +plt.tight_layout() +plt.savefig('histogram.png', dpi=150, bbox_inches='tight') +``` + +### Heatmap + +```python +fig, ax = plt.subplots(figsize=(10, 8)) + +# Pivot data for heatmap format +pivot = df.pivot_table(index='row_dim', columns='col_dim', values='metric', aggfunc='sum') + +sns.heatmap(pivot, annot=True, fmt=',.0f', cmap='YlOrRd', + linewidths=0.5, ax=ax, cbar_kws={'label': 'Metric Value'}) + +ax.set_title('Metric by Row Dimension and Column Dimension', fontweight='bold') +ax.set_xlabel('Column Dimension') +ax.set_ylabel('Row Dimension') + +plt.tight_layout() +plt.savefig('heatmap.png', dpi=150, bbox_inches='tight') +``` + +### Small Multiples + +```python +categories = df['category'].unique() +n_cats = len(categories) +n_cols = min(3, n_cats) +n_rows = (n_cats + n_cols - 1) // n_cols + +fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 4*n_rows), sharex=True, sharey=True) +axes = axes.flatten() if n_cats > 1 else [axes] + +for i, cat in enumerate(categories): + ax = axes[i] + subset = df[df['category'] == cat] + ax.plot(subset['date'], subset['value'], color=PALETTE_CATEGORICAL[i % len(PALETTE_CATEGORICAL)]) + ax.set_title(cat, fontsize=12) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + +# Hide empty subplots +for j in range(i+1, len(axes)): + axes[j].set_visible(False) + +fig.suptitle('Trends by Category', fontsize=14, fontweight='bold', y=1.02) +plt.tight_layout() +plt.savefig('small_multiples.png', dpi=150, bbox_inches='tight') +``` + +### Number Formatting Helpers + +```python +def format_number(val, format_type='number'): + """Format numbers for chart labels.""" + if format_type == 'currency': + if abs(val) >= 1e9: + return f'${val/1e9:.1f}B' + elif abs(val) >= 1e6: + return f'${val/1e6:.1f}M' + elif abs(val) >= 1e3: + return f'${val/1e3:.1f}K' + else: + return f'${val:,.0f}' + elif format_type == 'percent': + return f'{val:.1f}%' + elif format_type == 'number': + if abs(val) >= 1e9: + return f'{val/1e9:.1f}B' + elif abs(val) >= 1e6: + return f'{val/1e6:.1f}M' + elif abs(val) >= 1e3: + return f'{val/1e3:.1f}K' + else: + return f'{val:,.0f}' + return str(val) + +# Usage with axis formatter +ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: format_number(x, 'currency'))) +``` + +### Interactive Charts with Plotly + +```python +import plotly.express as px +import plotly.graph_objects as go + +# Simple interactive line chart +fig = px.line(df, x='date', y='value', color='category', + title='Interactive Metric Trend', + labels={'value': 'Metric Value', 'date': 'Date'}) +fig.update_layout(hovermode='x unified') +fig.write_html('interactive_chart.html') +fig.show() + +# Interactive scatter with hover data +fig = px.scatter(df, x='metric_a', y='metric_b', color='category', + size='size_metric', hover_data=['name', 'detail_field'], + title='Correlation Analysis') +fig.show() +``` + +## Design Principles + +### Color + +- **Use color purposefully**: Color should encode data, not decorate +- **Highlight the story**: Use a bright accent color for the key insight; grey everything else +- **Sequential data**: Use a single-hue gradient (light to dark) for ordered values +- **Diverging data**: Use a two-hue gradient with neutral midpoint for data with a meaningful center +- **Categorical data**: Use distinct hues, maximum 6-8 before it gets confusing +- **Avoid red/green only**: 8% of men are red-green colorblind. Use blue/orange as primary pair + +### Typography + +- **Title states the insight**: "Revenue grew 23% YoY" beats "Revenue by Month" +- **Subtitle adds context**: Date range, filters applied, data source +- **Axis labels are readable**: Never rotated 90 degrees if avoidable. Shorten or wrap instead +- **Data labels add precision**: Use on key points, not every single bar +- **Annotation highlights**: Call out specific points with text annotations + +### Layout + +- **Reduce chart junk**: Remove gridlines, borders, backgrounds that don't carry information +- **Sort meaningfully**: Categories sorted by value (not alphabetically) unless there's a natural order (months, stages) +- **Appropriate aspect ratio**: Time series wider than tall (3:1 to 2:1); comparisons can be squarer +- **White space is good**: Don't cram charts together. Give each visualization room to breathe + +### Accuracy + +- **Bar charts start at zero**: Always. A bar from 95 to 100 exaggerates a 5% difference +- **Line charts can have non-zero baselines**: When the range of variation is meaningful +- **Consistent scales across panels**: When comparing multiple charts, use the same axis range +- **Show uncertainty**: Error bars, confidence intervals, or ranges when data is uncertain +- **Label your axes**: Never make the reader guess what the numbers mean + +## Accessibility Considerations + +### Color Blindness + +- Never rely on color alone to distinguish data series +- Add pattern fills, different line styles (solid, dashed, dotted), or direct labels +- Test with a colorblind simulator (e.g., Coblis, Sim Daltonism) +- Use the colorblind-friendly palette: `sns.color_palette("colorblind")` + +### Screen Readers + +- Include alt text describing the chart's key finding +- Provide a data table alternative alongside the visualization +- Use semantic titles and labels + +### General Accessibility + +- Sufficient contrast between data elements and background +- Text size minimum 10pt for labels, 12pt for titles +- Avoid conveying information only through spatial position (add labels) +- Consider printing: does the chart work in black and white? + +### Accessibility Checklist + +Before sharing a visualization: +- [ ] Chart works without color (patterns, labels, or line styles differentiate series) +- [ ] Text is readable at standard zoom level +- [ ] Title describes the insight, not just the data +- [ ] Axes are labeled with units +- [ ] Legend is clear and positioned without obscuring data +- [ ] Data source and date range are noted + +## Workflow + +1. Name the message of the chart before choosing its form. +2. Choose chart type from the data relationship (comparison, trend, distribution, part-to-whole). +3. Encode honestly: zero-based bars, unbroken axes, colorblind-safe palettes, no color-only meaning. +4. Label directly where possible; title with the takeaway, not the dataset name. +5. Review against accessibility and the audience's decision context. + +## Anti-patterns + +- Pie charts for more than a handful of categories, or any precise comparison. +- Truncated bar axes that inflate small differences into visual drama. +- Dual y-axes implying correlation the data does not establish. +- Meaning carried by color alone, invisible to colorblind readers and grayscale prints. + +## The Interdependency information-design extension + +Use this extension for information-bearing color and visual signaling in charts, explanatory diagrams, infographics, study materials, dashboards, knowledge maps, and related visual surfaces. The evidence summary is in `references/information-design-evidence.md`; machine-readable defaults are in `visual-grammar.json`. + +### Evidence boundary + +The strongest practical evidence supports **signaling**: selective, meaningful visual cues can improve attention, organization, retention, and transfer when they clarify what belongs together or where the reader should look. This does not establish universal hue psychology. Do not claim that red inherently impairs reasoning, blue creates creativity, green improves learning, or similar fixed effects. + +### Core grammar + +- **Color encodes structure; it does not substitute for structure.** +- **Salience is relational.** A hue attracts attention because of contrast with its surround and competing signals, not because it is intrinsically dominant. +- **Hue gets one independent semantic dimension per local visual field.** If hue means component family, evidence state must use another channel such as shape + label. +- **Critical meaning is redundant.** Repeat information-bearing color with a label, shape, line style, pattern, icon, enclosure, or position. +- **Luminance and contrast are load-bearing.** Critical boundaries need adequate lightness contrast and line weight, not hue alone. +- **Stable mappings support learning.** Reuse the same mapping for the same meaning across an artifact family. +- **Exact claims remain explicit.** Labels, numbers, provenance, uncertainty, and status carry exact meaning. + +Recommended visual-channel allocation: + +| Channel | Preferred meaning | +|---|---| +| Hue | one categorical family or one semantic dimension | +| Lightness | emphasis or ordered magnitude | +| Shape | state class or category redundancy | +| Position | structural layer or reading order | +| Line direction | processing, dependency, causal, or temporal flow | +| Line style | current, provisional, historical, or unavailable | +| Border | authority, selection, or scope boundary | +| Text | exact semantic meaning | +| Pattern | color-independent redundancy | + +### Default semantic states + +These are communication defaults, not transfers of epistemic authority or project canon: + +| State | Color | Required non-color redundancy | +|---|---|---| +| supported | `#009E73` | solid circle/line + `SUPPORTED` | +| falsified | `#D55E00` | octagon/cross + `FALSIFIED` | +| errored | `#CC79A7` | diamond/zigzag + `ERROR` | +| unavailable / NA | `#F0E442` | hollow square/dotted line + `NA` | +| historical provenance | `#6B7280` | dashed enclosure + `HISTORICAL` | +| current maintained authority | `#0072B2` | double border + `CURRENT` | + +Project branding may override hues, but not contrast, redundancy, or semantic-audit requirements. + +### Information-design workflow + +1. **Declare the message.** Write one sentence describing what the reader should understand or decide. +2. **Declare semantic dimensions.** List component family, evidence state, modality, recursion depth, authority/provenance, uncertainty, temporal status, magnitude, or other variables and assign each to a visual channel. +3. **Establish the neutral substrate.** Most content should be neutral or low-chroma so accents retain signaling power. +4. **Allocate a salience budget.** Reserve the strongest saturation/contrast for the information deserving first attention; decoration must never outrank evidence/status distinctions. +5. **Add redundant state markers.** No critical state is color-only. +6. **Check contrast.** Minimum targets: normal text `4.5:1`; large text `3:1`; essential graphical objects and state boundaries `3:1`. +7. **Run four publication gates:** grayscale, color-vision-deficiency review, contrast, and semantic audit. +8. **Preserve a nonvisual representation.** Alt text or structured metadata states the message, entities, relations, flow, statuses, uncertainty, and necessary quantitative values. + +A deterministic manifest can be checked with: + +```bash +python data-visualization/information_design_audit.py \ + data-visualization/examples/information-design-manifest.json +``` + +The checker verifies declared WCAG contrast and color-independent state redundancy. It does **not** claim to simulate human perception or prove comprehension. + +### Extension anti-patterns + +- Rainbow decoration where every node competes for attention. +- Reusing one hue for both component identity and evidence status. +- White or light text on yellow/orange fills without a contrast check. +- Thin isoluminant colored lines as the only critical boundary. +- Treating `NA` as zero or implying neutrality when data is unavailable. +- Coloring recursion depth progressively redder unless severity is actually being encoded. +- Letting risk-matrix color bands replace the underlying number or threshold definition. +- Calling a palette "colorblind-safe" and assuming its text contrast therefore passes WCAG. +- Claiming an automated contrast check proves accessibility, memory, comprehension, or emotional effect. + +## Provenance + +Imported from `anthropics/knowledge-work-plugins` @ `94e1a08` (`data/skills/data-visualization/`), Apache-2.0. +Local modifications: trigger phrasing normalized to skill-lib convention; the Workflow/Anti-patterns/Provenance bookend and the clearly marked The Interdependency information-design extension were appended. The imported upstream body remains otherwise unmodified. The extension is evidence-grounded in `references/information-design-evidence.md`. See `ATTRIBUTION.md` at repo root. + +hmmm +- Image-level protan/deutan/tritan simulation is intentionally not claimed by the stdlib audit; rendered-artifact perceptual review remains a separate tool/human gate. +- Exact project brand palettes may override default hues while preserving the shared grammar and publication gates. +- Whether image-generation workflows should emit a sidecar design manifest automatically is unresolved. +- Upstream re-sync cadence with `anthropics/knowledge-work-plugins` is undecided; drift against upstream is currently invisible. diff --git a/.agents/skills/data-visualization/examples/information-design-manifest.json b/.agents/skills/data-visualization/examples/information-design-manifest.json new file mode 100644 index 0000000..f6bf69e --- /dev/null +++ b/.agents/skills/data-visualization/examples/information-design-manifest.json @@ -0,0 +1,32 @@ +{ + "message": "The reader can distinguish structural families from evidence states without relying on color alone.", + "semantic_dimensions": { + "component_family": "hue", + "evidence_state": "shape+label", + "authority": "border", + "flow": "line_direction" + }, + "text_pairs": [ + {"foreground": "#FFFFFF", "background": "#0B1020", "size": "normal"}, + {"foreground": "#111827", "background": "#F7F9FC", "size": "normal"} + ], + "nontext_pairs": [ + {"foreground": "#0072B2", "background": "#F7F9FC"} + ], + "states": [ + {"name": "supported", "color": "#009E73", "redundancy": ["label", "solid_circle"]}, + {"name": "falsified", "color": "#D55E00", "redundancy": ["label", "octagon"]}, + {"name": "errored", "color": "#CC79A7", "redundancy": ["label", "diamond"]}, + {"name": "unavailable", "color": "#F0E442", "redundancy": ["label", "hollow_square"]}, + {"name": "historical", "color": "#6B7280", "redundancy": ["label", "dashed_enclosure"]}, + {"name": "current", "color": "#0072B2", "redundancy": ["label", "double_border"]} + ], + "manual_gates": { + "grayscale": "hmmm", + "cvd": "hmmm", + "semantic": "pass" + }, + "hmmm": [ + "Image-level grayscale and CVD review must be performed on the rendered artifact." + ] +} diff --git a/.agents/skills/data-visualization/information_design_audit.py b/.agents/skills/data-visualization/information_design_audit.py new file mode 100644 index 0000000..f5b9fbf --- /dev/null +++ b/.agents/skills/data-visualization/information_design_audit.py @@ -0,0 +1,149 @@ +# ratios: loc_comments=117:4 imports_exports=7:4 calls_definitions=61:6 +"""Pure-stdlib information-design manifest audit. + +Checks declared WCAG contrast pairs and verifies that information-bearing states +include at least one non-color redundancy. It does not simulate human vision. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path +from typing import Any + +HEX_RE = re.compile(r"^#[0-9A-Fa-f]{6}$") + + +def _srgb_channel(value: int) -> float: + channel = value / 255.0 + if channel <= 0.04045: + return channel / 12.92 + return ((channel + 0.055) / 1.055) ** 2.4 + + +def relative_luminance(color: str) -> float: + if not HEX_RE.match(color): + raise ValueError(f"invalid sRGB hex color: {color!r}") + r = _srgb_channel(int(color[1:3], 16)) + g = _srgb_channel(int(color[3:5], 16)) + b = _srgb_channel(int(color[5:7], 16)) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def contrast_ratio(foreground: str, background: str) -> float: + first = relative_luminance(foreground) + second = relative_luminance(background) + light, dark = max(first, second), min(first, second) + return (light + 0.05) / (dark + 0.05) + + +def _audit_pairs(pairs: list[dict[str, Any]], *, default_threshold: float, pair_type: str) -> list[dict[str, Any]]: + findings: list[dict[str, Any]] = [] + for index, pair in enumerate(pairs): + foreground = str(pair.get("foreground", "")) + background = str(pair.get("background", "")) + threshold = 3.0 if pair_type == "text" and pair.get("size") == "large" else default_threshold + try: + ratio = contrast_ratio(foreground, background) + except ValueError as exc: + findings.append({"severity": "error", "code": "invalid_color", "index": index, "message": str(exc)}) + continue + if ratio < threshold: + findings.append({ + "severity": "error", + "code": f"{pair_type}_contrast", + "index": index, + "ratio": round(ratio, 2), + "threshold": threshold, + "foreground": foreground, + "background": background, + }) + return findings + + +def audit_manifest(manifest: dict[str, Any]) -> dict[str, Any]: + findings: list[dict[str, Any]] = [] + if not str(manifest.get("message", "")).strip(): + findings.append({"severity": "error", "code": "missing_message", "message": "manifest requires a one-sentence reader takeaway"}) + + dimensions = manifest.get("semantic_dimensions", {}) + if not isinstance(dimensions, dict) or not dimensions: + findings.append({"severity": "error", "code": "missing_semantic_dimensions", "message": "declare semantic dimensions and their visual channels"}) + else: + hue_dimensions = [ + name for name, channel in dimensions.items() + if "hue" in re.split(r"[^a-z]+", str(channel).strip().lower()) + ] + if len(hue_dimensions) > 1: + findings.append({"severity": "error", "code": "hue_overloaded", "dimensions": hue_dimensions, "message": "hue carries more than one independent semantic dimension"}) + + text_pairs = manifest.get("text_pairs", []) + nontext_pairs = manifest.get("nontext_pairs", []) + if not isinstance(text_pairs, list): + findings.append({"severity": "error", "code": "bad_text_pairs", "message": "text_pairs must be a list"}) + text_pairs = [] + if not isinstance(nontext_pairs, list): + findings.append({"severity": "error", "code": "bad_nontext_pairs", "message": "nontext_pairs must be a list"}) + nontext_pairs = [] + findings.extend(_audit_pairs(text_pairs, default_threshold=4.5, pair_type="text")) + findings.extend(_audit_pairs(nontext_pairs, default_threshold=3.0, pair_type="nontext")) + + states = manifest.get("states", []) + if not isinstance(states, list): + findings.append({"severity": "error", "code": "bad_states", "message": "states must be a list"}) + states = [] + for index, state in enumerate(states): + name = str(state.get("name", "")).strip() or f"state[{index}]" + color = str(state.get("color", "")) + if not HEX_RE.match(color): + findings.append({"severity": "error", "code": "invalid_state_color", "state": name, "color": color}) + redundancy = state.get("redundancy", []) + noncolor = [ + item for item in redundancy + if str(item).strip() and str(item).strip().lower() not in {"color", "hue"} + ] if isinstance(redundancy, list) else [] + if not noncolor: + findings.append({"severity": "error", "code": "color_only_state", "state": name, "message": "state requires a non-color cue such as label, shape, pattern, or line style"}) + + manual = manifest.get("manual_gates", {}) + required_manual = {"grayscale", "cvd", "semantic"} + if not isinstance(manual, dict): + manual = {} + missing = sorted(required_manual - set(manual)) + if missing: + findings.append({"severity": "warning", "code": "manual_gates_unrecorded", "gates": missing, "message": "manual perceptual/semantic gates remain visible hmmm until reviewed"}) + + errors = [item for item in findings if item["severity"] == "error"] + warnings = [item for item in findings if item["severity"] == "warning"] + return {"status": "pass" if not errors else "fail", "errors": errors, "warnings": warnings, "hmmm": manifest.get("hmmm", [])} + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Audit an information-design manifest for contrast and semantic redundancy.") + parser.add_argument("manifest", type=Path) + parser.add_argument("--json", action="store_true", help="emit machine-readable output") + args = parser.parse_args(argv) + + try: + manifest = json.loads(args.manifest.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + print(f"information-design audit: fail ({exc})", file=sys.stderr) + return 2 + + report = audit_manifest(manifest) + if args.json: + print(json.dumps(report, indent=2, sort_keys=True)) + else: + print(f"information-design audit: {report['status']} ({len(report['errors'])} errors, {len(report['warnings'])} warnings)") + for finding in [*report["errors"], *report["warnings"]]: + print(f"{finding['severity'].upper()} {finding['code']}: {finding}") + + return 0 if report["status"] == "pass" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=117:4 imports_exports=7:4 calls_definitions=61:6 diff --git a/.agents/skills/data-visualization/references/information-design-evidence.md b/.agents/skills/data-visualization/references/information-design-evidence.md new file mode 100644 index 0000000..f5ed972 --- /dev/null +++ b/.agents/skills/data-visualization/references/information-design-evidence.md @@ -0,0 +1,101 @@ +# Evidence basis for information design + +This reference records the evidence boundary behind the local information-design extension to `data-visualization/SKILL.md`. It is deliberately narrower than popular "color psychology": the operational doctrine relies most strongly on perception, visual search, multimedia signaling, accessibility, and replicated/meta-analytic findings. + +## High-confidence operational findings + +### Signaling and multimedia learning + +Visual signaling — including color coding, arrows, outlines, labels, typographic emphasis, and other correspondence cues — generally improves learning when it clarifies organization or text-picture relations. Meta-analytic effects are positive but moderate; signaling cannot rescue incoherent content. + +- Schneider, Beege, Nebel & Rey, *A meta-analysis of how signaling affects learning with media*, Educational Research Review 23 (2018): https://www.sciencedirect.com/science/article/abs/pii/S1747938X17300581 +- Richter, Scheiter & Eitel, signaling text-picture relations dataset / meta-analysis materials: https://psycharchives.org/handle/20.500.12034/2019 +- Cambridge Handbook of Multimedia Learning, signaling/cueing principle: https://www.cambridge.org/core/books/cambridge-handbook-of-multimedia-learning/signaling-or-cueing-principle-in-multimedia-learning/3972D4ACC628D5B53F7B2B4785DB2B06 + +Operational consequence: use color as one cue in a broader signaling system; prefer stable semantic mappings and direct correspondence over decorative saturation. + +### Attention and visual search + +Color is a basic visual feature that can support efficient target selection. A salient but irrelevant singleton can also capture attention. Salience depends on contrast with the surround, competing features, task goals, and expectations. + +- Treisman & Gelade, *A Feature-Integration Theory of Attention*: https://www.cse.psu.edu/~rtc12/CSE597E/papers/treismanFeatIntegration.pdf +- Wolfe review of visual search: https://search.bwh.harvard.edu/new/pubs/the_review.pdf +- Adam et al., additional-singleton visual-search replication/open dataset: https://pmc.ncbi.nlm.nih.gov/articles/PMC8323537/ + +Operational consequence: budget salience. A bright accent means less if every element is bright, and decorative accents can compete with evidence-bearing signals. + +### Color perception and luminance + +Human color perception emerges from cone signals, opponent mechanisms, and interacting visual pathways. Fine chromatic differences and isoluminant boundaries can be weaker than boundaries reinforced by luminance, thickness, form, or motion. + +- Gegenfurtner & Kiper, *Color Vision*: https://pubmed.ncbi.nlm.nih.gov/12574494/ +- Masri et al., parvocellular/magnocellular pathway review: https://pmc.ncbi.nlm.nih.gov/articles/PMC7574660/ + +Operational consequence: do not make hue the only carrier of a critical boundary. Use luminance contrast, adequate line weight, shape, and labels. + +### Accessibility and non-color redundancy + +WCAG requires sufficient text and non-text contrast and requires a non-color means of conveying information when color carries meaning. + +- W3C, Use of Color: https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html +- W3C, Non-text Contrast: https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html +- W3C WCAG quick reference: https://www.w3.org/WAI/WCAG22/quickref/ +- Okabe & Ito Color Universal Design guidance: https://jfly.uni-koeln.de/color/ + +Operational consequence: normal text target 4.5:1; large text and essential graphical objects 3:1; state distinctions require label/shape/pattern/line-style redundancy. + +## Useful but context-dependent findings + +### Color and memory + +Color can support recognition and memory when it contributes meaningful object or category information, organizes material, or supplies a stable retrieval cue. Arbitrary or excessive color can increase distraction and search cost. + +- Review of color and memory: https://pmc.ncbi.nlm.nih.gov/articles/PMC3743993/ +- Memory effects on color perception, *Handbook of Color Psychology*: https://www.cambridge.org/core/books/handbook-of-color-psychology/memory-effects-on-color-perception/8B6D32011AD699E4BBCD2299CDE46F3B + +Operational consequence: color-code stable relations and categories, not every sentence or node. + +### Color-emotion associations + +There are broad cross-cultural regularities, but associations vary with language, geography, culture, context, brightness, saturation, and task. + +- Jonauskaite et al., 30-country color-emotion study: https://pubmed.ncbi.nlm.nih.gov/32900287/ + +Operational consequence: treat cultural color meanings as priors to test, not universal laws. + +### Red and cognitive performance + +Popular claims that red reliably impairs cognition are not supported as a general rule. A meta-analysis of 67 effects found negligible estimates for several task classes and weak/unstable evidence elsewhere after publication-bias adjustment. + +- Gnambs, *Limited evidence for the effect of red color on cognitive performance: A meta-analysis*: https://pubmed.ncbi.nlm.nih.gov/32696125/ + +Operational consequence: use vermillion/red because it communicates a declared diagnostic or warning role in context, not because it is assumed to manipulate reasoning. + +### Color and decision framing + +Color can change risk perception or choice salience in some contexts, including online risk tasks and risk matrices. + +- Gnambs et al., red and risk-taking: https://journals.plos.org/plosone/article?id=10.1371%2Fjournal.pone.0134033 +- Proto et al., colored cells in risk matrices: https://onlinelibrary.wiley.com/doi/full/10.1111/risa.14091 + +Operational consequence: never let a color band replace the underlying number, threshold definition, or uncertainty. + +## Claims this extension rejects + +The evidence above does **not** establish universal rules such as: + +- red makes people worse at reasoning; +- blue makes people more creative; +- green makes people learn better; +- one palette is optimal for every culture, task, display, or viewer; +- a colorblind-safe categorical palette automatically satisfies text-contrast requirements; +- passing a contrast calculation proves comprehension or accessibility of the whole artifact. + +## Research provenance + +This evidence list was assembled from the deep-research report **"Color as a Cognitive and Information-Design Instrument"**, completed 2026-08-07. The report synthesized peer-reviewed cognitive psychology, neuroscience, multimedia-learning meta-analyses, information-design evidence, and W3C accessibility guidance. This reference extracts only the claims needed for the operational extension; it does not reproduce the report as project canon or theorem evidence. + +hmmm + +- Future updates should prioritize systematic reviews, meta-analyses, replications, and standards over single-study novelty. +- Image-level CVD simulation needs a separate rendering tool; the stdlib audit only verifies color-independent redundancy and declared contrast. diff --git a/.agents/skills/data-visualization/visual-grammar.json b/.agents/skills/data-visualization/visual-grammar.json new file mode 100644 index 0000000..26f34ea --- /dev/null +++ b/.agents/skills/data-visualization/visual-grammar.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "purpose": "Evidence-grounded defaults for information-bearing color and visual signaling.", + "wcag": { + "normal_text_min_contrast": 4.5, + "large_text_min_contrast": 3.0, + "essential_nontext_min_contrast": 3.0 + }, + "channels": { + "hue": "one categorical family or one semantic dimension", + "lightness": "emphasis or ordered magnitude", + "shape": "state class or category redundancy", + "position": "structural layer or reading order", + "line_direction": "processing, dependency, causal, or temporal flow", + "line_style": "current, provisional, historical, or unavailable", + "border": "authority, selection, or scope boundary", + "text": "exact semantic meaning", + "pattern": "color-independent redundancy" + }, + "palette": { + "blue": {"hex": "#0072B2", "default_role": "stable structural or informational family"}, + "bluish_green": {"hex": "#009E73", "default_role": "supported or continuous family"}, + "orange": {"hex": "#E69F00", "default_role": "candidate, transformation, routing, or active transition"}, + "vermillion": {"hex": "#D55E00", "default_role": "diagnostic tension, contradiction, interruption, or error boundary"}, + "sky_blue": {"hex": "#56B4E9", "default_role": "secondary informational channel"}, + "reddish_purple": {"hex": "#CC79A7", "default_role": "meta-level, recursive, semantic, or transformation family"}, + "yellow": {"hex": "#F0E442", "default_role": "localized highlight or unresolved marker"}, + "charcoal": {"hex": "#111827", "default_role": "primary ink or authoritative outline"}, + "provenance_gray": {"hex": "#6B7280", "default_role": "historical or de-emphasized provenance"}, + "dark_background": {"hex": "#0B1020", "default_role": "dark neutral substrate"}, + "light_background": {"hex": "#F7F9FC", "default_role": "light neutral substrate"}, + "bone": {"hex": "#E7E1D5", "default_role": "neutral continuity or historical material"} + }, + "state_defaults": { + "supported": {"color": "#009E73", "redundancy": ["label", "solid_circle_or_line"]}, + "falsified": {"color": "#D55E00", "redundancy": ["label", "octagon_or_cross"]}, + "errored": {"color": "#CC79A7", "redundancy": ["label", "diamond_or_zigzag"]}, + "unavailable": {"color": "#F0E442", "redundancy": ["label", "hollow_square_or_dotted_line"]}, + "historical": {"color": "#6B7280", "redundancy": ["label", "dashed_enclosure"]}, + "current": {"color": "#0072B2", "redundancy": ["label", "double_border"]} + }, + "publication_gates": [ + "grayscale", + "color_vision_deficiency", + "contrast", + "semantic" + ], + "principles": [ + "color encodes structure rather than decoration", + "no critical state is conveyed by color alone", + "salience is relational and budgeted", + "exact meaning remains textual or otherwise explicit", + "hue does not carry two independent semantic dimensions in one local visual field", + "project branding may override hues but not contrast, redundancy, or semantic audit requirements" + ] +} diff --git a/.agents/skills/deps-build/SKILL.md b/.agents/skills/deps-build/SKILL.md new file mode 100644 index 0000000..088047c --- /dev/null +++ b/.agents/skills/deps-build/SKILL.md @@ -0,0 +1,93 @@ +--- +name: deps-build +description: Self-declaring dependency topology built on msdmd. Each module declares dependency edges it owns in a `# === DEPENDENCIES ===` block; a runner builds an import/call/capability graph, detects unresolved edges and cycles, and surfaces visible dependency coverage gaps. Load this when declaring module dependencies, auditing architecture drift, checking graph cycles, or wiring dependency topology checks into CI. +--- + +# deps-build — Dependency topology on msdmd + +`deps-build` is an application of [msdmd](../msdmd/SKILL.md). It makes a +module's dependency edges visible beside the code that creates them, so +architecture drift becomes inspectable instead of hidden in imports. + +Implementation status: this skill defines the `DEPENDENCIES` block and runner +contract. This repo does not currently ship a DEPENDENCIES graph runner; +consuming repos should implement the contract below with local resolvers. + +Read `msdmd/SKILL.md` first if you have not. The block syntax, parser +contract, and visible gap rule are inherited. + +## The block + +```python +# === DEPENDENCIES === +# id: chat_route_dependency_edges +# summary: chat API route depends on auth context and chat repository +# imports: auth.user_context, repositories.chat +# calls: ChatRepository.get_by_owner +# requires: auth_user_context, chat_repository +# class: runtime +# === END DEPENDENCIES === +``` + +## Field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Stable dependency declaration id. | +| `summary` | One-sentence description of why these edges exist. | + +At least one edge field is required unless the entry records `hmmm`: + +| Edge field | Meaning | +|---|---| +| `imports` | Comma-separated modules/packages imported by this module. | +| `calls` | Comma-separated functions, methods, routes, commands, or capabilities called by this module. | +| `requires` | Comma-separated msdmd ids this module depends on. | +| `provides` | Comma-separated ids or surfaces this module provides to others. | +| `external` | Comma-separated external services, APIs, or packages this module depends on. | + +Optional: + +| Field | Meaning | +|---|---| +| `class` | Dependency class (`runtime`, `build`, `test`, `docs`, `ops`, `agent`). | +| `direction` | `inbound`, `outbound`, `bidirectional`, or `hmmm`. | +| `owner` | Person, role, or team responsible for this dependency shape. | +| `since` | Version or date the declaration was added. | +| `deprecated` | If present, marks an edge scheduled for removal. | + +## Runner contract + +A DEPENDENCIES runner MUST: + +1. Parse every `DEPENDENCIES` block with the universal msdmd parser. +2. Build a graph from `imports`, `calls`, `requires`, `provides`, and + `external` fields where resolvers exist. +3. Report unresolved non-`hmmm` edges as drift. +4. Report cycles in classes where cycles are disallowed by local policy. +5. Report modules with imports/calls but no DEPENDENCIES block as visible + coverage gaps when the runner can detect them. +6. Exit non-zero for malformed required fields, unresolved resolvable edges, + or forbidden cycles. Coverage gaps fail only in strict mode. + +## Reporting shape + +- `EDGE`: declared edge and source module. +- `UNRESOLVED`: declared edge no longer resolves. +- `CYCLE`: graph cycle detected. +- `PENDING`: edge or direction recorded as `hmmm`. +- `GAP`: dependency-bearing module without DEPENDENCIES metadata. + +## Anti-patterns + +- Treating an import list as architecture without explaining why edges exist. +- Declaring dependencies only in a central graph file. +- Hiding unresolved dependencies by omitting them; use `hmmm`. +- Failing all cycles blindly; some test or plugin graphs may intentionally cycle. + +hmmm +- exact resolver syntax for cross-language call and route edges +- which dependency classes disallow cycles by default +- whether package-manager dependencies should be declared here or only source-level edges diff --git a/.agents/skills/distributed-publication/README.md b/.agents/skills/distributed-publication/README.md new file mode 100644 index 0000000..8635940 --- /dev/null +++ b/.agents/skills/distributed-publication/README.md @@ -0,0 +1,7 @@ +# distributed-publication + +Read [`SKILL.md`](SKILL.md) before assembling one public reading surface from source-owned content distributed across repositories or independently governed files. + +Load [`../interdependent-work-graph/SKILL.md`](../interdependent-work-graph/SKILL.md) with it. The work graph establishes participants and authority; this skill governs ordered source spines, exact retrieval identities, source-local status and license boundaries, explicit fallback, correction routing, rendering, and publication build provenance. + +The first reference implementation is the Chapters Zero through Seven reader in `The-Interdependency/The-Interdependency.github.io`. diff --git a/.agents/skills/distributed-publication/SKILL.md b/.agents/skills/distributed-publication/SKILL.md new file mode 100644 index 0000000..681a383 --- /dev/null +++ b/.agents/skills/distributed-publication/SKILL.md @@ -0,0 +1,272 @@ +--- +name: distributed-publication +description: Provenance-bearing publication from distributed source owners. Load this when assembling, displaying, or maintaining one ordered textbook, report, standard, corpus, archive, knowledge surface, or public reading sequence whose authoritative units live in multiple repositories or independently owned files; when a publication consumer must retrieve exact commits, blobs, and content digests; when source order, source-local license or status, explicit fallback, correction routing, or public build identity must remain intact. Load interdependent-work-graph with it. Do not load for ordinary single-repository documentation, a link index that does not reproduce source content, or cross-repository code coordination with no publication artifact. +--- + +# distributed-publication — gather the reading, not the authority + +Use this procedural skill when one publication must present content whose ownership remains distributed. The publication consumer may assemble, order, render, index, search, and expose provenance. It may not silently become the author, licensor, canonical source, theorem authority, or status owner of the material it displays. + +Load `interdependent-work-graph/SKILL.md` first or alongside this skill. The work-graph skill identifies participants, exact identities, authority roles, relations, and non-transfer boundaries. This skill specializes the publication edge: how independently owned source units become one bounded reading surface without losing their ownership. + +## Core contract + +```text +one reading surface != one source authority +publication order != ownership transfer +content identity != producer authentication +``` + +- Declare the complete ordered source spine before implementing routes or rendering. +- Resolve every displayed source to an exact commit or immutable artifact identity. +- Bind content to repository, path, expected title or identity marker, commit, blob or object identity, and content digest. +- Preserve source-local license, canon, theorem, proof, certification, measurement, empirical, and frontier status. +- Render source text exactly unless a separately labeled transformation is explicitly requested. +- Route corrections to the source owner; refresh the publication after the source changes. +- Fail closed in production when required current sources cannot be resolved. +- Make retained snapshots or offline copies visibly fallback, never silently current. +- Publish the source identities used by the built artifact so the live surface can be independently checked. +- Preserve unresolved signatures, licensing questions, renderer limits, and source conflicts as `hmmm`. + +## Non-trigger + +Do not load this skill for: + +- a document whose authoritative content and release history live entirely inside one repository; +- a directory of outbound links that does not reproduce, transform, or order the linked content; +- ordinary package dependencies or a multi-repository code change with no publication surface; +- a one-time quotation or citation that already follows the source's normal attribution rules; +- a plain-language companion view over one source — use `plain-lens` for that transformation contract. + +Load it when publication creates a new combined reading object whose truth depends on preserving several independently owned sources together. + +## Authority model + +Each publication has at least two kinds of participant: + +```text +source owner owns its content, license, status, and correction history +publication consumer owns ordering, retrieval, rendering, navigation, indexing, and display provenance +``` + +Additional participants may include a canon source, research ledger, external corpus, renderer, static fallback, search index, or deployment environment. Their authority must be stated rather than inferred from proximity. + +The publication consumer may say: + +- which source was displayed; +- which immutable identity and digest were used; +- where it appears in the reading order; +- which license declaration and license-review state the source supplied; +- whether retrieval was current or fallback; +- which rendering and accessibility checks passed. + +The publication consumer may not say, merely by displaying the source: + +- that it owns or supersedes the source; +- that all source licenses have merged; +- that theorem or proof status crosses source boundaries; +- that an implementation chapter validates a theory chapter; +- that an exact digest authenticates who authored or transported the content; +- that a retained snapshot is the current source. + +## Workflow + +1. **Load the work graph.** Resolve every source owner, publication consumer, renderer, deployment surface, and other participant that can change the published result. +2. **Declare the publication object.** Name the textbook, report, standard, corpus, archive, or other combined reading surface. State whether order is load-bearing. +3. **Define the ordered source spine.** For every unit declare position, stable source identifier, repository or artifact owner, path or object locator, expected title or identity marker, source-local license and license-review state, source-local status, and correction destination. +4. **Resolve immutable identity.** Pin commit and blob/object identity where available, plus a content digest over the exact bytes displayed. Branch names remain navigation aids only. +5. **Declare non-transfer boundaries.** At minimum cover authorship, ownership, license, canon status, proof status, certification status, measurement validity, empirical validity, and frontier status. +6. **Choose rendering mode.** Prefer exact, static-first rendering. Disable source HTML unless it is explicitly trusted and sanitized. If content is transformed, label the output as interpretation and preserve a path to exact source. +7. **Define failure and fallback policy.** Required current sources fail closed in production. Offline or degraded builds may use a retained snapshot only when visibly marked with its retained identity and retrieval failure. +8. **Build navigable publication surfaces.** Provide an ordered index, stable unit routes, source evidence, previous/next navigation where sequence matters, search where useful, and a static/no-JavaScript reading path. +9. **Publish build identity.** Emit a machine-readable artifact binding the publication build to every displayed source identity and fallback state. +10. **Validate locally and across sources.** Check order, completeness, expected titles or identity markers, license fields, digests, exact source links, status labels, routes, accessibility, and the public build manifest. +11. **Route corrections upstream.** Patch source content only in its owning repository. Patch ordering, rendering, or provenance defects only in the publication consumer. +12. **Carry hmmm forward.** Preserve unresolved signatures, source conflicts, license ambiguity, inaccessible formats, renderer gaps, or deployment freshness as explicit boundaries. + +## Distributed-publication reference contract + +A machine-consumed publication manifest may use this reference shape: + +```json +{ + "schema": "the-interdependency.distributed-publication", + "version": "1.0.0", + "publication_id": "", + "title": "", + "order_is_load_bearing": true, + "sources": [ + { + "position": 0, + "source_id": "", + "repository": "owner/name", + "path": "path/to/source.md", + "expected_title": "", + "commit": "<40-hex commit>", + "blob": "", + "content_sha256": "<64-hex digest>", + "authority": "what this source owns", + "license": "SPDX expression|source-declared text|hmmm", + "license_status": "declared|unknown|human-review-required", + "status": "source-local status", + "correction_target": "owner/name:path/to/source.md", + "fallback": false + } + ], + "consumer": { + "repository": "owner/publication", + "commit": "<40-hex commit>", + "route_prefix": "/publication/", + "renderer": "exact-markdown-static", + "fallback_policy": "fail-closed-production|explicit-retained-snapshot" + }, + "boundaries": { + "authorship_transfer": false, + "ownership_transfer": false, + "license_transfer": false, + "canonical_status_transfer": false, + "proof_status_transfer": false, + "certification_status_transfer": false, + "measurement_status_transfer": false, + "empirical_status_transfer": false, + "digest_is_authentication": false, + "hmmm": [] + }, + "publication_sha256": "" +} +``` + +For version `1.0.0`, `publication_sha256` is SHA-256 over canonical JSON containing exactly `publication_id`, `title`, `order_is_load_bearing`, `sources`, `consumer`, and `boundaries`, sorted by object key with compact separators. Array order is preserved and therefore part of identity. A schema revision is required to add or reinterpret hashed fields. + +`expected_title` may contain a complete title, heading prefix, or other declared identity marker, but its matching rule must be explicit in the consumer. `license` records the source's own declaration rather than a publication-wide inference. `license_status` keeps unknown or human-review-required compatibility visible; neither field authorizes the consumer to relicense the source. + +This reference contract complements, rather than replaces, `the-interdependency.stack-manifest`. The stack manifest identifies the complete work graph. The distributed-publication manifest identifies the exact ordered reading artifact produced from that graph. + +## Exact source and transformed views + +Exact source display and companion interpretation are separate publication modes. + +- Exact mode reproduces source content without editorial rewriting and exposes immutable source identity. +- Transformed mode may summarize, translate, annotate, or provide an audience/domain lens, but must label the transformation and link to exact source. +- When transformed views are required, load `plain-lens` in addition to this skill. +- A transformed view must not replace the exact source route or inherit source-local status merely because it is adjacent. + +## Failure and fallback discipline + +A production publication must not silently omit, reorder, or replace required units. + +Fail production when: + +- a required source cannot resolve to its declared path or object; +- a source title or identity marker no longer matches `expected_title` under the declared matching rule; +- an exact commit, object identity, or digest is missing; +- a source-local license declaration or license-review state is absent rather than explicitly `hmmm` or `unknown`; +- the ordered spine contains duplicates, gaps, or unexpected reordering; +- current retrieval falls back while the release claims current source coverage; +- rendered output loses a required status or provenance boundary. + +A degraded or offline mode may continue only when: + +- the retained source identity is visible; +- fallback state is machine-readable and human-readable; +- no retained copy is called current; +- missing content becomes `hmmm`, not invented prose; +- production and degraded policies are distinguishable in tests and configuration. + +## Output shape + +When this skill is active, produce or maintain: + +```markdown +## Publication object +- title, stable ID, ordered/unordered status + +## Ordered source spine +- position: source identity — expected title/marker — authority — license/status — correction target + +## Publication consumer +- repository, routes, renderer, search/navigation, fallback policy + +## Non-transfer boundaries +- authorship, ownership, license, canon, proof, certification, measurement, empirical, authentication + +## Build identity +- consumer commit +- source commits/objects/digests +- fallback states + +## Validation +- source completeness and order +- expected identity markers and source-local license fields +- exact rendering and routes +- browser/accessibility/static fallback +- public build-manifest check + +## hmmm +- unresolved source, license, authentication, rendering, or deployment boundaries +``` + +For machine consumption, also emit the versioned distributed-publication manifest or an explicitly named equivalent with the same obligations. + +## Validation + +A successful application demonstrates: + +- every required source appears exactly once in the declared order; +- every source has an exact or visibly unresolved identity; +- every source carries an expected title or identity marker with a declared matching rule; +- every source carries its own license declaration and license-review state, including explicit `hmmm` or `unknown` where unresolved; +- content digests recompute from the bytes displayed; +- the publication consumer does not shadow or rewrite source-owned content in exact mode; +- source-local licenses and statuses remain visible and do not transfer; +- corrections route to the owning source; +- production fails closed on missing or fallback required sources; +- degraded mode remains explicit and non-inventive; +- stable index and unit routes render without JavaScript; +- sequential navigation works where order matters; +- automated accessibility checks pass and manual review remains acknowledged; +- the public build artifact exposes every source identity used; +- later agents can reproduce the same publication from the manifest without rediscovering the source spine. + +## Anti-patterns + +- Copying distributed source files into the publication repository and treating the copies as new authority. +- Fetching `main`, `latest`, package availability, or an unpinned URL during an evidence-producing build without recording the resolved immutable identity. +- Treating a digest as a signature or author authentication. +- Omitting per-source license declarations and then implying one publication-wide license. +- Combining licenses into one implied publication license without explicit permission. +- Letting a theory chapter inherit implementation or test status from neighboring chapters. +- Silently dropping a source that failed retrieval. +- Reordering a load-bearing sequence according to filename or fetch completion order. +- Rendering trusted HTML from distributed Markdown by default. +- Fixing source prose in the publication consumer instead of the owning source. +- Allowing fallback content in a production build that claims current completeness. +- Making the exact reading experience depend on client-side JavaScript. +- Publishing provenance only in logs rather than in the artifact visitors receive. + +## Minimal example + +The first reference implementation is the distributed Interdependency textbook: + +```text +Chapter 0 metapat root theory +Chapter 1 ucns carrier foundations +Chapter 2 edcm measurement discipline +Chapter 3 skill-lib self-declaration method +Chapter 4 interdependent-lib cross-repository canon placement +Chapter 5 ptcna architecture +Chapter 6 a0 research instrument +Chapter 7 zfae theory under development +website publication consumer +``` + +The website owns the reading sequence, routes, rendering, accessibility, and publication provenance. It owns none of the chapter texts or their source-local status. + +## hmmm + +- Content digests establish byte identity, not cryptographic authorship or transport authentication. +- A general signed-source contract for distributed publications is not yet selected. +- License compatibility can be displayed and checked for declared metadata, but legal compatibility still requires competent human review. +- Mathematical notation accessibility requires more than automated HTML accessibility scanning; the canonical cross-format expectation remains unresolved. +- Whether the reference manifest becomes its own metadata-block/schema skill after multiple independent implementations. +- Whether publication consumers should retain source snapshots in version control, release artifacts, object storage, or an external content-addressed archive remains context-dependent. diff --git a/.agents/skills/distributed-publication/examples/manifest.json b/.agents/skills/distributed-publication/examples/manifest.json new file mode 100644 index 0000000..7dfb44f --- /dev/null +++ b/.agents/skills/distributed-publication/examples/manifest.json @@ -0,0 +1,47 @@ +{ + "schema": "the-interdependency.distributed-publication", + "version": "1.0.0", + "publication_id": "interdependency-textbook-0-7", + "title": "The Interdependency — Chapters Zero Through Seven", + "order_is_load_bearing": true, + "sources": [ + { + "position": 0, + "source_id": "chapter-zero", + "repository": "The-Interdependency/metapat", + "path": "CHAPTER_ZERO.md", + "expected_title": "Chapter Zero", + "commit": "hmmm", + "blob": "hmmm", + "content_sha256": "hmmm", + "authority": "Chapter Zero content, license, and source-local status", + "license": "hmmm", + "license_status": "human-review-required", + "status": "root ontology and theory", + "correction_target": "The-Interdependency/metapat:CHAPTER_ZERO.md", + "fallback": false + } + ], + "consumer": { + "repository": "The-Interdependency/The-Interdependency.github.io", + "commit": "hmmm", + "route_prefix": "/chapters/", + "renderer": "exact-markdown-static", + "fallback_policy": "fail-closed-production|explicit-retained-snapshot" + }, + "boundaries": { + "authorship_transfer": false, + "ownership_transfer": false, + "license_transfer": false, + "canonical_status_transfer": false, + "proof_status_transfer": false, + "certification_status_transfer": false, + "measurement_status_transfer": false, + "empirical_status_transfer": false, + "digest_is_authentication": false, + "hmmm": [ + "Example identities and source license are intentionally unresolved; a real publication manifest pins exact values and matching rules." + ] + }, + "publication_sha256": "hmmm" +} diff --git a/.agents/skills/doc-build/SKILL.md b/.agents/skills/doc-build/SKILL.md new file mode 100644 index 0000000..e4b03a1 --- /dev/null +++ b/.agents/skills/doc-build/SKILL.md @@ -0,0 +1,90 @@ +--- +name: doc-build +description: Self-declaring documentation coverage built on msdmd. Each module declares the public, developer, operator, or agent-facing documentation it owns in a `# === DOCS ===` block; a runner verifies linked docs and anchors exist, reports stale or missing documentation, and surfaces visible coverage gaps. Load this when adding or auditing module documentation, when tying code surfaces to docs, or when wiring documentation coverage checks into CI. +--- + +# doc-build — Documentation contracts on msdmd + +`doc-build` is an application of [msdmd](../msdmd/SKILL.md). It turns a +module's documentation obligations into colocated metadata so docs drift is +observable instead of discovered by surprise. + +Implementation status: this skill defines the `DOCS` block and runner contract. +This repo does not currently ship a DOCS runner script; consuming repos should +implement the contract below against their own documentation tree. + +Read `msdmd/SKILL.md` first if you have not. The block syntax, parser +contract, and visible gap rule are inherited. + +## The block + +Every module with user, developer, operator, or agent-facing behavior may +declare one or more documentation contracts: + +```python +# === DOCS === +# id: chat_api_public_docs +# summary: public documentation for creating and reading chat conversations +# audience: developer +# source: docs/chat.md#conversations +# covers: create_conversation, get_conversation +# status: current +# === END DOCS === +``` + +## Field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Stable documentation contract id. | +| `summary` | One-sentence description of what the docs promise to explain. | +| `audience` | One of `user`, `developer`, `operator`, `agent`, `internal`, or `hmmm`. | +| `source` | Path to the documentation file, optionally with an anchor (`docs/file.md#heading`). Use `hmmm` if the target is not resolved yet. | +| `status` | `current`, `draft`, `deprecated`, or `hmmm`. | + +Optional: + +| Field | Meaning | +|---|---| +| `covers` | Comma-separated module surfaces, routes, functions, components, or concepts covered by the doc. | +| `examples` | Comma-separated example ids, files, or anchors the doc depends on. | +| `requires` | Comma-separated ids this documentation contract depends on. | +| `owner` | Person, role, or team responsible for doc freshness. | +| `since` | Version or date the contract was added. | + +## Runner contract + +A DOCS runner MUST: + +1. Parse every `DOCS` block with the universal msdmd parser. +2. Verify each non-`hmmm` `source` path exists. +3. If `source` includes an anchor, verify the target heading or anchor + exists when the file format supports anchors. +4. Report `status: draft` and `source: hmmm` as pending, not passing. +5. Report modules with no `DOCS` block as documentation coverage gaps. +6. Exit non-zero for missing files, missing anchors, malformed required + fields, or deprecated docs referenced as current. Coverage gaps fail only + in strict mode. + +## Reporting shape + +Normal output should group results as: + +- `PASS`: docs target exists and required fields are valid. +- `PENDING`: `hmmm` or `draft` documentation contracts. +- `DRIFT`: source path, anchor, or covered surface no longer resolves. +- `GAP`: source modules with no DOCS block. + +## Anti-patterns + +- Putting documentation ownership only in a separate docs index. +- Marking docs `current` when the source is `hmmm`. +- Treating missing DOCS blocks as invisible because the code has comments. +- Letting generated docs replace the source-owned declaration. + +hmmm +- whether examples listed in `examples` must execute or only resolve +- whether public exported surfaces without DOCS should fail strict mode by default +- how to normalize anchors across Markdown renderers diff --git a/.agents/skills/domain-claims/SKILL.md b/.agents/skills/domain-claims/SKILL.md new file mode 100644 index 0000000..cf28a73 --- /dev/null +++ b/.agents/skills/domain-claims/SKILL.md @@ -0,0 +1,347 @@ +--- +name: domain-claims +description: Domain-first lexical and semantic governance for canonical terms. Load this when a word or phrase is being promoted into a theorem term, ontology primitive, schema field, encoding label, skill doctrine, cross-domain mapping, or other meaning-bearing control surface; when multiple domains use the same word differently; when an acronym, initialism, symbol, or compact handle is being mistaken for a fixed expansion or definition; or when conversational provenance is about to be attached to a definition. Do not load for ordinary prose, casual wording choices, or simple dictionary explanations that will not control canon or structure. +--- + +# domain-claims — establish semantic standing before provenance + +`domain-claims` prevents an ambiguous word from acquiring structural authority merely +because a definition or conversation can be cited for it. + +The governing rule is: + +> Before a word becomes a canonical term, identify which domain has standing to +> claim the applicable sense and scope. + +For compact handles, an additional rule applies: + +> |∆|Acronyms identify; they do not define. Expansions are instance-resolved +> properties, not canonical identities, unless the claiming domain explicitly +> ratifies a fixed expansion as identity-bearing.|∆| + +A domain claim does not own a word everywhere. It claims authority over one bounded +sense of that word inside a declared scope. + +## Load this when + +- A word is being promoted into canon, a theorem, a schema, an ontology, an encoding, + a skill, a metric, a module field, or another structural control surface. +- A definition is being derived from conversation and will later authorize an + implementation or encoding. +- The same surface word appears in several domains with different meanings. +- An acronym, initialism, symbol, or compact project handle is being expanded and the + expansion could be mistaken for the handle's identity. +- A cross-domain import, specialization, translation, or shared term needs to be + declared. +- An agent must decide whether provenance belongs to this sense of a word or to a + different homonym. + +## Do not load this when + +- The user is choosing ordinary prose that will not become canon or structure. +- The task is only to provide a general-language dictionary meaning. +- A spelling, grammar, or style edit does not alter the governing sense of a term. +- The relevant domain claim has already been ratified and no collision or scope + change is present. + +## Zeroth-provenance doctrine + +The semantic dependency order is: + +```text +domain claim + -> domain-bound definition + -> conversational/source provenance + -> ratification status + -> canonical semantic record + -> downstream encoding or implementation +``` + +Provenance may show where words were spoken or written. It cannot by itself establish +which domain had standing to define the operative sense. + +Therefore: + +- a definition without a domain claim is **unscoped**; +- provenance attached to an unscoped word is **lexically ambiguous**; +- an encoding based on an unresolved word is **not authorized**; +- a domain collision is a fail-closed boundary, not a cue to choose the most familiar + meaning. + +## Despecified handles + +A **despecified handle** is a stable surface identifier whose identity is not exhausted +by, and does not require, one canonical lexical expansion. This is the default treatment +for The Interdependency's acronym-like semantic control surfaces unless a domain claim +explicitly ratifies a fixed expansion. + +Despecified does not mean unspecified. Identity remains constrained by the resolved +term id, claiming domain, scope, relations, invariants, provenance, and current instance. +An expansion may improve legibility in one outward-facing artifact without becoming a +global definition. + +For an acronym, initialism, symbol, or compact handle, extend the domain-claim record +when needed: + +```yaml +handle: + kind: acronym | initialism | symbol | compact-name + identity_mode: despecified | fixed-expansion + canonical_expansion: none | | hmmm + instance_expansions: + - text: + scope: + status: instance-definition | proposed | ratified | superseded + provenance: +``` + +Rules: + +- `despecified` means the handle can remain stable while instance expansions vary; +- an instance expansion is a property of that instance, not retroactive authority over + every use of the handle; +- `fixed-expansion` requires explicit domain ratification; familiarity, search rank, + historical usage, or lexical plausibility is insufficient; +- external acronym collisions do not redefine an internal handle; they remain separate + domain claims and are tested only where scopes overlap; +- changing an expansion does not change identity unless the active domain claim makes + that expansion identity-bearing; +- a handle whose relations, invariants, scope, and provenance cannot recover its + operative identity is underspecified and remains `hmmm`. + +## Domain-claim record + +Before canonization or encoding, produce a record containing at least: + +```yaml +surface_form: +term_id: +claiming_domain: +claimed_sense: +scope: +claim_type: native | borrowed | specialized | translated | shared | contested | provisional +authority_source: +status: proposed | provisional | ratified | contested | superseded +included_uses: + - +excluded_uses: + - +neighboring_terms: + - +known_collisions: + - +effective_version: +supersedes: +unresolved: + - +``` + +The stable `term_id` should be domain-qualified, for example: + +```text +ucns.relational_geometry.radius +metapat.encoding.fork +software_architecture.layer +``` + +Do not use the bare surface word as the global identifier. + +## Claim types + +- **native** — the domain defines the sense directly. +- **borrowed** — the domain imports another domain's sense without changing it. +- **specialized** — the domain narrows a broader sense. +- **translated** — the domain maps a source-domain term into a target vocabulary. +- **shared** — several domains deliberately use one reconciled sense. +- **contested** — overlapping claims remain live and unreconciled. +- **provisional** — the sense may be used experimentally but is not ratified canon. + +Claim type and status are separate. A translated claim may be ratified; a native +claim may still be provisional. + +## Collision test + +A `DOMAIN_COLLISION` exists when all are true: + +1. the surface word, phrase, or handle is the same or treated as equivalent; +2. the scopes overlap for the current task; +3. the claimed senses differ materially; +4. no explicit translation, specialization, precedence, or disambiguation rule + resolves the overlap. + +Different expansions of a despecified handle are not by themselves a collision. Test +the domain-bound senses and overlapping scopes, not merely the words chosen to expand +the letters. + +On collision, emit: + +```text +DOMAIN_COLLISION +term: +applicable claims: +- : +- : +resolution required before canonization or encoding +``` + +Do not silently choose by recency, popularity, model familiarity, lexical similarity, +repository proximity, or the most common acronym expansion returned by search. + +## Workflow + +1. **Detect promotion.** Determine whether the word will control canon, structure, + theorem language, implementation, measurement, or encoding. If not, stop; + ordinary language remains fluid. +2. **Enumerate candidate domains.** Name every domain whose claim could reasonably + apply in the current scope. +3. **Declare the claim.** Create or retrieve the domain-claim record before drafting + the operative definition. +4. **Resolve handle identity.** For acronyms, initialisms, symbols, and compact names, + determine whether identity is `despecified` or explicitly `fixed-expansion` before + treating any expansion as semantic authority. +5. **Run the collision test.** Fail closed on unresolved overlap. +6. **Bind the definition.** Write the definition as a claim of the domain, not as a + universal statement about the surface word or expansion. +7. **Attach provenance.** Attach conversation excerpts, documents, commits, examples, + corrections, and counterexamples to the domain-qualified sense. +8. **Ratify honestly.** Mark proposed, provisional, ratified, contested, or superseded. + Do not turn accepted discussion into retroactive authority for earlier artifacts. +9. **Authorize downstream use.** Only a resolved domain-bound definition may control + an ontology, schema, theorem term, METAPAT record, UCNS encoding, or other structural + surface. +10. **Preserve hmmm.** Unresolved scope, collisions, borrowing rules, handle identity, + and authority questions remain visible. + +## Relationship to other skills + +- **Before `canon`:** `domain-claims` establishes which domain-qualified sense is under + review; `canon` then determines whether the claim is declared, implemented, + repo-local, inferred, desired, or `hmmm`. +- **Before `gonol-build`:** lexical classification must not collapse domain-specific + senses that require separate term identities. +- **Before `plain-lens`:** companion views may simplify wording but must preserve the + active domain claim and disclose when a familiar word carries a specialized sense. + Expanding a despecified handle for readability must remain explicitly instance-bound. +- **Before semantic encodings:** structural possibility does not authorize meaning. + The encoding must cite the resolved domain claim. + +## Minimal examples + +### Radius + +```yaml +surface_form: radius +term_id: ucns.relational_geometry.radius +claiming_domain: UCNS relational geometry +claimed_sense: recursive payload depth +scope: UCNS objects and UCNS-derived relational projections +claim_type: specialized +status: ratified +excluded_uses: + - breadth valuation log(len(A_plus)) + - Euclidean physical distance from a center +neighboring_terms: + - ucns.relational_geometry.breadth +``` + +### Despecified acronym + +```yaml +surface_form: EDCM +term_id: the-interdependency.edcm +claiming_domain: The Interdependency +claimed_sense: +scope: +handle: + kind: acronym + identity_mode: despecified + canonical_expansion: none + instance_expansions: + - text: + scope: + status: instance-definition + provenance: +``` + +A search result that expands `EDCM` differently supplies another domain claim, not a +replacement definition. Resolve scope and sense first. The letters remain the stable +handle. + +### Fork + +UCNS may claim the structural sense "multiple payload-bearing branches." METAPAT may +separately claim the semantic sense "simultaneous constitutive components of one +parent." The structural fact does not supply the semantic authorization. An encoding +must cite the METAPAT claim before treating a payload fork as a hyper-tensor layer. + +## Output shape + +When this skill is active, return: + +```markdown +## Domain claim +- Surface form: +- Term id: +- Claiming domain: +- Claimed sense: +- Scope: +- Claim type: +- Status: +- Handle identity: despecified | fixed-expansion | not-applicable | hmmm + +## Boundaries +- Included: +- Excluded: +- Neighboring terms: + +## Collision check +- Applicable claims: +- Resolution: clear | DOMAIN_COLLISION | hmmm + +## Provenance allowed next +- Sources that may now attach to this domain-qualified sense: + +## Downstream authorization +- Canon/encoding/implementation permitted: yes | no | provisional +``` + +## Validation + +A successful application demonstrates that: + +- the domain claim appears before the definition and provenance; +- the term has a stable domain-qualified identifier; +- scope and exclusions are explicit; +- acronym/handle expansions are not promoted to identity without explicit ratification; +- overlapping claims were tested rather than ignored; +- unresolved collisions block downstream structural use; +- provenance is attached to the claimed sense, not merely the surface word; +- ordinary language was not needlessly forced into a registry. + +## Anti-patterns + +- Treating a dictionary definition as authority for a specialized domain term. +- Attaching a conversation to a bare word and assuming every later use inherits it. +- Saying a domain owns a word globally. +- Treating the most familiar or searchable acronym expansion as the handle's definition. +- Freezing an outward-facing expansion into global canon without explicit ratification. +- Using repository location as semantic precedence. +- Encoding a homonym because its field name matches. +- Treating structural detectability as semantic authorization. +- Creating domain claims for every ordinary word and making conversation unusable. +- Erasing contested claims instead of preserving the collision. + +hmmm + +- whether domain-claim records should later gain an msdmd metadata-block sibling and + registry runner +- whether despecified-handle records need an organization-wide machine-readable registry + or should remain attached only to domain-qualified claims +- whether ratified conversational definition events should have a standard immutable + transcript-envelope schema +- how multilingual surface forms share or fork a term identity without assuming exact + translation +- how domain authority is delegated, revoked, or shared across organizations + +The word remains common. The claim gives one sense standing. The handle remains stable. +Conversation then earns the definition that structure is permitted to carry. diff --git a/.agents/skills/epac-selection-display/SKILL.md b/.agents/skills/epac-selection-display/SKILL.md new file mode 100644 index 0000000..3f0039f --- /dev/null +++ b/.agents/skills/epac-selection-display/SKILL.md @@ -0,0 +1,268 @@ +--- +name: epac-selection-display +description: Evidence-bound EPAC target selection and display for WebMCP handoffs and other human-facing surfaces. Load this when choosing an EPAC element, molecule, construction receipt, comparison result, or available visualization to present; when preparing a receipt-backed EPAC display packet; when exposing the EPAC workflow as a selectable WebMCP skill; or when a requested EPAC display would require missing or invented geometry so the request can be refused or downgraded to verified source-backed output. Do not load to select EPAC or a UCNS candidate as canon, or for unrelated WebMCP catalogue changes. +--- + +# epac-selection-display — choose what to show without promoting what it means + +Use this procedural skill to turn an exact EPAC research artifact into a +human-readable, provenance-bearing display request or result. It governs target +selection and presentation. It does not define EPAC, select a constructor as +canon, or give a public MCP server authority to execute repository code. + +## Core boundary + +```text +display selection != canon selection +presented claim <= source receipt and its standing +WebMCP handoff != EPAC execution authority +``` + +The EPAC handle is despecified here. A source repository may use an expansion in +one provisional research instance, but this skill does not freeze that expansion +as EPAC's canonical identity. + +At source state `The-Interdependency/stack@5b24db9a7fe40df4b2791e1137ade5de01c78942`, +`research/epac` is a provisional research scaffold with no independent authoritative +source repository. Treat its code, receipts, standings, nonclaims, and `hmmm` as +scoped research evidence, not organization canon. This identity records the source +observed when this workflow was repaired; resolve the current source again on every +use rather than treating it as a permanent inventory. + +## Selection record + +Fix the following before constructing or displaying anything: + +```text +source_repository: +source_commit_or_snapshot: +source_path: +working_tree_state: clean | dirty-with-digests | hmmm +target_kind: element | molecule | receipt | comparison | population | hmmm +target_id: +occurrence_or_instance: +constructor_or_reader: +display_mode: summary | text | svg | receipt-json | comparison | hmmm +audience: +destination: WebMCP handoff | file | response | other +``` + +The target kinds and display modes are descriptive, not an evergreen API enum. +Admit only targets and renderers that the selected source identity actually +provides. + +## Workflow + +### 1. Resolve authority and exact source identity + +Inspect the current EPAC-owning or incubating repository, its governing +instructions, status documentation, code, tests, and registries. Record a commit +or immutable snapshot. If uncommitted bytes are explicitly in scope, record the +base commit plus content digests for every consumed dirty file; do not present +those bytes as a commit-pinned public artifact. + +When the display crosses from EPAC source to a website or MCP consumer, use +`interdependent-work-graph`. EPAC retains research-artifact authority; +`skill-lib` owns this reusable workflow; the website owns presentation. No +semantic, mathematical, empirical, measurement, proof, or canon status transfers +between them. + +### 2. Separate content choice from status choice + +Selecting `C`, `H2O`, a receipt, or a comparison for display chooses content. It +does not choose EPAC, its constructor, its geometry, or a UCNS option as canonical. + +Preserve the source's `selection_effect`, standing, nonclaims, and unresolved +items. If the request asks which candidate should win or whether evidence permits +promotion, pause display selection and load the applicable option-selection and +domain-authority workflow. A display preference cannot ratify a candidate. + +### 3. Discover the live target surface + +Read the selected source identity rather than relying on a remembered list. For +the current stack scaffold, relevant surfaces may include: + +- declared element records and `construct_element_gonol`; +- declared molecule compositions and `construct_molecule`; +- `PublicGonolReceipt` plus its replay function; +- post-construction comparison records; and +- a text or SVG renderer only when it exists in the selected source identity and + its tests establish the requested projection. + +Do not assume a local, untracked, proposed, or previously observed visualizer is +available in a commit-pinned source. If no verified renderer exists, emit the +receipt-backed summary rather than inventing a visual projection. A request that +would require invented geometry is therefore a load-to-refuse case for this skill, +not a reason to bypass it. + +### 4. Admit and construct the exact target + +Resolve the user-facing target to one exact registered identifier and occurrence. +Reject ambiguous symbols, formulas, aliases, or instances instead of selecting a +familiar default. + +Use the source-owned public constructor or reader. Preserve closed participants, +carried options, declared couplings, charge states, and occurrence identity. For +an existing receipt, independently replay it and require the reconstructed digest +to match before calling the display verified. + +For preregistered molecular comparison work, construction remains blind to sealed +known-shape labels. Open comparison-only data after construction and keep +`SURVIVED`, `FALSIFIED`, and `UNRESOLVED` distinct from selection or canon status. + +### 5. Choose a representation that does not add claims + +Use the smallest representation that meets the human request: + +- `summary` — identity, standing, digest, structure summary, nonclaims, and + `hmmm`; +- `text` — a source-provided textual projection plus the summary; +- `svg` — a source-provided deterministic SVG plus equivalent text and accessible + title/description; +- `receipt-json` — canonical or source-declared receipt serialization; and +- `comparison` — source readouts and terminal standings, visibly labeled as + post-construction evidence. + +A renderer may project only values already carried by the construction, receipt, +or an explicitly pinned upstream law. It must not infer positions, couplings, +Cartesian coordinates, empirical angles, shape labels, or a missing geometric +operation. + +### 6. Emit the evidence packet beside the display + +Include every available field that controls interpretation: + +```text +source repository + exact identity + path +target kind + exact target id + occurrence +constructor/reader id and version +constructor standing and selection_effect +source_id and receipt digest +pinned carrier or upstream digest +display mode and renderer identity +audience +destination +replay/verification result +structure/readout fields actually projected +nonclaims +hmmm +``` + +Omit absent fields only by marking them `hmmm` or explaining that the selected +artifact type does not define them. Never fill them from a neighboring checkout +or from general chemistry knowledge. + +### 7. Preserve the WebMCP boundary + +When this skill is presented by The Interdependency WebMCP surface: + +1. expose the exact canonical skill entry to both the human catalogue and the + read-only registry tools; +2. require repository selection before skill selection; +3. require an explicit Send before publishing the page-session handoff; +4. carry the selected repository head, canonical skill identity and closure, and + the human's target/display request; and +5. leave all construction, file writes, repository access, and deployment to the + agent's separately authorized tools. + +The remote MCP server remains a read-only registry. Do not import EPAC modules, +open sealed comparison data, render SVG, persist the request, or mutate a +repository inside that server merely because this skill is selectable. + +### 8. Validate the completed presentation + +A presentation is `READY` only when the source identity and target are exact, the +requested representation exists, receipt replay or the source-declared verifier +passes, and the evidence packet accompanies the display. + +Return `BLOCKED` for a missing declared prerequisite, `INVALID` for an ambiguous +or rejected target or digest mismatch, and `hmmm` when authority, source identity, +or representation semantics remain unresolved. These are display-workflow +results, not EPAC research standings. + +## Output shape + +```markdown +## EPAC selection +- Source identity: +- Target: +- Display mode: +- Audience: +- Destination: +- Display status: READY | BLOCKED | INVALID | hmmm + +## Evidence packet +- Constructor / reader: +- Receipt / upstream identities: +- Replay / verification: +- Standing and selection effect: +- Audience: +- Destination: + +## Display +- Human-readable result or artifact link: +- Equivalent text / accessibility: + +## Boundaries +- Claims carried: +- Nonclaims: +- hmmm: +``` + +## Usage guidance + +On the WebMCP page, choose the repository that contains the intended EPAC source, +then choose **Select and display EPAC**, and send a bounded request such as: + +```text +Display the committed EPAC element C as a receipt-backed text summary. Pin the +source commit and path, replay the receipt, show the constructor standing and +selection_effect, and preserve every nonclaim and hmmm. Do not select canon. +``` + +For a graphical request, make renderer availability conditional: + +```text +If the selected commit contains a tested receipt-backed SVG renderer, display H2O +as SVG with equivalent text and the complete evidence packet. Otherwise return a +verified receipt summary and mark SVG display BLOCKED; do not invent geometry. +``` + +## Validation + +A valid use demonstrates that: + +- target selection and canon/option selection remained distinct; +- the exact source and renderer identities were recorded; +- only a source-admitted target and representation were used; +- receipt replay or the declared verifier closed successfully; +- sealed comparison labels did not leak into construction; +- the display added no geometry or empirical interpretation; +- audience and destination survived into the evidence packet and output; +- nonclaims, research standings, `selection_effect`, and `hmmm` stayed visible; +- human-readable and agent-readable WebMCP catalogues exposed the same skill; and +- the MCP registry/handoff stayed read-only and permission-neutral. + +## Anti-patterns + +- Treating a displayed EPAC object as selected or canonical. +- Expanding EPAC and presenting the expansion as a globally fixed identity. +- Selecting a target by spelling correction, chemical familiarity, or an + undeclared alias. +- Using dirty or untracked renderer code while claiming commit-pinned provenance. +- Drawing bonds, angles, positions, or shape names absent from the receipt. +- Opening sealed comparison labels during construction. +- Dropping a failed or unresolved standing because it makes the display awkward. +- Making the public MCP server execute research code or accept repository writes. +- Showing a skill card to the human that the MCP registry cannot inspect, or the + reverse. + +## hmmm + +- the future independent EPAC repository, authority, and release identity; +- whether EPAC will ever ratify a fixed expansion rather than remain a + despecified handle; +- the first committed, source-owned public renderer and its stable interface; +- the durable schema, if any, for EPAC display packets; +- whether a future separately authorized service should execute EPAC displays; + the current WebMCP server is registry and handoff only. diff --git a/.agents/skills/epac-selection-display/STACK_SOURCE_FIXTURE.json b/.agents/skills/epac-selection-display/STACK_SOURCE_FIXTURE.json new file mode 100644 index 0000000..4480aa3 --- /dev/null +++ b/.agents/skills/epac-selection-display/STACK_SOURCE_FIXTURE.json @@ -0,0 +1,22 @@ +{ + "schema": "the-interdependency.epac-selection-display-source-fixture", + "version": "1.0.0", + "source": { + "repository": "The-Interdependency/stack", + "commit": "5b24db9a7fe40df4b2791e1137ade5de01c78942", + "path": "research/epac/README.md", + "git_blob_sha": "a1f1dd6a50252349797806b2dc59897f1fb3a991" + }, + "standing": "provisional stack-local EPAC research scaffold", + "boundaries": { + "authority_transfer": false, + "canon_status_transfer": false, + "proof_status_transfer": false, + "measurement_status_transfer": false, + "empirical_status_transfer": false + }, + "usage": "Verify that the historical source identity cited by epac-selection-display is anchored to an exact stack commit, path, and Git blob before relying on it as provenance. Resolve the current live EPAC source separately on every use.", + "hmmm": [ + "EPAC has no independent authoritative source repository at this historical source state." + ] +} diff --git a/.agents/skills/explore-data/SKILL.md b/.agents/skills/explore-data/SKILL.md new file mode 100644 index 0000000..c42e1b4 --- /dev/null +++ b/.agents/skills/explore-data/SKILL.md @@ -0,0 +1,351 @@ +--- +name: explore-data +description: Profile and explore a dataset to understand its shape, quality, and patterns. Use this when encountering a new table or file, checking null rates and column distributions, spotting data quality issues like duplicates or suspicious values, or deciding which dimensions and metrics to analyze. +argument-hint: "" +--- + +# /explore-data - Profile and Explore a Dataset + +> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md). + +Generate a comprehensive data profile for a table or uploaded file. Understand its shape, quality, and patterns before diving into analysis. + +## Usage + +``` +/explore-data +``` + +## Workflow + +### 1. Access the Data + +**If a data warehouse MCP server is connected:** + +1. Resolve the table name (handle schema prefixes, suggest matches if ambiguous) +2. Query table metadata: column names, types, descriptions if available +3. Run profiling queries against the live data + +**If a file is provided (CSV, Excel, Parquet, JSON):** + +1. Read the file and load into a working dataset +2. Infer column types from the data + +**If neither:** + +1. Ask the user to provide a table name (with their warehouse connected) or upload a file +2. If they describe a table schema, provide guidance on what profiling queries to run + +### 2. Understand Structure + +Before analyzing any data, understand its structure: + +**Table-level questions:** +- How many rows and columns? +- What is the grain (one row per what)? +- What is the primary key? Is it unique? +- When was the data last updated? +- How far back does the data go? + +**Column classification** — categorize each column as one of: +- **Identifier**: Unique keys, foreign keys, entity IDs +- **Dimension**: Categorical attributes for grouping/filtering (status, type, region, category) +- **Metric**: Quantitative values for measurement (revenue, count, duration, score) +- **Temporal**: Dates and timestamps (created_at, updated_at, event_date) +- **Text**: Free-form text fields (description, notes, name) +- **Boolean**: True/false flags +- **Structural**: JSON, arrays, nested structures + +### 3. Generate Data Profile + +Run the following profiling checks: + +**Table-level metrics:** +- Total row count +- Column count and types breakdown +- Approximate table size (if available from metadata) +- Date range coverage (min/max of date columns) + +**All columns:** +- Null count and null rate +- Distinct count and cardinality ratio (distinct / total) +- Most common values (top 5-10 with frequencies) +- Least common values (bottom 5 to spot anomalies) + +**Numeric columns (metrics):** +``` +min, max, mean, median (p50) +standard deviation +percentiles: p1, p5, p25, p75, p95, p99 +zero count +negative count (if unexpected) +``` + +**String columns (dimensions, text):** +``` +min length, max length, avg length +empty string count +pattern analysis (do values follow a format?) +case consistency (all upper, all lower, mixed?) +leading/trailing whitespace count +``` + +**Date/timestamp columns:** +``` +min date, max date +null dates +future dates (if unexpected) +distribution by month/week +gaps in time series +``` + +**Boolean columns:** +``` +true count, false count, null count +true rate +``` + +**Present the profile as a clean summary table**, grouped by column type (dimensions, metrics, dates, IDs). + +### 4. Identify Data Quality Issues + +Apply the quality assessment framework below. Flag potential problems: + +- **High null rates**: Columns with >5% nulls (warn), >20% nulls (alert) +- **Low cardinality surprises**: Columns that should be high-cardinality but aren't (e.g., a "user_id" with only 50 distinct values) +- **High cardinality surprises**: Columns that should be categorical but have too many distinct values +- **Suspicious values**: Negative amounts where only positive expected, future dates in historical data, obviously placeholder values (e.g., "N/A", "TBD", "test", "999999") +- **Duplicate detection**: Check if there's a natural key and whether it has duplicates +- **Distribution skew**: Extremely skewed numeric distributions that could affect averages +- **Encoding issues**: Mixed case in categorical fields, trailing whitespace, inconsistent formats + +### 5. Discover Relationships and Patterns + +After profiling individual columns: + +- **Foreign key candidates**: ID columns that might link to other tables +- **Hierarchies**: Columns that form natural drill-down paths (country > state > city) +- **Correlations**: Numeric columns that move together +- **Derived columns**: Columns that appear to be computed from others +- **Redundant columns**: Columns with identical or near-identical information + +### 6. Suggest Interesting Dimensions and Metrics + +Based on the column profile, recommend: + +- **Best dimension columns** for slicing data (categorical columns with reasonable cardinality, 3-50 values) +- **Key metric columns** for measurement (numeric columns with meaningful distributions) +- **Time columns** suitable for trend analysis +- **Natural groupings** or hierarchies apparent in the data +- **Potential join keys** linking to other tables (ID columns, foreign keys) + +### 7. Recommend Follow-Up Analyses + +Suggest 3-5 specific analyses the user could run next: + +- "Trend analysis on [metric] by [time_column] grouped by [dimension]" +- "Distribution deep-dive on [skewed_column] to understand outliers" +- "Data quality investigation on [problematic_column]" +- "Correlation analysis between [metric_a] and [metric_b]" +- "Cohort analysis using [date_column] and [status_column]" + +## Output Format + +``` +## Data Profile: [table_name] + +### Overview +- Rows: 2,340,891 +- Columns: 23 (8 dimensions, 6 metrics, 4 dates, 5 IDs) +- Date range: 2021-03-15 to 2024-01-22 + +### Column Details +[summary table] + +### Data Quality Issues +[flagged issues with severity] + +### Recommended Explorations +[numbered list of suggested follow-up analyses] +``` + +--- + +## Quality Assessment Framework + +### Completeness Score + +Rate each column: +- **Complete** (>99% non-null): Green +- **Mostly complete** (95-99%): Yellow -- investigate the nulls +- **Incomplete** (80-95%): Orange -- understand why and whether it matters +- **Sparse** (<80%): Red -- may not be usable without imputation + +### Consistency Checks + +Look for: +- **Value format inconsistency**: Same concept represented differently ("USA", "US", "United States", "us") +- **Type inconsistency**: Numbers stored as strings, dates in various formats +- **Referential integrity**: Foreign keys that don't match any parent record +- **Business rule violations**: Negative quantities, end dates before start dates, percentages > 100 +- **Cross-column consistency**: Status = "completed" but completed_at is null + +### Accuracy Indicators + +Red flags that suggest accuracy issues: +- **Placeholder values**: 0, -1, 999999, "N/A", "TBD", "test", "xxx" +- **Default values**: Suspiciously high frequency of a single value +- **Stale data**: Updated_at shows no recent changes in an active system +- **Impossible values**: Ages > 150, dates in the far future, negative durations +- **Round number bias**: All values ending in 0 or 5 (suggests estimation, not measurement) + +### Timeliness Assessment + +- When was the table last updated? +- What is the expected update frequency? +- Is there a lag between event time and load time? +- Are there gaps in the time series? + +## Pattern Discovery Techniques + +### Distribution Analysis + +For numeric columns, characterize the distribution: +- **Normal**: Mean and median are close, bell-shaped +- **Skewed right**: Long tail of high values (common for revenue, session duration) +- **Skewed left**: Long tail of low values (less common) +- **Bimodal**: Two peaks (suggests two distinct populations) +- **Power law**: Few very large values, many small ones (common for user activity) +- **Uniform**: Roughly equal frequency across range (often synthetic or random) + +### Temporal Patterns + +For time series data, look for: +- **Trend**: Sustained upward or downward movement +- **Seasonality**: Repeating patterns (weekly, monthly, quarterly, annual) +- **Day-of-week effects**: Weekday vs. weekend differences +- **Holiday effects**: Drops or spikes around known holidays +- **Change points**: Sudden shifts in level or trend +- **Anomalies**: Individual data points that break the pattern + +### Segmentation Discovery + +Identify natural segments by: +- Finding categorical columns with 3-20 distinct values +- Comparing metric distributions across segment values +- Looking for segments with significantly different behavior +- Testing whether segments are homogeneous or contain sub-segments + +### Correlation Exploration + +Between numeric columns: +- Compute correlation matrix for all metric pairs +- Flag strong correlations (|r| > 0.7) for investigation +- Note: Correlation does not imply causation -- flag this explicitly +- Check for non-linear relationships (e.g., quadratic, logarithmic) + +## Schema Understanding and Documentation + +### Schema Documentation Template + +When documenting a dataset for team use: + +```markdown +## Table: [schema.table_name] + +**Description**: [What this table represents] +**Grain**: [One row per...] +**Primary Key**: [column(s)] +**Row Count**: [approximate, with date] +**Update Frequency**: [real-time / hourly / daily / weekly] +**Owner**: [team or person responsible] + +### Key Columns + +| Column | Type | Description | Example Values | Notes | +|--------|------|-------------|----------------|-------| +| user_id | STRING | Unique user identifier | "usr_abc123" | FK to users.id | +| event_type | STRING | Type of event | "click", "view", "purchase" | 15 distinct values | +| revenue | DECIMAL | Transaction revenue in USD | 29.99, 149.00 | Null for non-purchase events | +| created_at | TIMESTAMP | When the event occurred | 2024-01-15 14:23:01 | Partitioned on this column | + +### Relationships +- Joins to `users` on `user_id` +- Joins to `products` on `product_id` +- Parent of `event_details` (1:many on event_id) + +### Known Issues +- [List any known data quality issues] +- [Note any gotchas for analysts] + +### Common Query Patterns +- [Typical use cases for this table] +``` + +### Schema Exploration Queries + +When connected to a data warehouse, use these patterns to discover schema: + +```sql +-- List all tables in a schema (PostgreSQL) +SELECT table_name, table_type +FROM information_schema.tables +WHERE table_schema = 'public' +ORDER BY table_name; + +-- Column details (PostgreSQL) +SELECT column_name, data_type, is_nullable, column_default +FROM information_schema.columns +WHERE table_name = 'my_table' +ORDER BY ordinal_position; + +-- Table sizes (PostgreSQL) +SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) +FROM pg_catalog.pg_statio_user_tables +ORDER BY pg_total_relation_size(relid) DESC; + +-- Row counts for all tables (general pattern) +-- Run per-table: SELECT COUNT(*) FROM table_name +``` + +### Lineage and Dependencies + +When exploring an unfamiliar data environment: + +1. Start with the "output" tables (what reports or dashboards consume) +2. Trace upstream: What tables feed into them? +3. Identify raw/staging/mart layers +4. Map the transformation chain from raw data to analytical tables +5. Note where data is enriched, filtered, or aggregated + +## Tips + +- For very large tables (100M+ rows), profiling queries use sampling by default -- mention if you need exact counts +- If exploring a new dataset for the first time, this command gives you the lay of the land before writing specific queries +- The quality flags are heuristic -- not every flag is a real problem, but each is worth a quick look + +## Workflow + +1. Establish shape: row count, column count, types, grain (what one row means). +2. Profile quality: nulls, duplicates, suspicious values, range violations per column. +3. Profile distributions: cardinality, top values, outliers for key columns. +4. Note relationships and candidate keys across columns/tables. +5. Summarize findings and which dimensions/metrics merit analysis, flagging quality risks. + +## Anti-patterns + +- Profiling a sample but asserting population-level claims without saying so. +- Aggregating before checking duplicates and grain — double-counting by construction. +- Trusting column names over observed values (a `revenue` column of negative cents). + +## Provenance + +Imported from `anthropics/knowledge-work-plugins` @ `94e1a08` (`data/skills/explore-data/`), Apache-2.0. +Local modifications: trigger phrasing normalized to skill-lib convention; this +Workflow/Anti-patterns/Provenance/hmmm bookend appended. Upstream body above is +otherwise unmodified. See `ATTRIBUTION.md` at repo root. + +hmmm +- Sample-size thresholds for when profiling may subsample large tables are unstated. +- No decision on emitting a machine-readable profile artifact vs prose summary. +- Upstream re-sync cadence with `anthropics/knowledge-work-plugins` is undecided; drift against upstream is currently invisible. diff --git a/.agents/skills/fresh-making/SKILL.md b/.agents/skills/fresh-making/SKILL.md new file mode 100644 index 0000000..ce1be15 --- /dev/null +++ b/.agents/skills/fresh-making/SKILL.md @@ -0,0 +1,427 @@ +--- +name: fresh-making +description: Deterministic restoration of derived-artifact consistency after authoritative inputs change. Load this when deciding whether generated or derived outputs are current; when regenerating MSDMD collections, documentation, projections, package indexes, calibration artifacts, or other outputs from exact source identities; when computing the minimum affected rebuild closure; when designing retryable regeneration across unreliable executors; or when a system needs to make artifacts provably fresh rather than merely recently rebuilt. Do not load for ordinary one-shot builds whose inputs and outputs have no persistent freshness contract. +--- + +# fresh-making — restore derivation consistency + +`fresh-making` is a procedural skill for turning changed authoritative inputs into the smallest verified set of regenerated outputs needed to restore consistency. + +It does not own the source canon, the generator's domain semantics, or the executor. It owns the **freshness decision and restoration discipline** connecting them. + +## Core contract + +```text +fresh != recent +fresh == provably consistent with declared current inputs +``` + +A target is **fresh** only when all of the following are true: + +1. every required input has an exact current identity; +2. the receipt records those same input identities; +3. the generator identity and generation contract match the current declaration; +4. every declared output exists and matches its recorded content digest; +5. the verifier passes against the current output; +6. no required relation is unresolved as `hmmm`. + +A timestamp may aid operations and audit. It never proves freshness. + +## Load this when + +- A source commit, schema, package version, corpus digest, skill, generator, or configuration changes and derived outputs may need regeneration. +- A user asks whether an artifact is stale/current/fresh, or asks to make it fresh. +- Computing which MSDMD collections, organization aggregations, website projections, generated docs, package surfaces, calibration products, or reports must rebuild after a change. +- Building a durable regeneration backend that must survive failed, missing, or unreliable GitHub Actions runs. +- Designing executor-independent retries, leases, receipts, verification, or dependency ordering for derived work. +- A cross-repository work graph has exact identities but still needs a deterministic derivation/freshness layer. + +When the derivation crosses authority boundaries, load `interdependent-work-graph` as well. When designing a continuing automated feedback cycle around make-fresh operations, load `loop-eng` as well. + +## Non-trigger + +Do not load this skill merely because code is being compiled, tests are being run, or a user says "rebuild" once. + +It is unnecessary when: + +- the build has no persistent derived artifact; +- no later consumer needs to know whether an existing artifact remains valid; +- the operation is already completely owned by a repository-local deterministic build with no stored freshness decision; +- the question is only about scheduling or queue implementation rather than derivation correctness. + +Fresh-making may use a scheduler or queue, but it is not queue doctrine. + +## Authority boundary + +Fresh-making never promotes a derived consumer into the authority for its inputs. + +```text +producer authority -> exact input identity + | + v + fresh-making decision + | + v + derived artifact + receipt +``` + +- A UCNS-derived artifact does not become UCNS canon because it was regenerated successfully. +- An organization projection does not become authority for repository-owned MSDMD declarations. +- A backend may trigger an owning repository's generator; it must not silently substitute its own interpretation of that repository's canon. +- Successful regeneration proves derivation consistency under the declared contract, not theorem status, empirical validity, semantic correctness, certification, or publication approval unless those are separately verified by their owning authorities. + +## State model + +Use these target states: + +```text +fresh +making-fresh +blocked +hmmm +``` + +`stale` is a **diagnosis**, not a durable workflow state. It means a known freshness predicate is false and therefore induces make-fresh work. + +Keep execution attempt state separate from target freshness state. A useful attempt state machine is: + +```text +requested +-> ready +-> leased +-> running +-> verifying +-> succeeded + + \-> failed + \-> cancelled + \-> hmmm +``` + +Important distinctions: + +- a target can be not fresh even when no attempt has failed; +- an attempt can fail while the previously published target remains valid for its older declared inputs; +- a successful process exit is not a fresh result until verification and receipt publication succeed; +- unknown identity, ambiguous authority, or unverifiable output is `hmmm`, not fresh and not fabricated failure evidence. + +## Derivation specification + +Every make-fresh target needs a declared derivation specification. The minimal conceptual shape is: + +```json +{ + "schema": "the-interdependency.fresh-making-spec", + "version": "1.0.0", + "target": "org-msdmd", + "inputs": [ + {"name": "ucns", "identity": "git:<40-hex-sha>"}, + {"name": "edcm", "identity": "git:<40-hex-sha>"} + ], + "generator": { + "identity": "git:@<40-hex-sha>", + "command": "python -m ..." + }, + "outputs": [ + {"path": "generated/example.ts"} + ], + "verifier": { + "identity": "git:@<40-hex-sha>|builtin:", + "command": "python ... --check" + }, + "depends_on": [] +} +``` + +The strings are reference shapes, not a universal transport format. A consuming implementation may use structured identities, but it must preserve the same information and version its schema. + +### Input identity rules + +Prefer immutable identities: + +- Git commit SHA for repository state; +- content digest for files, corpora, or sealed artifacts; +- package name + immutable version + artifact digest when package bytes matter; +- schema identifier + version + digest when interpretation depends on a schema; +- explicit `hmmm` when the authoritative identity cannot be resolved. + +Never use `main`, `latest`, filesystem modification time, successful workflow name, or "generated today" as freshness evidence. + +## Freshness key + +A consuming implementation should compute a deterministic **freshness key** from the identity-bearing parts of the derivation specification. + +At minimum the key binds: + +```text +target +ordered/resolved input identities +generator identity + generation contract +verifier identity + verification contract +declared outputs +dependency identities where they affect interpretation +schema version +``` + +Recommended reference algorithm: + +```text +freshness_key_sha256 = SHA256(canonical_json(identity_bearing_spec)) +``` + +Use sorted object keys and an explicitly declared array ordering. Do not include observation timestamps, retry counters, hostnames, temporary paths, executor choice, or other incidental runtime state unless they genuinely change the derivation semantics. + +## Receipt contract + +A successful make-fresh operation emits a receipt. Minimal reference shape: + +```json +{ + "schema": "the-interdependency.fresh-making-receipt", + "version": "1.0.0", + "target": "org-msdmd", + "freshness_key_sha256": "<64-hex>", + "inputs": [ + {"name": "ucns", "identity": "git:<40-hex-sha>"} + ], + "generator": { + "identity": "git:@<40-hex-sha>", + "command": "python -m ..." + }, + "outputs": [ + {"path": "generated/example.ts", "sha256": "<64-hex>"} + ], + "verification": { + "verifier_identity": "git:@<40-hex-sha>|builtin:", + "result": "pass" + }, + "executor": { + "kind": "vm|github-actions|local|other", + "attempt_id": "" + }, + "made_fresh_at": "", + "hmmm": [] +} +``` + +`executor` and `made_fresh_at` are audit fields. They do not determine freshness unless the derivation specification explicitly says executor environment is semantically relevant. + +A receipt is evidence of one derivation result. It is not producer authentication unless separately signed under a declared signature contract. + +## Freshness predicate + +Given a current derivation specification and the latest accepted receipt: + +```text +fresh(target) iff + current freshness key == receipt freshness key + AND every declared output exists + AND every output digest == receipt output digest + AND current verifier passes + AND required dependencies are fresh + AND no required identity/verification relation is hmmm +``` + +If the key differs, the target is diagnosed as stale and enters make-fresh planning. + +If the key matches but output bytes differ, the target is not fresh even if timestamps look new. + +If the output bytes match but the verifier cannot run or its required identity is unknown, return `hmmm` unless another declared verifier contract proves equivalence. + +## Make-fresh workflow + +1. **Resolve authority and identities.** Identify each authoritative input and resolve its exact immutable identity. Cross-repository jobs use `interdependent-work-graph` rather than rediscovering authority ad hoc. +2. **Load the derivation specification.** Resolve target, inputs, generator, outputs, verifier, and dependency edges. +3. **Evaluate freshness.** Recompute the current freshness key, verify output digests, run the verifier where required, and classify the target as `fresh`, known-not-fresh, `blocked`, or `hmmm`. +4. **Compute affected closure.** Starting from changed identities or explicitly requested targets, traverse the derivation graph and select only targets whose freshness predicate can have changed. Include dependencies before consumers. +5. **Minimize work.** Before scheduling generation, re-check each selected target. Do not rebuild a target already proven fresh under the same key. +6. **Create idempotent jobs.** Job identity binds target + freshness key + operation. Repeated requests for the same logical transition converge on one job/result rather than multiplying work. +7. **Lease one attempt.** Exactly one executor owns an active attempt. Record lease expiry/heartbeat so dead workers can be recovered without permanent `running` state. +8. **Execute through an adapter.** Prefer a directly controlled VM/local executor for the reference path when available; GitHub Actions or another hosted runner may be an executor but never the sole source of orchestration truth. +9. **Verify independently of executor success.** Recompute output digests and run the declared verifier. Treat "workflow succeeded" or exit code zero as attempt evidence only. +10. **Publish atomically.** Make verified outputs and their receipt visible as one logical transition, or preserve the previously accepted receipt/output as current for its older key while reporting the new transition failure. +11. **Propagate freshness.** Only after dependencies are verified fresh may dependent targets become candidates for fresh status. +12. **Report closure.** Return what was already fresh, what was made fresh, what failed, what is blocked, and every surviving `hmmm`. + +## Affected-closure algorithm + +The scheduler should be boring: + +```text +changed identities +-> reverse dependency traversal +-> candidate targets +-> topological order +-> freshness re-check +-> enqueue only known-not-fresh targets +-> verify each result +-> unlock consumers +``` + +Do not "rebuild all" merely because invalidation logic is inconvenient. Full rebuild remains an explicit recovery or audit operation, not the normal freshness strategy. + +Cycles in the derivation graph are `blocked` unless the specification declares a bounded fixed-point protocol with an explicit convergence verifier. + +## Durable orchestration contract + +Fresh-making is backend-agnostic, but a durable implementation should preserve: + +- persistent job and attempt identity; +- transactional state transitions; +- lease owner and lease expiry; +- heartbeat or equivalent abandoned-attempt recovery; +- retry count and complete previous-attempt evidence; +- dependency edges; +- desired freshness key; +- accepted receipt identity; +- failure and `hmmm` reason; +- executor adapter and bounded executor-specific metadata. + +SQLite with transactional leases is sufficient for a single-machine or modest-volume reference implementation. Do not introduce a distributed queue merely to simulate scale that does not exist. + +Executor adapters should expose one conceptual interface: + +```text +start(job) -> attempt +observe(attempt) -> running | terminal | hmmm +cancel(attempt) -> result +collect(attempt) -> candidate outputs + execution evidence +``` + +Verification and receipt acceptance remain outside the executor adapter. + +## MSDMD application + +For MSDMD regeneration, a typical dependency chain is: + +```text +repo source identity + -> repo-owned _msdmd.ts + -> organization aggregation + -> website/public projection +``` + +A change to a repository's relevant source invalidates that repository collection and every downstream projection that binds its digest. + +A change to the MSDMD generator contract invalidates every collection whose freshness key binds that generator identity, even when repository source commits did not change. + +Each repository remains authority for its own declarations and generator invocation. A stack-level backend may discover, trigger, retry, verify, and aggregate those operations; it must not silently fabricate repo-owned MSDMD content when the owning regeneration path fails. + +## CLI/output surface + +A consuming control plane may expose commands such as: + +```text +fresh status [target] +fresh make +fresh make --affected +fresh make --all +fresh explain +fresh retry +fresh cancel +``` + +`explain` should answer with evidence, not a boolean alone: + +```text +target: org-msdmd +state: making-fresh +reason: ucns input identity changed +old key: ... +new key: ... +blocked_by: [] +active_attempt: ... +hmmm: [] +``` + +## Validation + +A successful implementation must demonstrate at least these cases: + +1. **No-op:** unchanged identities + matching digests + passing verifier schedule no regeneration. +2. **Input change:** one upstream identity change selects that target and its downstream closure, not unrelated targets. +3. **Generator change:** a generator identity/contract change invalidates all bound targets even when source inputs are unchanged. +4. **Tamper:** matching receipt key but changed output bytes is detected as not fresh. +5. **Dead worker:** an expired lease can be recovered without two active attempts owning the same job. +6. **Executor failure:** one executor may fail and a later adapter may retry without losing earlier attempt evidence. +7. **False green:** executor reports success but verifier fails; no fresh receipt is accepted. +8. **Dependency block:** a failed prerequisite prevents a consumer from being declared fresh. +9. **Unknown identity:** unresolved required input becomes `hmmm`, not a guessed identity and not a fresh result. +10. **Idempotency:** repeated make-fresh requests for the same target/key converge on the same logical job/result. +11. **Minimal closure:** unrelated fresh targets are not rebuilt. +12. **Receipt replay:** a second agent/process can reproduce the freshness decision from the spec, identities, outputs, and receipt without relying on hidden scheduler memory. + +For this skill repository itself, run the normal skill-lib drift and compliance checks after adding or changing the skill. + +## Anti-patterns + +- Using modification time, build time, "latest", or workflow recency as freshness evidence. +- Treating every source change as justification for rebuilding every artifact. +- Regenerating before resolving which exact source identities should govern the result. +- Letting an executor mark work fresh merely because it exited successfully. +- Storing job state only in GitHub Actions, a terminal session, or process memory. +- Running the same attempt simultaneously on several executors as an accidental retry strategy. +- Deleting failed attempt history when retrying. +- Publishing an updated receipt before output verification completes. +- Overwriting the last accepted artifact with an unverified candidate. +- Reconstructing producer-owned metadata in the orchestrator when the producer regeneration path is unavailable. +- Making timestamps part of derivation identity without a semantic reason. +- Calling a target `failed` when no regeneration attempt occurred; diagnose it as known-not-fresh instead. +- Calling a target `fresh` when required evidence is `hmmm`. + +## Output shape when this skill is active + +```markdown +## Freshness target +- target: ... +- current state: fresh | making-fresh | blocked | hmmm +- desired freshness key: ... +- accepted receipt: ... + +## Cause / affected closure +- changed identity: ... +- selected targets in dependency order: ... +- skipped because already fresh: ... + +## Execution +- job/attempt: ... +- executor: ... +- retry/lease state: ... + +## Verification +- output digests: ... +- verifier: pass | fail | hmmm +- resulting receipt: ... + +## hmmm +- ... +``` + +## Relationship to neighboring skills + +```text +interdependent-work-graph + owns: participant identities, authorities, cross-repository relations + +fresh-making + owns: derivation consistency, affected closure, restoration, receipts + +loop-eng + owns: repeated feedback-cycle design and stopping/escalation structure + +repo-audit-repair + owns: broader evidence-led repository defect finding and repair + +msdmd application skills + own: domain-specific declaration/generation contracts +``` + +Fresh-making should consume those contracts rather than absorbing them. + +## hmmm + +- The first production derivation-spec storage location and schema implementation in `stack` are not yet selected. +- Atomic publication semantics differ for local files, Git commits, package registries, and remote publication targets; consuming implementations must make the acceptance boundary explicit rather than pretending one universal filesystem rename solves all cases. +- Cryptographic producer authentication is not provided by content digests or Git identities alone; signed receipts may become a separate contract if threat models require them. +- Fixed-point derivations are intentionally unsupported by the baseline workflow until a concrete bounded convergence case earns the complexity. +- A generator that regenerates itself is either a carefully versioned bootstrap problem or a small machine eating its own instruction manual. Treat it as `hmmm` until the bootstrap boundary is explicit. \ No newline at end of file diff --git a/.agents/skills/gonol-build/SKILL.md b/.agents/skills/gonol-build/SKILL.md new file mode 100644 index 0000000..86747c8 --- /dev/null +++ b/.agents/skills/gonol-build/SKILL.md @@ -0,0 +1,108 @@ +--- +name: gonol-build +description: Construction, closure, and replay contract for gonols across UCNS and EDCM. Load this when building or reviewing UCNS geometry used by gonols, or building EDCM character, word, definition, or recursive-relation gonols. UCNS owns geometry; EDCM owns text construction. The required EDCM order is characters -> words -> definitions -> recursive gonol relations. Pronunciation is not required unless an explicitly declared later experiment makes it part of the construction. Do not load for unrelated geometry, ordinary prose editing, or measurement over already-closed gonols. +--- + +# gonol-build + +Use this skill to keep gonol construction on the declared architecture and nothing else. + +## Workflow + +1. Resolve the current UCNS and EDCM authorities before building. +2. Before launching construction or replay whose completion materially depends on scarce resources, preflight the resources required to finish the declared scope. +3. Keep the EDCM order load-bearing: characters -> words -> definitions -> recursive gonol relations. +4. Close each completed gonol before it participates atomically at the next declared scale. +5. If required UCNS geometry is unresolved, preserve that boundary as `hmmm`. +6. Replay the complete declared scope only where replay is required by the governing protocol. + +## Authority + +```text +UCNS = geometry +EDCM = text-domain gonol construction +skill-lib = construction/replay discipline +``` + +Resolve the current UCNS and EDCM authorities before building. Do not move text semantics into UCNS or invent geometry in EDCM. + +## EDCM construction contract + +```text +characters -> words -> definitions -> recursive gonol relations +``` + +This order is load-bearing. + +- Every admitted character is a gonol. +- Ordered character gonols close into a word gonol. +- A closed word gonol is atomic at the consuming scale while its constituent identities, order, multiplicity, source positions, and provenance remain recoverable. +- Definition gonols are constructed from the applicable closed word gonols and exact source definition evidence. +- Recursive relations are constructed from already-closed gonols without reopening or erasing their internal structure. + +Do not insert another required stage into this sequence unless the governing contract is explicitly changed. + +## Pronunciation boundary + +Pronunciation is not required for this construction. Pronunciation, phonetic spelling, IPA, audio, or other sound representations must not alter gonol identity, closure, ordering, or relations unless a later explicitly declared experiment makes phonology part of its construction. + +Source pronunciation data may remain source metadata. It is not a dependency of the current build. + +## Construction invariant + +At every scale: + +```text +ordered eligible gonols +-> authorized UCNS geometric relation/application +-> closure +-> deterministic identity + provenance receipt +-> atomic participation at the next declared scale +``` + +Preserve exact source identity, occurrence order, multiplicity, and provenance. Do not normalize, deduplicate, infer relations, or substitute tokens, embeddings, hashes, or another representation for gonol identity unless the active contract explicitly authorizes it. + +If required UCNS geometry is unresolved, preserve that boundary as `hmmm`; do not fill it with an invented rule. + +## Candidate boundary + +An unresolved constructor is permission to construct a named, bounded candidate; it does not block declared experimentation. It blocks promotion beyond the evidence, not construction or testing. + +## Completion and replay + +Before launching a construction or replay run whose completion materially depends on scarce resources, preflight the resources required to finish it. If the preflight cannot establish enough resource confidence to finish the declared scope, do not start the compute run; record the unresolved resource boundary as `hmmm` or narrow the declared scope under the governing protocol. Once a healthy admitted run begins, let it reach its natural terminal condition unless a genuine safety/resource boundary or preregistered load-bearing stop condition fires. Do not add arbitrary wall-clock limits. + +A completion claim requires: + +1. exact UCNS, EDCM, source/profile, and constructor identities; +2. the complete declared source scope; +3. deterministic construction receipts; and +4. independent complete replay where replay is required by the governing protocol. + +Replay establishes reproducibility of that construction only. It does not by itself establish semantic quality, measurement validity, cognition claims, or canon outside the declared scope. + +## Usage guidance + +For text construction, start in EDCM and consume current UCNS geometry. + +```text +UCNS: geometry +EDCM: characters -> words -> definitions -> recursive gonol relations +``` + +When a word closes, use that word gonol atomically at the next scale. Ignore pronunciation unless a future explicit construction says otherwise. + +## Anti-patterns + +- Moving text semantics into UCNS or inventing geometry in EDCM. +- Inserting another required EDCM stage without an explicit contract change. +- Letting pronunciation alter gonol identity, closure, ordering, or relations unless a later explicitly declared experiment makes phonology part of its construction. +- Normalizing, deduplicating, inferring relations, or substituting tokens, embeddings, or hashes for gonol identity unless the active contract explicitly authorizes it. +- Adding arbitrary wall-clock limits to a healthy admitted run. +- Treating replay as semantic quality, measurement validity, cognition, or extra-scope canon. + +## hmmm + +- exact UCNS geometric operations that remain unresolved in current implementation; +- any future construction that explicitly adds phonology or another stage; +- any recursive relation whose governing source or geometry is not yet established. diff --git a/.agents/skills/interdependent-work-graph/PORTFOLIO_PLAN.md b/.agents/skills/interdependent-work-graph/PORTFOLIO_PLAN.md new file mode 100644 index 0000000..bc6f8c0 --- /dev/null +++ b/.agents/skills/interdependent-work-graph/PORTFOLIO_PLAN.md @@ -0,0 +1,139 @@ +# Interdependent work graph — portfolio plan projection + +This is the machine-reachable portfolio projection for the existing `interdependent-work-graph` skill. + +## Contract + +Each participating repository remains self-contained and authoritative for its own canon, implementation, evidence, status, and unresolved boundaries. It publishes exactly one repo-owned report at the conventional path: + +```text +docs/work-graphs/repository-plan-report.json +``` + +That report validates against: + +```text +interdependent-work-graph/repository-plan-report.schema.json +schema: the-interdependency.repository-plan-report +version: 1.0.0 +blob SHA: 9b347b2dff7692054b571602f30ee6d00c2e7265 +``` + +The Git blob SHA pins the exact schema bytes independently of branch names or eventual PR merge strategy. The report's cited `source.commit` separately identifies the repository state being described. The commit which adds or refreshes a report is coordination metadata and does not silently become mathematical, semantic, empirical, measurement, runtime, or theorem evidence. + +## Authority rule + +```text +repo owns claim + evidence + status +skill-lib owns reporting contract + deterministic projection +portfolio plan owns no repo canon +``` + +Aggregation never transfers authority. It never upgrades candidate status, proof status, measurement validity, empirical validity, semantic validity, deployment authority, or permissions. + +## Current visibility versus archive history + +A GitHub repository marked **archived** is historical state, not a current portfolio member merely because it remains public or discoverable. + +For any surface claiming to show the **current organization, current projects, current portfolio, current dependency map, or current repository constellation**: + +- filter archived repositories **before** route generation, counts, categories, map nodes, graph-edge projection, portfolio membership, or ordinary visitor navigation; +- do not render an archived repository as a current node with an `archived` badge — absence from the current surface is the default; +- do not allow retained/offline snapshots to reintroduce archived repositories into a current view; +- keep archive status available in source-host metadata and provenance when needed; +- permit an archived repository only when the task explicitly requests history, lineage, migration, reproducibility, or an archive surface, and label that historical scope as such; +- preserve an archived repository as an exact evidence/source participant when a bounded historical work graph explicitly depends on it. Historical evidence is not current portfolio membership. + +This is a **selection boundary before projection**, not a deletion rule. Archiving does not erase commits, provenance, old dependencies, or evidence; it removes the repository from ordinary current-facing organization displays. + +A current-view implementation should therefore satisfy: + +```text +current candidates = discovered repositories - archived repositories +projection(current candidates) -> routes + counts + categories + nodes + current relations +``` + +Filtering only at the final HTML/CSS layer is insufficient because archived repositories would still distort counts, topology, and generated identities. + +## Machine use + +From a workspace containing checked-out repository reports: + +```bash +python interdependent-work-graph/portfolio_plan.py \ + ../a0/docs/work-graphs/repository-plan-report.json \ + ../edcm/docs/work-graphs/repository-plan-report.json \ + ../metapat/docs/work-graphs/repository-plan-report.json \ + ../ucns/docs/work-graphs/repository-plan-report.json \ + ../zfae/docs/work-graphs/repository-plan-report.json \ + docs/work-graphs/repository-plan-report.json \ + --output portfolio-plan.json +``` + +The derived output shape is declared at: + +```text +interdependent-work-graph/portfolio-plan.schema.json +schema: the-interdependency.portfolio-plan +version: 1.0.0 +``` + +The aggregator uses only the Python standard library. It validates the frozen contract identity, rejects duplicate repositories and authority transfer, sorts reports by repository identity, content-addresses every input report, and emits: + +- source identities; +- repo authority and portfolio roles; +- delivered surfaces; +- cross-repository dependencies; +- active frontier; +- next actions; +- blocked work; +- `hmmm`; +- `portfolio_plan_sha256` over the deterministic projected body. + +Local checkout paths are deliberately excluded from the projected body and its digest. Identical report content therefore produces identical portfolio identity regardless of where repositories are checked out. + +The reference aggregator does not query GitHub archive status itself. A caller constructing a **current** report set from repository discovery must apply the archive-selection boundary above before invoking `portfolio_plan.py`. Explicitly supplied historical report sets remain valid because membership is intentional and their scope is historical rather than inferred current state. + +## Staleness + +A report is stale when the repository state of interest no longer matches `source.commit`. An aggregator may expose that mismatch if its execution environment can resolve repository HEAD, but it must not rewrite the report or infer what changed. Refreshing a report is a repo-owned operation. + +## Missing reports + +A portfolio view is complete only for reports actually supplied. A missing repository must not be synthesized from memory, neighboring repositories, package metadata, or a consumer's assumptions. If a known participant lacks a report, record that absence as `hmmm` in the calling workflow. + +An archived repository omitted from a current view is **not** a missing report. It is intentionally outside current membership. Do not turn correct archive exclusion into a `hmmm` completeness warning. + +## Relationship to stack manifests + +`repository-plan-report.json` answers: + +> What part of the larger effort does this repository own, what has it delivered, and what remains live? + +A stack manifest answers: + +> Which exact participants and authority boundaries constitute this particular cross-repository work graph? + +They are complementary. Neither replaces the other. + +An archived repository may still appear in a stack manifest when that bounded task intentionally consumes its exact historical state. That does not make the repository current again. + +## Validation + +For a current organization or portfolio surface, require all of the following before publication: + +- no archived repository appears in the projected current repository set; +- current repository counts equal the post-archive-filter set; +- generated current routes contain no archived repository route; +- current graph nodes and current relation tables have no archived endpoint; +- offline/fallback inputs are subjected to the same archive-selection rule before current projection; +- a historical/archive view, when present, is explicitly named and cannot be mistaken for current state. + +For the deterministic report aggregator itself, retain the existing contract checks: exact report/schema identity, no duplicates, authority non-transfer, deterministic ordering, and machine-local-path exclusion. + +## hmmm + +- Automatic discovery of organization repositories is deliberately not part of v1; an explicit input set avoids silently treating repository visibility as portfolio membership. +- The reference aggregator deliberately does not contact GitHub to determine archive status; current-view callers must resolve and apply that host-state selection before aggregation. +- Cryptographic producer authentication remains separate from content identity. +- A future service may fetch reports directly from GitHub or another registry, but the deterministic local projection remains the reference behavior. diff --git a/.agents/skills/interdependent-work-graph/SKILL.md b/.agents/skills/interdependent-work-graph/SKILL.md new file mode 100644 index 0000000..fc0ad3d --- /dev/null +++ b/.agents/skills/interdependent-work-graph/SKILL.md @@ -0,0 +1,230 @@ +--- +name: interdependent-work-graph +description: Cross-repository coordination for The Interdependency. Load this when a task spans, consumes, compares, publishes to, or can change the contract between two or more repositories; when an agent is about to choose one repo as its workspace for a stack-level problem; when exact producer, evidence-source, skill, semantic, mathematical, or measurement identities must travel together; or when creating a shared stack manifest, multi-repo handoff, coordinated release, or cross-repo validation plan. +--- + +# interdependent-work-graph — coordinate the problem, not the folder + +Use this procedural skill when the real work graph crosses repository boundaries. Repository boundaries preserve authority, provenance, permissions, and release history. They do not define the limit of one agent's attention. + +## Core contract + +```text +repository boundary != agent boundary +repository boundary == authority and provenance boundary +``` + +- Resolve the complete participating graph before choosing where to edit. +- Record exact commits, not only branch names or package availability. +- State what authority each repository or evidence source owns. +- Let one agent coordinate the graph, or let several agents consume the same deterministic graph record. +- Transfer no semantic authority, theorem status, certification status, measurement validity, or empirical status merely because repositories are connected. +- Preserve unresolved mappings and authentication questions as `hmmm`. + +## Non-trigger + +Do not load this skill merely because a repository has ordinary runtime dependencies. A self-contained patch whose correctness, authority, and validation all remain inside one repository does not require a shared work graph. + +Load it when crossing a boundary changes what must be known, preserved, validated, published, or refused. + +## Authority model + +A participant declares an authority role and a work relation. Common examples: + +```text +METAPAT semantic authority +UCNS mathematical representation and its own proof/status evidence +EDCM measurement, projection, and result contracts +skill-lib reusable build and evidence discipline +corpus bounded external source evidence +website publication and presentation consumer +``` + +These are examples, not a universal fixed list. Read the participating repositories before assigning roles. + +## Workflow + +1. **Discover the graph.** Identify every repository, package, corpus, schema, workflow, or publication surface whose exact state can change the answer. +2. **Resolve identity.** Pin an exact commit, immutable artifact digest, versioned schema, or explicit `hmmm` for each participant. Moving branch names are navigation aids, not evidence identities. +3. **Assign authority.** State what each participant may define and what it merely consumes. +4. **Declare relations.** Record producer, consumer, evidence source, build-doctrine source, publication target, compatibility peer, or other precise relation. +5. **Declare non-transfer boundaries.** At minimum consider authority, proof status, certification, measurement validity, empirical validity, and user-data permissions. +6. **Choose edit locations.** Patch each claim at its owning source. Do not repair a producer defect by shadowing its schema in a consumer. +7. **Coordinate execution.** Prefer one shared identity record, fixture, or manifest that all agents and workflows consume over separate repo-local reconstructions. +8. **Validate the graph.** Run repository-local gates plus at least one cross-repository fixture or identity check proving that the connected surfaces remain distinct and compatible. +9. **Publish bounded results.** Each PR describes its local changes and cites the shared graph identity. Do not merge dependent consumers before required producers are available. +10. **Carry hmmm forward.** Unknown semantic mappings, signatures, release ordering, or governance choices remain explicit boundary objects. + +## Stack-manifest reference contract + +The first executable reference shape is: + +```json +{ + "schema": "the-interdependency.stack-manifest", + "version": "1.0.0", + "work_graph_sha256": "", + "repositories": [ + { + "repository": "owner/name", + "commit": "<40-hex commit>", + "authority": "what this participant may define", + "relation": "how it participates in this work" + } + ], + "boundaries": { + "authority_transfer": false, + "proof_status_transfer": false, + "measurement_status_transfer": false, + "semantic_mapping": "external-provenance|declared mapping|hmmm", + "agent_scope": "cross-repository-work-graph", + "hmmm": [] + } +} +``` + +The digest is SHA-256 over canonical JSON containing exactly `repositories` and `boundaries`, sorted by key with compact separators. In version 1.0.0 the order of the `repositories` array is itself part of the hashed identity: an emitter lists participants in a declared, stable order, and the same participants in a different order produce a different digest. Key sorting does not reorder arrays, so two agents rebuilding the same graph must consume the emitter's declared order rather than re-discovering it. Consuming implementations may add versioned fields only through an explicit schema revision. + +The 1.0.0 `boundaries` block is the minimal machine-carried set. Certification-status and empirical-validity non-transfer are binding obligations of this skill (workflow step 5) even where a 1.0.0 manifest carries no explicit fields for them; explicit `certification_status_transfer` and `empirical_status_transfer` fields arrive through the next schema revision, not through ad-hoc emitter extensions. Non-repository participants (corpus, package, schema, workflow, or publication surfaces) are encoded in 1.0.0 as `repositories` entries whose `authority` and `relation` describe the evidence source; typed participant records are likewise deferred to a schema revision. + +A manifest is identity and coordination evidence. It is not cryptographic producer authentication unless a separate signature contract exists. + +## Workflow separation + +For expensive or generated cross-repository work: + +- Pull requests validate read-only whenever possible. +- Materialization is an explicit operation with narrow write permission and a named non-default target branch. +- Generated evidence is sealed once and reused by later agents or workflows rather than independently reconstructed. +- A write-back workflow must not patch source code immediately before claiming to validate that source. +- Repository-specific artifacts include the shared graph identity when their interpretation depends on the graph. + +## Output shape + +When this skill is active, produce or maintain: + +```markdown +## Work graph +- participant: exact identity — authority — relation + +## Edit ownership +- repository/path: change and why it belongs there + +## Cross-repository boundaries +- no-transfer statements +- permission boundaries +- hmmm + +## Validation +- local gates +- shared fixture or manifest check +- release/materialization order +``` + +For machine-consumed work, also emit the versioned stack manifest or an explicitly named equivalent. + +## Repository plan reports and overall portfolio plan + +When the question is not one bounded work graph but **what part of the overall effort each self-contained repository owns**, use the repository-report projection rather than inventing a centralized plan document. + +Each participating repository owns: + +```text +docs/work-graphs/repository-plan-report.json +``` + +That report states its exact source commit, authority and non-transfer boundaries, portfolio role, current claim, delivered surfaces, active frontier, next actions, blockers, cross-repository relations, machine entrypoints, and `hmmm`. + +The frozen input contract is: + +```text +interdependent-work-graph/repository-plan-report.schema.json +schema: the-interdependency.repository-plan-report +version: 1.0.0 +blob SHA: 9b347b2dff7692054b571602f30ee6d00c2e7265 +``` + +The deterministic reference aggregator is: + +```text +interdependent-work-graph/portfolio_plan.py +``` + +Its output contract is: + +```text +interdependent-work-graph/portfolio-plan.schema.json +schema: the-interdependency.portfolio-plan +version: 1.0.0 +``` + +Read `interdependent-work-graph/PORTFOLIO_PLAN.md` before producing or changing this projection. + +The portfolio plan is a **derived index of repo-owned claims**. It owns no repository canon. Missing or stale repo reports remain visible `hmmm`; do not reconstruct them from memory, package metadata, a neighboring repository, or the aggregator itself. Local checkout paths are excluded from the portfolio identity so the same report set hashes identically across machines. + +A repository plan report and a stack manifest are complementary: + +```text +repository plan report -> what this repository owns and where its work stands +stack manifest -> exact identities in one bounded cross-repository work graph +portfolio plan -> deterministic projection across supplied repo reports +``` + +## Validation + +A successful application demonstrates: + +- every participant has an exact or visibly unresolved identity; +- every participant has one stated authority role and work relation; +- no consumer shadows a producer-owned schema or algebra; +- no theorem, semantic, certification, measurement, or empirical status transfers silently; +- the work-graph digest recomputes deterministically; +- repository-local tests pass; +- at least one cross-repository fixture, import, adapter, artifact, or workflow proves the connected path; +- later agents can resume from the graph record without guessing which commits were used. + +For the portfolio projection specifically, also require: + +- every supplied report validates against the frozen report contract; +- every report pins a source commit and the exact schema blob identity; +- duplicate repository reports fail closed; +- every cross-repository relation carries `authority_transfer: false`; +- report order does not change the derived portfolio identity; +- the portfolio digest excludes machine-local checkout paths; +- missing portfolio members are reported as incompletion rather than synthesized. + +## Anti-patterns + +- Assigning one AI to one repository when the task's truth conditions span several. +- Treating the currently open repository as the source of every term it imports. +- Installing “latest” dependencies during evidence-producing runs. +- Reimplementing a producer schema or algebra inside a consumer. +- Letting several agents rebuild incompatible local versions of the same evidence. +- Using a digest as though it were a signature. +- Hiding unresolved mappings behind constructor defaults. +- Allowing validation workflows to mutate the source they are validating. +- Copying repo canon into the portfolio plan instead of deriving a report from the owning repository. +- Treating repository discovery or GitHub visibility as automatic portfolio membership. + +## Minimal example + +A corpus-to-measurement run may bind: + +```text +corpus commit evidence source +skill-lib commit build/evidence doctrine +METAPAT commit semantic authority +UCNS commit mathematical producer +EDCM commit measurement/artifact producer +``` + +One agent can follow the complete path. Separate agents can work on different participants if each consumes the same manifest and respects edit ownership. + +## hmmm + +- The organization-wide persistent service or user interface for live work graphs is not yet selected. +- Content identities do not yet provide cryptographic producer or transport authentication. +- Whether the stack-manifest schema remains a procedural-skill reference contract or later becomes its own metadata-block/schema skill. +- The next stack-manifest schema revision is expected to add explicit `certification_status_transfer` and `empirical_status_transfer` boundary fields, a canonical `repositories` ordering (for example by `repository` then `commit`) in place of declared-order identity, typed non-repository participants with digest/version/schema identities, and an explicit hashed edge list (`from`, `to`, `relation_type`) so distinct work graphs over the same participants cannot share one digest. Version 1.0.0 stays as sealed by the EDCM OEWN 2025 run. +- Cross-repository merge orchestration remains repository-host dependent; the skill defines order and evidence, not a universal transaction mechanism. +- Automatic organization-wide portfolio discovery remains deliberately unselected; explicit report sets preserve intentional membership and visible incompletion. diff --git a/.agents/skills/interdependent-work-graph/portfolio-plan.schema.json b/.agents/skills/interdependent-work-graph/portfolio-plan.schema.json new file mode 100644 index 0000000..8ca7437 --- /dev/null +++ b/.agents/skills/interdependent-work-graph/portfolio-plan.schema.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/The-Interdependency/skill-lib/interdependent-work-graph/portfolio-plan.schema.json", + "title": "The Interdependency portfolio plan", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "version", + "contract", + "generated_from", + "repositories", + "cross_repository_dependencies", + "active_frontier", + "next_actions", + "blocked", + "hmmm", + "portfolio_plan_sha256" + ], + "properties": { + "schema": {"const": "the-interdependency.portfolio-plan"}, + "version": {"const": "1.0.0"}, + "contract": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "report_schema_path", "report_schema_version", "report_schema_blob_sha"], + "properties": { + "repository": {"const": "The-Interdependency/skill-lib"}, + "report_schema_path": {"const": "interdependent-work-graph/repository-plan-report.schema.json"}, + "report_schema_version": {"const": "1.0.0"}, + "report_schema_blob_sha": {"const": "9b347b2dff7692054b571602f30ee6d00c2e7265"} + } + }, + "generated_from": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "source_commit", "report_sha256"], + "properties": { + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "source_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "report_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"} + } + } + }, + "repositories": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "authority", "portfolio_role", "status", "delivered", "machine_entrypoints"], + "properties": { + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "authority": {"type": "object"}, + "portfolio_role": {"type": "object"}, + "status": {"type": "object"}, + "delivered": {"type": "array"}, + "machine_entrypoints": {"type": "object"} + } + } + }, + "cross_repository_dependencies": { + "type": "array", + "items": { + "type": "object", + "required": ["from", "to", "relation", "authority_transfer"], + "properties": { + "from": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "to": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "relation": {"type": "string", "minLength": 1}, + "authority_transfer": {"const": false} + } + } + }, + "active_frontier": {"$ref": "#/$defs/repoItems"}, + "blocked": {"$ref": "#/$defs/repoItems"}, + "hmmm": {"$ref": "#/$defs/repoItems"}, + "next_actions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "action", "owner", "dependency"], + "properties": { + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "action": {"type": "string", "minLength": 1}, + "owner": {"type": "string", "minLength": 1}, + "dependency": {"type": "string"} + } + } + }, + "portfolio_plan_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"} + }, + "$defs": { + "repoItems": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "item"], + "properties": { + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "item": {"type": "string", "minLength": 1} + } + } + } + } +} diff --git a/.agents/skills/interdependent-work-graph/portfolio_plan.py b/.agents/skills/interdependent-work-graph/portfolio_plan.py new file mode 100644 index 0000000..97b8b54 --- /dev/null +++ b/.agents/skills/interdependent-work-graph/portfolio_plan.py @@ -0,0 +1,228 @@ +# ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm +"""Validate repo-owned plan reports and derive one deterministic portfolio plan. + +No network access and no third-party packages are required. Repository reports +remain authoritative for their own claims; this program only validates, +orders, hashes, and projects them into a cross-repository view. + +Usage guidance: + python interdependent-work-graph/portfolio_plan.py \ + ../a0/docs/work-graphs/repository-plan-report.json \ + ../edcm/docs/work-graphs/repository-plan-report.json \ + --output portfolio-plan.json + +Supply only the repositories intentionally included in the portfolio view. +Missing repositories are not auto-discovered or synthesized. A report must pin +the exact frozen report-schema blob and the source commit it describes. +""" + +# === MODULE_BUILD === +# id: interdependent_work_graph_portfolio_plan +# module_name: portfolio_plan +# module_kind: instrument +# summary: validates repo-owned plan reports and derives a deterministic cross-repository portfolio projection without transferring authority +# owner: The-Interdependency/skill-lib maintainers +# public_surface: load_report, build_portfolio, main +# internal_surface: validate_report, canonical_bytes, digest +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: tests/test_interdependent_work_graph_portfolio_plan.py +# rollout: explicit CLI or library invocation after repo reports are supplied +# rollback: remove the aggregator, schemas, companion docs, and portfolio projection section without changing repo-owned source claims +# unresolved: automatic portfolio membership discovery, persistent live service, cryptographic producer authentication +# === END MODULE_BUILD === + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path +from typing import Any + +REPORT_SCHEMA = "the-interdependency.repository-plan-report" +REPORT_VERSION = "1.0.0" +PLAN_SCHEMA = "the-interdependency.portfolio-plan" +PLAN_VERSION = "1.0.0" +CONTRACT_REPOSITORY = "The-Interdependency/skill-lib" +CONTRACT_PATH = "interdependent-work-graph/repository-plan-report.schema.json" +CONTRACT_BLOB_SHA = "9b347b2dff7692054b571602f30ee6d00c2e7265" +COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") + + +def canonical_bytes(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + + +def digest(value: Any) -> str: + return hashlib.sha256(canonical_bytes(value)).hexdigest() + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise ValueError(message) + + +def _string_list(value: Any, field: str) -> list[str]: + _require(isinstance(value, list), f"{field} must be an array") + _require(all(isinstance(item, str) and item for item in value), f"{field} must contain non-empty strings") + return value + + +def validate_report(report: dict[str, Any], source_path: Path) -> None: + _require(report.get("schema") == REPORT_SCHEMA, f"{source_path}: unsupported schema") + _require(report.get("version") == REPORT_VERSION, f"{source_path}: unsupported version") + repository = report.get("repository") + _require(isinstance(repository, str) and repository.count("/") == 1, f"{source_path}: invalid repository") + + contract = report.get("contract") + _require(isinstance(contract, dict), f"{source_path}: contract must be an object") + _require(contract.get("repository") == CONTRACT_REPOSITORY, f"{source_path}: wrong contract repository") + _require(contract.get("path") == CONTRACT_PATH, f"{source_path}: wrong contract path") + _require(contract.get("version") == REPORT_VERSION, f"{source_path}: wrong contract version") + _require(contract.get("blob_sha") == CONTRACT_BLOB_SHA, f"{source_path}: report is not pinned to the frozen contract blob") + + source = report.get("source") + _require(isinstance(source, dict), f"{source_path}: source must be an object") + _require(COMMIT_RE.fullmatch(str(source.get("commit", ""))) is not None, f"{source_path}: source.commit must be 40 lowercase hex characters") + for field in ("branch", "generated_at", "note"): + _require(isinstance(source.get(field), str) and source[field], f"{source_path}: source.{field} is required") + + authority = report.get("authority") + _require(isinstance(authority, dict), f"{source_path}: authority must be an object") + _require(bool(_string_list(authority.get("owns"), "authority.owns")), f"{source_path}: authority.owns may not be empty") + _string_list(authority.get("does_not_own"), "authority.does_not_own") + _require(bool(_string_list(authority.get("non_transfer"), "authority.non_transfer")), f"{source_path}: authority.non_transfer may not be empty") + + portfolio_role = report.get("portfolio_role") + _require(isinstance(portfolio_role, dict), f"{source_path}: portfolio_role must be an object") + _require(isinstance(portfolio_role.get("summary"), str) and portfolio_role["summary"], f"{source_path}: portfolio_role.summary is required") + reports_to = portfolio_role.get("reports_to") + _require(isinstance(reports_to, dict), f"{source_path}: portfolio_role.reports_to must be an object") + _require(reports_to.get("repository") == CONTRACT_REPOSITORY, f"{source_path}: reports_to.repository must be skill-lib") + _require(reports_to.get("skill") == "interdependent-work-graph", f"{source_path}: reports_to.skill must be interdependent-work-graph") + _require(isinstance(reports_to.get("relation"), str) and reports_to["relation"], f"{source_path}: reports_to.relation is required") + + status = report.get("status") + _require(isinstance(status, dict), f"{source_path}: status must be an object") + for field in ("state", "current_claim"): + _require(isinstance(status.get(field), str) and status[field], f"{source_path}: status.{field} is required") + + delivered = report.get("delivered") + _require(isinstance(delivered, list), f"{source_path}: delivered must be an array") + for index, item in enumerate(delivered): + _require(isinstance(item, dict), f"{source_path}: delivered[{index}] must be an object") + for field in ("surface", "status", "boundary"): + _require(isinstance(item.get(field), str) and item[field], f"{source_path}: delivered[{index}].{field} is required") + + _string_list(report.get("active_frontier"), "active_frontier") + _string_list(report.get("blocked"), "blocked") + _string_list(report.get("hmmm"), "hmmm") + + actions = report.get("next_actions") + _require(isinstance(actions, list), f"{source_path}: next_actions must be an array") + for index, action in enumerate(actions): + _require(isinstance(action, dict), f"{source_path}: next_actions[{index}] must be an object") + for field in ("action", "owner", "dependency"): + _require(isinstance(action.get(field), str), f"{source_path}: next_actions[{index}].{field} must be a string") + _require(bool(action["action"] and action["owner"]), f"{source_path}: next_actions[{index}] requires action and owner") + + relations = report.get("cross_repository_relations") + _require(isinstance(relations, list), f"{source_path}: cross_repository_relations must be an array") + for index, relation in enumerate(relations): + _require(isinstance(relation, dict), f"{source_path}: cross_repository_relations[{index}] must be an object") + _require(isinstance(relation.get("repository"), str) and relation["repository"].count("/") == 1, f"{source_path}: invalid relation repository") + _require(isinstance(relation.get("relation"), str) and relation["relation"], f"{source_path}: relation text is required") + _require(relation.get("authority_transfer") is False, f"{source_path}: authority transfer must be false") + + entrypoints = report.get("machine_entrypoints") + _require(isinstance(entrypoints, dict) and entrypoints, f"{source_path}: machine_entrypoints must be a non-empty object") + _require(all(isinstance(k, str) and k and isinstance(v, str) and v for k, v in entrypoints.items()), f"{source_path}: machine_entrypoints keys and values must be non-empty strings") + + +def load_report(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + _require(isinstance(value, dict), f"{path}: report root must be an object") + validate_report(value, path) + return value + + +def build_portfolio(reports_with_paths: list[tuple[Path, dict[str, Any]]]) -> dict[str, Any]: + ordered = sorted(reports_with_paths, key=lambda item: item[1]["repository"]) + repository_names = [report["repository"] for _, report in ordered] + _require(len(repository_names) == len(set(repository_names)), "duplicate repository reports are not allowed") + + generated_from: list[dict[str, Any]] = [] + repository_views: list[dict[str, Any]] = [] + relations: list[dict[str, Any]] = [] + active_frontier: list[dict[str, str]] = [] + next_actions: list[dict[str, Any]] = [] + blocked: list[dict[str, str]] = [] + hmmm: list[dict[str, str]] = [] + + for _, report in ordered: + repository = report["repository"] + generated_from.append({ + "repository": repository, + "source_commit": report["source"]["commit"], + "report_sha256": digest(report), + }) + repository_views.append({ + "repository": repository, + "authority": report["authority"], + "portfolio_role": report["portfolio_role"], + "status": report["status"], + "delivered": report["delivered"], + "machine_entrypoints": report["machine_entrypoints"], + }) + for relation in report["cross_repository_relations"]: + relations.append({"from": repository, "to": relation["repository"], **{k: v for k, v in relation.items() if k != "repository"}}) + active_frontier.extend({"repository": repository, "item": item} for item in report["active_frontier"]) + next_actions.extend({"repository": repository, **action} for action in report["next_actions"]) + blocked.extend({"repository": repository, "item": item} for item in report["blocked"]) + hmmm.extend({"repository": repository, "item": item} for item in report["hmmm"]) + + body = { + "schema": PLAN_SCHEMA, + "version": PLAN_VERSION, + "contract": { + "repository": CONTRACT_REPOSITORY, + "report_schema_path": CONTRACT_PATH, + "report_schema_version": REPORT_VERSION, + "report_schema_blob_sha": CONTRACT_BLOB_SHA, + }, + "generated_from": generated_from, + "repositories": repository_views, + "cross_repository_dependencies": sorted(relations, key=lambda item: (item["from"], item["to"], item["relation"])), + "active_frontier": active_frontier, + "next_actions": next_actions, + "blocked": blocked, + "hmmm": hmmm, + } + return {**body, "portfolio_plan_sha256": digest(body)} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("reports", nargs="+", type=Path, help="repo-owned repository-plan-report.json files") + parser.add_argument("--output", type=Path, help="write JSON to this path instead of stdout") + args = parser.parse_args() + + reports = [(path, load_report(path)) for path in args.reports] + portfolio = build_portfolio(reports) + rendered = json.dumps(portfolio, indent=2, sort_keys=True, ensure_ascii=False) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=hmmm imports_exports=hmmm calls_definitions=hmmm diff --git a/.agents/skills/interdependent-work-graph/repository-plan-report.schema.json b/.agents/skills/interdependent-work-graph/repository-plan-report.schema.json new file mode 100644 index 0000000..9b347b2 --- /dev/null +++ b/.agents/skills/interdependent-work-graph/repository-plan-report.schema.json @@ -0,0 +1,137 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/The-Interdependency/skill-lib/interdependent-work-graph/repository-plan-report.schema.json", + "title": "The Interdependency repository plan report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "version", + "repository", + "contract", + "source", + "authority", + "portfolio_role", + "status", + "delivered", + "active_frontier", + "next_actions", + "blocked", + "cross_repository_relations", + "machine_entrypoints", + "hmmm" + ], + "properties": { + "schema": {"const": "the-interdependency.repository-plan-report"}, + "version": {"const": "1.0.0"}, + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "contract": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "path", "version", "blob_sha"], + "properties": { + "repository": {"const": "The-Interdependency/skill-lib"}, + "path": {"const": "interdependent-work-graph/repository-plan-report.schema.json"}, + "version": {"const": "1.0.0"}, + "blob_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"} + } + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["branch", "commit", "generated_at", "note"], + "properties": { + "branch": {"type": "string", "minLength": 1}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "generated_at": {"type": "string", "format": "date"}, + "note": {"type": "string", "minLength": 1} + } + }, + "authority": { + "type": "object", + "additionalProperties": false, + "required": ["owns", "does_not_own", "non_transfer"], + "properties": { + "owns": {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 1}, + "does_not_own": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "non_transfer": {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 1} + } + }, + "portfolio_role": { + "type": "object", + "additionalProperties": false, + "required": ["summary", "reports_to"], + "properties": { + "summary": {"type": "string", "minLength": 1}, + "upstream_contract": {"type": "string"}, + "downstream_contract": {"type": "string"}, + "reports_to": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "skill", "relation"], + "properties": { + "repository": {"const": "The-Interdependency/skill-lib"}, + "skill": {"const": "interdependent-work-graph"}, + "relation": {"type": "string", "minLength": 1} + } + } + } + }, + "status": { + "type": "object", + "additionalProperties": true, + "required": ["state", "current_claim"], + "properties": { + "state": {"type": "string", "minLength": 1}, + "current_claim": {"type": "string", "minLength": 1} + } + }, + "delivered": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["surface", "status", "boundary"], + "properties": { + "surface": {"type": "string", "minLength": 1}, + "status": {"type": "string", "minLength": 1}, + "boundary": {"type": "string", "minLength": 1} + } + } + }, + "active_frontier": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "next_actions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["action", "owner", "dependency"], + "properties": { + "action": {"type": "string", "minLength": 1}, + "owner": {"type": "string", "minLength": 1}, + "dependency": {"type": "string"} + } + } + }, + "blocked": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "cross_repository_relations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": ["repository", "relation", "authority_transfer"], + "properties": { + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "relation": {"type": "string", "minLength": 1}, + "authority_transfer": {"const": false} + } + } + }, + "machine_entrypoints": { + "type": "object", + "minProperties": 1, + "additionalProperties": {"type": "string", "minLength": 1} + }, + "hmmm": {"type": "array", "items": {"type": "string", "minLength": 1}} + } +} diff --git a/.agents/skills/llms-build/SKILL.md b/.agents/skills/llms-build/SKILL.md new file mode 100644 index 0000000..653b7a1 --- /dev/null +++ b/.agents/skills/llms-build/SKILL.md @@ -0,0 +1,131 @@ +--- +name: llms-build +description: Self-declaring LLM instructions file (llms.txt) built on msdmd. Modules or central files declare LLMS blocks with project overview, key definitions, architecture summary, and agent usage rules. A runner aggregates them into a standardized root llms.txt and surfaces drift/gaps. Load this when creating, updating, or maintaining llms.txt for any repo consumed by LLMs or agents. +--- + +# llms-build — Self-declaring LLM instructions (llms.txt) + +## The doctrine + +Every repo intended for consumption by LLMs or agents should maintain a single root file named exactly `llms.txt`. + +That file locks four things: + +- project overview +- key definitions, using a never-infer-or-expand rule +- architecture summary +- usage rules for agents + +The content of `llms.txt` is declared through msdmd `LLMS` blocks. This keeps instructions version-controlled in the same diff as code changes and makes missing or stale instructions visible as drift. + +The `llms-build` runner walks the tree, parses all `LLMS` blocks, assembles the canonical `llms.txt`, and reports drift between the generated file and the committed file. + +## Block syntax + +```markdown +# === LLMS === +# id: project_overview +# content: One-sentence tagline plus one or two sentences describing the repo. +# +# id: key_definitions +# msdmd: exact definition from msdmd/SKILL.md +# char-compress: exact definition from char-compress/SKILL.md +# any_other_key: exact one-line canonical text +# +# id: architecture_summary +# content: Short bullet-point or table version of the core architecture. +# +# id: usage_rules +# content: Bullet list of rules for LLMs and agents. +# === END LLMS === +``` + +Use the language-appropriate comment marker for the file containing the block. Markdown and Python use `#`; TypeScript, JavaScript, Rust, Go, Java, C, C++, Swift, and Kotlin use `//`; SQL, Lua, and Haskell use `--`. + +Multiple `LLMS` blocks or multiple `id:` entries are allowed and concatenated. The `content` field supports multi-line markdown when continuation lines remain inside the comment block. + +## Required entries + +| id | Required fields | Meaning | +|---|---|---| +| `project_overview` | `content` | One-sentence tagline plus one or two sentences describing the repo. | +| `key_definitions` | one field per key term | Canonical definitions. Never infer or expand these. | +| `architecture_summary` | `content` | Short bullet list or table describing the core architecture, skills, pipeline, or module map. | +| `usage_rules` | `content` | Rules for how LLMs and agents should use the repo. | + +Unknowns in any section are written as `hmmm`, not guessed. + +## The runner protocol + +A compliant `llms-build` runner: + +1. Uses the shared msdmd parser or an equivalent parser that preserves the same block contract. +2. Walks the source tree while skipping the same conventional paths as other msdmd runners. +3. Collects every `LLMS` block entry. +4. Ignores fenced code examples in Markdown so documentation examples do not become declarations. +5. Falls back gracefully when no explicit `LLMS` blocks exist, while writing unresolved values as `hmmm`. +6. Assembles `llms.txt` using the canonical template. +7. Writes or updates `llms.txt` when `--apply` is passed. +8. Reports drift between generated and committed `llms.txt`, and exits non-zero in `--check` mode. + +Reference generator in this repo: + +```bash +python -m llms.build --root . --out llms.txt +python -m llms.build --root . --out llms.txt --apply +python -m llms.build --root . --out llms.txt --check +``` + +## Output template + +The runner produces this shape: + +```markdown +# LLM Instructions for + +## Project Overview +[content from id: project_overview] + +## Key Definitions (never infer or expand these) +- **msdmd** = ... +- **char-compress** = ... +- [any other keys you declared] + +## Architecture Summary +[content from id: architecture_summary] + +## How to Use This Repo with LLMs / Agents +[content from id: usage_rules] + +This file is the single source of truth. If something is not explicitly stated in the files listed above, it does not exist in this repository. +``` + +## Editing doctrine + +- Edit declarations in the source `LLMS` blocks first. +- Run the generator to update `llms.txt`. +- Commit both the block and the generated file in the same change. +- Unknowns in any section are written as `hmmm`, never guessed. +- Definitions in `key_definitions` are canonical source text. Do not infer expansions from acronyms, repo names, or neighboring prose. + +## Anti-patterns + +- Hand-editing `llms.txt` as independent doctrine instead of changing source + `LLMS` blocks and regenerating. +- Letting Markdown examples become declarations; runners must ignore fenced code + examples. +- Expanding acronyms or definitions from model memory when the source block did + not define them. +- Treating missing `LLMS` blocks as proof that no repo instructions exist; + report the gap and preserve `hmmm`. + +## Primary source files for this skill + +- `llms-build/SKILL.md` — canonical spec for the skill. +- `llms/build.py` — stdlib reference runner implementing the command declared above. +- `msdmd/SKILL.md` — parser contract and metadata-block doctrine. +- `msdmd/parsers/universal.py` — shared reference parser whose contract this runner follows. + +See `AGENTS.md` for loading triggers and `skills.json` for registration. + +Last updated: 2026-06-10 diff --git a/.agents/skills/loop-eng/SKILL.md b/.agents/skills/loop-eng/SKILL.md new file mode 100644 index 0000000..7966441 --- /dev/null +++ b/.agents/skills/loop-eng/SKILL.md @@ -0,0 +1,70 @@ +--- +name: loop-eng +description: Loop engineering for designing closed feedback cycles (Discover→Plan→Execute→Verify→Iterate), single-agent and fleet loops with subagent maker/checker separation, and automated verify-iterate workflows. Load this when building or orchestrating agent systems (a0p, AIMMH), EDCMBONE analysis pipelines, repeatable AI workflows, or any The-Interdependency project that benefits from structured loops instead of manual prompting. Cross-load with the-interdependency for org workflow context. +--- + +# loop-eng — Loop Engineering for Agent Workflows + +`loop-eng` is a procedural skill that turns the mindset shift from "prompt engineer" to "loop engineer" into reusable doctrine for The Interdependency. It provides patterns for closed, reliable, structure-preserving feedback cycles that reduce token waste, improve output quality through separation of concerns (maker vs checker), and integrate cleanly with existing org tools (skill-lib, msdmd, a0p, AIMMH, EDCMBONE, canon). + +## Load this when + +- Designing, implementing, or reviewing agent workflows, orchestrations, or feedback systems in a0p, AIMMH, Emergent App, or any The-Interdependency project. +- User requests involve building loops, closed feedback cycles, subagent fleets, verification stages, or moving from one-off prompting to automated iterate-until-verified systems. +- Working on EDCMBONE transcript analysis pipelines, research loops, coding loops, content loops, or any repeatable process that needs Discover→Plan→Execute→Verify→Iterate structure. +- Choosing between single-agent self-improvement loops vs fleet loops (orchestrator + specialists + subagents). +- Any context where the human is currently the manual feedback loop and we want to automate it reliably while preserving structure and epistemic clarity. +- Cross-referenced from `the-interdependency` workflow or `agent-instantiation` / `a0p-instancing`. + +## Core Doctrine + +- **Closed loops are the default**: Bounded, reliable, cheaper, and neurodivergence-compatible. Define clear goal, steps, evaluation criteria, stop condition, and hand-off before opening the loop. Open loops are powerful for exploration but burn tokens and risk drift/flattening; use them only after closed-loop checks are strong. +- **5-stage cycle as the skeleton**: Every loop follows Discover → Plan → Execute → Verify → Iterate. The Verify stage is where quality and structure are enforced (use EDCMBONE lens for transcript/analysis work; custom checkers or subagents for code/docs). +- **6 building blocks must be considered**: + - **Automations**: The heartbeat (scheduled, event-driven, or goal-driven triggers). If a human must manually start every run, the loop is incomplete. + - **Worktrees** (or equivalent isolation): Prevent agent collisions when multiple agents edit the same repo/files in parallel. + - **Skills**: Reusable context (exactly what skill-lib + msdmd provides: VISION, ARCHITECTURE, rules, never-do lists, build/test steps). Every loop should start "warm" with accumulated project knowledge. + - **Plugins & Connectors**: GitHub, Linear, Slack, databases, staging APIs, etc. Turn suggestions into real actions (opened PR, updated ticket, posted update). + - **Subagents**: Maker and checker should rarely be the same model/agent. The agent that wrote the code/article is often too generous in review. Separate exploration, implementation, review, testing, fact-checking, and final summary roles. + - **Memory**: The loop remembers across runs via Markdown files, project logs, GitHub issues, msdmd blocks, or EDCMBONE-structured transcripts. Without memory the loop cold-starts every time. +- **Maker/checker separation is non-negotiable for quality**: The generator is biased toward its own output. Use distinct subagents or models for verification. This aligns with structure-preservation goals (catch flattening, lost variables, epistemic drift). +- **Usage guidance in every loop output**: Every artifact, summary, code change, or analysis produced by a loop must include clear, copy-pasteable usage guidance, examples, integration notes, and limitations. +- **Structure preservation across the loop**: Before any compression, summarization, or decision inside the loop, preserve full relational topology, variables, epistemic status (declared/implemented/inferred/hmmm), and layers. Mark unresolveds explicitly. +- **Integration with org stack**: `loop-eng` works alongside `the-interdependency` (overall workflow), `agent-instantiation`/`a0p-instancing` (orchestration mechanics), `canon` (source-backed decisions), `char-compress` (context handoff), and EDCMBONE (Verify-stage analysis for transcripts and agent outputs). + +## Workflow + +1. **Define the loop contract first** (closed by default): Goal, success criteria, stop condition, hand-off rules, which building blocks are active. +2. **Map to 5 stages**: Explicitly design what happens in Discover, Plan, Execute, Verify (EDCMBONE or subagent checker), and Iterate (fix + re-verify). +3. **Provision the 6 building blocks**: Skills (load relevant skill-lib entries), Memory (project log or msdmd), Subagents (via a0p), Connectors, Automations, isolation strategy. +4. **Run closed first**: Let the loop execute autonomously inside its bounds. Human only intervenes on hand-off or when confidence/stuck threshold is hit. +5. **Capture memory & usage guidance**: Every iteration or final output includes structured memory update + usage guidance section. +6. **Review & evolve**: Use canon skill for any pattern that should become org doctrine. Update the loop contract if drift or new requirements appear. + +## Anti-patterns + +- Treating the human as the permanent manual feedback loop (the old prompting habit). +- Running open/exploratory loops without strong Verify and stop conditions (token burn + drift risk). +- Using the same agent/model for both generation and verification. +- Starting loops cold without Skills/Memory (every run reinvents context). +- Skipping usage guidance in loop outputs or artifacts. +- Flattening structure or dropping variables/relations during any stage of the loop. +- Building loops that cannot be inspected or debugged (no memory, no clear stages). +- Canonizing loop patterns without source backing or testing in closed form first. + +## Output Rubric (when this skill is active) + +- Lead with the loop contract (goal, stages, building blocks in use, closed vs open rationale). +- Show explicit mapping to the 5 stages and how Verify enforces quality/structure (EDCMBONE when applicable). +- Document the 6 building blocks status for this specific loop. +- Include maker/checker separation plan and which subagents/models are used. +- Every output/artifact contains prominent usage guidance + examples. +- Memory updates are structured and reference previous iterations. +- Close with `hmmm` items, next smallest improvement to the loop itself, and any canon proposals. + +hmmm +- Concrete a0p-native patterns for worktree isolation and parallel subagent execution in TIW repos. +- Standardized memory schema (beyond ad-hoc Markdown) that all loops can write/read (possible msdmd block candidate later). +- How tightly to couple `loop-eng` with `the-interdependency` vs keeping them as peer skills that cross-load. +- Whether to add a lightweight metadata-block companion (e.g. `# === LOOP_CONTRACT ===`) for self-declaring loop definitions inside modules. +- Exact thresholds and hand-off protocols for when a closed loop should escalate to human or open exploratory mode. diff --git a/.agents/skills/manifest/SKILL.md b/.agents/skills/manifest/SKILL.md new file mode 100644 index 0000000..4c11611 --- /dev/null +++ b/.agents/skills/manifest/SKILL.md @@ -0,0 +1,134 @@ +--- +name: manifest +description: >- + Living-spec generator. Derives the mechanical, observable facts of a repo + (package name, version, description, license, authors, repository, build + backend, development status, supported Python versions, keywords, runtime + dependencies, optional extras, top-level layout, CI workflows) from + pyproject.toml + the file tree and splices them into a machine-owned, marked + block inside CLAUDE.md — keeping the doc from silently drifting from the code. + Ships a stdlib-only generator with --write (refresh), --check (CI drift gate), + and --print modes. Load this when: setting up or maintaining a CLAUDE.md / + AGENTS.md so its factual half is generated rather than hand-typed; wiring a CI + check that fails when docs drift from pyproject/version/deps/layout; deciding + which parts of a doc to generate vs. hand-author; or onboarding a new org repo + to the living-spec convention. +--- + +# manifest — living spec from source + +`manifest` is the build-as-spec tool for The Interdependency. It treats the +**factual half** of a `CLAUDE.md` like a lockfile: generated from the code, +never hand-edited, and CI-enforced so it can't drift. + +## The line it draws + +- **Generate** what is *observable*: package name, version, description, + license, authors, repository URL, build backend, development status, + supported Python versions, keywords, runtime dependencies, optional extras, + top-level layout, and CI workflow filenames. These are the fields that repeat + across repos and rot silently — the "myriad variables" nobody should type + twice. (Fuzzy facts that can't be read with confidence — e.g. the exact test + command — are deliberately left to hand-authored prose rather than risk + emitting a wrong "fact".) +- **Author** what is *judgement*: why a boundary exists, scope, claim status + (`DEFENDED`/`FRONTIER`), gotchas. The generator never touches these. +- **`hmmm`** is the seam: an unknown fact renders as `hmmm`, a visible gap, not + a guess (per the `msdmd` doctrine this skill is built on). + +Everything generated lives between two markers; everything else in the file is +yours: + +``` + +...derived facts... + +``` + +## Usage + +```bash +# Print the block (no writes) — see what would be generated +python .agents/skills/manifest/generate.py --root . --print + +# Insert or refresh the block in CLAUDE.md +python .agents/skills/manifest/generate.py --root . --write + +# CI drift gate: exit 1 if CLAUDE.md's block is stale or missing +python .agents/skills/manifest/generate.py --root . --check + +# src-layout / non-root pyproject (e.g. edcmbone): +python .agents/skills/manifest/generate.py --pyproject backend/pyproject.toml --write +``` + +Flags: `--root ` (default `.`), `--file ` (default `CLAUDE.md`), +`--pyproject ` (default `pyproject.toml`), and exactly one of +`--write` / `--check` / `--print`. + +## Field requirements + +The required generated fields are the observable repo facts the runner can +derive: package metadata, runtime dependencies, optional extras, top-level +layout, and CI workflow names. Judgement, rationale, test-command guesses, and +doctrine stay hand-authored outside the generated block. Unknown observable +facts render as `hmmm`. + +## Runner contract + +A compliant manifest runner is stdlib-only, deterministic, idempotent, and +non-destructive. It reads `pyproject.toml` plus the file tree, rewrites only the +bytes between the manifest markers, supports `--write`, `--check`, and +`--print`, and exits non-zero when `--check` detects drift. + +## Wiring a repo (the propagation recipe) + +1. Vendor `generate.py` to `.agents/skills/manifest/generate.py` (verbatim copy + from this canonical source). Record the source commit SHA in the local + `.agents/skills/README.md` note, and cite this repo + SHA in the propagation + PR (the PR-citation requirement is in `ORG_DISTRIBUTION.md`). +2. Run `--write` once to insert the block near the top of `CLAUDE.md`. Leave the + markers in place; never hand-edit between them. +3. Add a CI step that runs `--check` (a tiny `manifest-check.yml` workflow, or a + step in the existing one) so a stale block fails the build. +4. **Pin the vendored copy (drift gate).** Vendoring duplicates this file, so + guard the duplication: record its checksum and verify it in CI, so a repo's + copy can never be silently forked. + + ```bash + # at vendor time, from the repo root: + ( cd .agents/skills/manifest && sha256sum generate.py > generate.py.sha256 ) + ``` + ```yaml + # in the CI job, before the --check step: + - name: Vendored generate.py matches skill-lib (no local fork) + run: cd .agents/skills/manifest && sha256sum -c generate.py.sha256 + ``` + + The checksum is the same one `skill-lib@/manifest/generate.py` + produces, so a reviewer can verify the copy is a pristine artifact of that + commit. The only legal way to change a repo's generator is to change it here + and re-vendor (which updates the SHA + checksum together). Catching the *other* + direction — copies falling behind a newer skill-lib — is a skill-lib-side push + concern (mirror/propagation), not the consumer gate. +5. After any change to `pyproject.toml` / version / deps / layout, run `--write` + and commit the refreshed block — the same discipline as a lockfile. + +## Contract & boundaries + +- **Stdlib only.** Uses `tomllib`, so it needs Python 3.11+ to *run*; it is a + dev/CI tool and is never shipped inside a package. +- **Deterministic & idempotent.** `--write` twice is a no-op; that is what makes + `--check` a stable gate. +- **Non-destructive.** It only ever rewrites the bytes between the two markers + (or appends the block once if absent). Hand-authored prose is untouched. +- **Additive scope.** Start with the high-signal/low-noise fields above. New + derived fields are an extension here (bump the block, keep markers stable), not + a per-repo fork — portability depends on one generator. + +## Anti-patterns + +- Hand-editing bytes inside the generated manifest markers. +- Emitting fuzzy or judgement-shaped facts as if they were mechanically derived. +- Forking the vendored generator in a consuming repo instead of changing this + canonical source and re-vendoring. +- Running `--write` in CI when the intended gate is `--check`. diff --git a/.agents/skills/manifest/generate.py b/.agents/skills/manifest/generate.py new file mode 100755 index 0000000..f0a4630 --- /dev/null +++ b/.agents/skills/manifest/generate.py @@ -0,0 +1,303 @@ +# ratios: loc_comments=209:37 imports_exports=6:5 calls_definitions=81:16 +"""manifest — generate the mechanical half of a repo's CLAUDE.md from source. + +Living-spec tool (msdmd family). It derives *observable* facts about a repo — +package metadata (name, version, description, license, authors, repository, +build backend, development status, supported Python versions, keywords, runtime +dependencies, optional extras), plus tree facts (top-level layout, CI +workflows) — directly from `pyproject.toml` and the file tree, and splices them +into a marked, machine-owned block in CLAUDE.md: + + + ...derived facts... + + +Everything *outside* that block stays hand-authored (the judgement: why a +boundary exists, scope, claim status, gotchas). The block is treated like a +lockfile: never hand-edited, regenerated by tooling, and a CI `--check` fails +the build when it drifts from the code. + +It only derives facts it can read with confidence; anything genuinely absent +renders as `hmmm` (a visible gap to fix at the source), never a guess. Fuzzy +inference (e.g. the exact test command) is intentionally left to hand-authored +prose rather than risk emitting a wrong "fact". If the pyproject itself is +missing or unparseable, the tool fails loudly rather than emit an all-`hmmm` +block. + +Stdlib only (msdmd ethos). Requires Python 3.11+ to run because it uses +`tomllib`; it is a dev/CI tool and is never shipped inside any package. + +Usage (exactly one mode is required): + python generate.py --print # show the block, no writes + python generate.py --write # splice/refresh the block + python generate.py --check # exit 1 if the block is stale + python generate.py --root . --file CLAUDE.md --pyproject backend/pyproject.toml --write +""" +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +try: + import tomllib +except ModuleNotFoundError: # Python < 3.11 + tomllib = None + +BEGIN = "" +END = "" +HMMM = "hmmm" # never guess an unknown fact; surface it as a visible gap + +# Directories that are noise rather than structure. +SKIP_DIRS = { + ".git", ".github", "__pycache__", ".mypy_cache", ".ruff_cache", + ".pytest_cache", "node_modules", ".venv", "venv", "dist", "build", + ".idea", ".vscode", ".agents", +} + + +class ManifestError(Exception): + """Raised for unrecoverable conditions (unreadable source, broken markers).""" + + +def _refresh_cmd(args: argparse.Namespace) -> str: + """The exact command to regenerate this block, including non-default args.""" + parts = ["python .agents/skills/manifest/generate.py"] + if args.root != ".": + parts.append(f"--root {args.root}") + if args.file != "CLAUDE.md": + parts.append(f"--file {args.file}") + if args.pyproject != "pyproject.toml": + parts.append(f"--pyproject {args.pyproject}") + parts.append("--write") + return " ".join(parts) + + +def _load_pyproject(path: Path) -> dict: + """Parse pyproject; raise ManifestError if missing or invalid (never silent).""" + if not path.exists(): + raise ManifestError(f"pyproject not found: {path}") + try: + with path.open("rb") as fh: + return tomllib.load(fh) + except (OSError, tomllib.TOMLDecodeError) as exc: + raise ManifestError(f"could not parse {path}: {exc}") from exc + + +def _top_dirs(root: Path) -> list[str]: + out = [] + for p in sorted(root.iterdir()): + if not p.is_dir(): + continue + if p.name in SKIP_DIRS or p.name.startswith(".") or p.name.endswith(".egg-info"): + continue + out.append(p.name + "/") + return out + + +def _ci_workflows(root: Path) -> list[str]: + wf = root / ".github" / "workflows" + if not wf.is_dir(): + return [] + return sorted(p.name for p in wf.iterdir() if p.suffix in (".yml", ".yaml")) + + +def _authors(proj: dict) -> list[str]: + out = [] + for a in proj.get("authors") or []: + name, email = a.get("name"), a.get("email") + if name and email: + out.append(f"{name} <{email}>") + elif name: + out.append(name) + elif email: + out.append(email) + return out + + +def _repository(urls: dict) -> str | None: + low = {k.lower(): v for k, v in urls.items()} + for key in ("repository", "source", "homepage"): + if key in low: + return low[key] + return None + + +def _dev_status(classifiers: list[str]) -> str | None: + for c in classifiers: + if c.startswith("Development Status :: "): + return c.split("Development Status :: ", 1)[1] + return None + + +def _py_versions(classifiers: list[str]) -> list[str]: + out = [] + for c in classifiers: + m = re.match(r"Programming Language :: Python :: (\d+\.\d+)$", c) + if m: + out.append(m.group(1)) + return out + + +def derive_facts(root: Path, pyproject_rel: str) -> dict: + data = _load_pyproject(root / pyproject_rel) + proj = data.get("project", {}) + classifiers = list(proj.get("classifiers") or []) + + lic = proj.get("license") + if isinstance(lic, dict): + lic = lic.get("text") or lic.get("file") or HMMM + + return { + "name": proj.get("name", HMMM), + "version": proj.get("version", HMMM), + "description": proj.get("description", HMMM), + "requires_python": proj.get("requires-python", HMMM), + "license": lic or HMMM, + "authors": _authors(proj), + "repository": _repository(proj.get("urls") or {}), + "build_backend": data.get("build-system", {}).get("build-backend"), + "dev_status": _dev_status(classifiers), + "py_versions": _py_versions(classifiers), + "keywords": list(proj.get("keywords") or []), + "dependencies": list(proj.get("dependencies") or []), + "extras": sorted((proj.get("optional-dependencies") or {}).keys()), + "pyproject": pyproject_rel, + "dirs": _top_dirs(root), + "ci_workflows": _ci_workflows(root), + } + + +def _cell(value: str) -> str: + """Make a value safe for a one-line markdown table cell.""" + return str(value).replace("|", "\\|").replace("\n", " ").strip() + + +def _scalar(value) -> str: + return _cell(value) if value else HMMM + + +def _joined(items: list[str], code: bool = False, empty: str = "none") -> str: + if not items: + return empty + return ", ".join(f"`{_cell(i)}`" for i in items) if code else ", ".join(_cell(i) for i in items) + + +def render(facts: dict, refresh_cmd: str) -> str: + note = ( + f"" + ) + py = _scalar(facts["requires_python"]) + if facts["py_versions"]: + py += f" (classifiers: {', '.join(facts['py_versions'])})" + + rows = [ + ("Package", f"`{_scalar(facts['name'])}`"), + ("Version", f"`{_scalar(facts['version'])}`"), + ("Description", _scalar(facts["description"])), + ("Status", _scalar(facts["dev_status"])), + ("Python", py), + ("License", _scalar(facts["license"])), + ("Build backend", f"`{_cell(facts['build_backend'])}`" if facts["build_backend"] else HMMM), + ("Author(s)", "; ".join(_cell(a) for a in facts["authors"]) or HMMM), + ("Repository", _scalar(facts["repository"])), + ("Runtime dependencies", _joined(facts["dependencies"], code=True, empty="none (stdlib only)")), + ("Optional extras", _joined(facts["extras"], code=True)), + ("Keywords", _joined(facts["keywords"])), + ("CI workflows", _joined(facts["ci_workflows"], code=True)), + ("Top-level directories", " · ".join(f"`{d}`" for d in facts["dirs"]) or HMMM), + ] + + lines = [BEGIN, note, "", "| Field | Value |", "|---|---|"] + lines += [f"| {label} | {value} |" for label, value in rows] + lines += [ + "", + f"Derived from `{facts['pyproject']}` + the repo tree. " + f"Unknown fields surface as `{HMMM}` rather than a guess.", + END, + ] + return "\n".join(lines) + + +def splice(text: str, block: str) -> str: + """Replace an existing manifest block, or append one if none is present. + + Fails loudly on a malformed doc (only one marker, or duplicates) so a broken + block is repaired rather than silently compounded by a second one. + """ + n_begin, n_end = text.count(BEGIN), text.count(END) + if n_begin != n_end or n_begin > 1: + raise ManifestError( + f"malformed manifest markers in document ({n_begin}x BEGIN, {n_end}x END); " + "repair the file so it has exactly one matched pair (or none)." + ) + if n_begin == 1: + pre = text.split(BEGIN, 1)[0] + post = text.split(END, 1)[1] + return pre + block + post + if not text: + return block + "\n" + sep = "" if text.endswith("\n") else "\n" + return text + sep + "\n" + block + "\n" + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--root", default=".", help="repo root (default: .)") + ap.add_argument("--file", default="CLAUDE.md", help="doc to splice, relative to root") + ap.add_argument("--pyproject", default="pyproject.toml", help="pyproject path, relative to root") + mode = ap.add_mutually_exclusive_group(required=True) + mode.add_argument("--write", action="store_true", help="splice/refresh the block in place") + mode.add_argument("--check", action="store_true", help="exit 1 if the block is stale") + mode.add_argument("--print", dest="do_print", action="store_true", help="print the block to stdout") + args = ap.parse_args(argv) + + if tomllib is None: + sys.stderr.write("[manifest] needs Python 3.11+ (tomllib) to read pyproject.toml\n") + return 2 + + root = Path(args.root).resolve() + if not root.is_dir(): + sys.stderr.write(f"[manifest] --root is not a directory: {root}\n") + return 2 + refresh_cmd = _refresh_cmd(args) + try: + block = render(derive_facts(root, args.pyproject), refresh_cmd) + except ManifestError as exc: + sys.stderr.write(f"[manifest] {exc}\n") + return 2 + + if args.do_print: + print(block) + return 0 + + doc = root / args.file + current = doc.read_text(encoding="utf-8") if doc.exists() else "" + try: + updated = splice(current, block) + except ManifestError as exc: + sys.stderr.write(f"[manifest] {exc}\n") + return 2 + + if args.check: + if current != updated: + sys.stderr.write( + f"[manifest] {args.file} manifest block is stale or missing.\n" + f" Run: {refresh_cmd}\n" + ) + return 1 + print(f"[manifest] {args.file} manifest block is up to date.") + return 0 + + doc.write_text(updated, encoding="utf-8") + verb = "refreshed" if BEGIN in current else "inserted" + print(f"[manifest] {verb} manifest block in {args.file}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=209:37 imports_exports=6:5 calls_definitions=81:16 diff --git a/.agents/skills/meta-module-build/SKILL.md b/.agents/skills/meta-module-build/SKILL.md new file mode 100644 index 0000000..e9ca23f --- /dev/null +++ b/.agents/skills/meta-module-build/SKILL.md @@ -0,0 +1,185 @@ +--- +name: meta-module-build +description: Metadata-first module build skill built on msdmd. Use this when turning a capability idea into a bounded module manifest, file plan, public/internal surface, permission boundary, tests, docs, rollout, and rollback notes before implementation. +--- + +GPT generated; context, prompt Erin Spencer + +# meta-module-build — Metadata-first module scaffolding + +`meta-module-build` is an application of [msdmd](../msdmd/SKILL.md). It uses self-declared metadata to keep a proposed module's purpose, surfaces, dependencies, boundaries, tests, and rollout notes visible beside the files that implement it. + +Read `msdmd/SKILL.md` first. This skill inherits the block syntax, parser contract, and visible gap-reporting requirement. + +## Doctrine + +A module build is not an unbounded patch. It is a staged transformation: + +```text +intent -> manifest -> file plan -> tests -> scaffold -> reviewable change +``` + +If a field is not known, write `hmmm`. Do not guess certainty into the manifest. + +## The block + +A module owns its build declaration in a `MODULE_BUILD` block: + +```python +# === MODULE_BUILD === +# id: ucns_object_record +# module_name: object_record +# module_kind: service +# summary: describes a UCNS object without running factorization +# owner: Erin Spencer +# public_surface: object_record, UCNSObjectRecord +# internal_surface: status_for_object, depth_of, stable_hash +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: ucns_recursive.tests.test_object_record +# rollout: default_enabled +# rollback: remove export and call sites +# === END MODULE_BUILD === +``` + +## Field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Unique snake_case identifier, stable across refactors. | +| `module_name` | Human/module name being built. | +| `module_kind` | One of `skill`, `service`, `route`, `adapter`, `engine`, `instrument`, `ui_panel`, `schema`, `migration`, `worker`, `experiment`, or `hmmm`. | +| `summary` | One-sentence purpose. | +| `owner` | Responsible person, role, or agent. | +| `public_surface` | Public exports, routes, commands, or user-visible functions. Use `none` if absent. | +| `internal_surface` | Internal functions/classes/routes touched. Use `none` if absent. | +| `tests` | Test module/path or `hmmm` if not written yet. | +| `rollout` | How the module becomes active. | +| `rollback` | How to disable or remove it cleanly. | + +Boundary fields are required because module generation often crosses hidden lines: + +| Field | Meaning | +|---|---| +| `auth_boundary` | Auth or permission effect: `none`, `read`, `write`, `admin`, or `hmmm`. | +| `storage_boundary` | Persistent storage effect: `none`, `read`, `write`, `migration`, or `hmmm`. | +| `network_boundary` | Network/API effect: `none`, `internal`, `external`, or `hmmm`. | +| `user_data_boundary` | User-data effect: `none`, `read`, `write`, `delete`, or `hmmm`. | +| `admin_only` | `true`, `false`, or `hmmm`. | + +Optional: + +| Field | Meaning | +|---|---| +| `ui_surface` | UI tab/panel/component affected. | +| `api_surface` | API route or RPC surface affected. | +| `data_schema` | Schema name or shape affected. | +| `feature_flag` | Flag or config gate. | +| `requires` | Comma-separated MODULE_BUILD ids this one depends on. | +| `since` | Date/version added. | +| `unresolved` | Comma-separated unresolved items. | + +## File plan rule + +Every implementation PR produced by this skill should include a file plan in the PR body: + +```text +path +created_or_modified +purpose +risk +required_tests +``` + +Do not hide unrelated file edits inside a module build. + +## A0 console metadata rule + +If a module touches the console, route surface, or dynamic UI, the manifest must name the metadata contract it preserves or adds. + +Expected concepts: + +```text +UI_META +DATA_SCHEMA +route namespace +renderer expectation +permission tier +empty state +error state +``` + +If the codebase uses different names, use the codebase names and map them to these concepts in the PR body. + +## UCNS-aware rule + +When a module touches UCNS objects, identity, factorization, or recursive interpretation, prefer the public safe boundary: + +```python +from ucns import a0_safe +``` + +Preferred calls: + +```python +a0_safe.describe(obj) +a0_safe.identity(obj) +a0_safe.canonical(obj) +a0_safe.factor(obj) +``` + +Do not use raw factorization sentinels for A0-facing claims when a scoped envelope exists. + +## Runner behavior + +A `MODULE_BUILD` runner should: + +1. parse all `MODULE_BUILD` blocks using the msdmd universal parser; +2. validate required fields; +3. report boundary fields visibly; +4. report modules without `MODULE_BUILD` as coverage gaps; +5. optionally fail in strict mode when required build metadata is missing; +6. emit a review summary grouped by `module_kind` and boundary risk. + +## Anti-patterns + +- Building code first and writing the manifest after. +- Omitting boundary fields because the module feels small. +- Marking unknowns as solved instead of `hmmm`. +- Using implementation-shaped ids that do not explain the protected capability. +- Adding UI or route behavior without declaring metadata expectations. +- Treating frontier-domain UCNS results as absolute claims. + +## Completion criteria + +A meta-module-build run is complete when it produces either: + +```text +manifest-only PR +``` + +or + +```text +scaffold PR with tests and docs +``` + +It is incomplete if it only produces an idea, a patch with no manifest, or a module with no boundary/test plan. + +## hmmm + +Default unresolved items for new modules: + +```text +exact registry location +feature flag or default activation +admin gate +persistence behavior +UI metadata naming +rollback owner +``` diff --git a/.agents/skills/meta/SKILL.md b/.agents/skills/meta/SKILL.md new file mode 100644 index 0000000..d994e94 --- /dev/null +++ b/.agents/skills/meta/SKILL.md @@ -0,0 +1,146 @@ +--- +name: meta +description: METAPAT consultation router for The Interdependency. Load this when deciding which distinctions, relations, boundaries, transformations, scales, or cross-domain correspondences should organize downstream work; when examining available observations or metrics to determine which questions and projections are worth measuring; when the-interdependency skill's METAPAT consultation gate triggers; when an unresolved conceptual choice would constrain architecture, semantics, measurement, ontology, or later claims; or when explicitly asked to consult, apply, or interpret current METAPAT. Do not load merely for routine implementation under already-fixed conceptual contracts. +--- + +# meta — consult current METAPAT + +This skill does not contain METAPAT doctrine. + +Its purpose is to recognize when METAPAT is required, retrieve the current source, and return the relevant conceptual boundary without creating a competing frozen copy inside `skill-lib`. + +## Source of truth + +Current `The-Interdependency/metapat` outranks this skill on every METAPAT claim. + +When this skill loads, inspect the current repository state before reasoning from METAPAT. Start with the files relevant to the question, normally including: + +- `AXIOMS.md` for root commitments; +- `POSTULATES.md` for revisable working commitments; +- `DOMAIN_RESTRAINT.md` for cross-domain transfer boundaries; +- `THEORIES.md`, `THEOREMS.md`, `CHAPTER_ZERO.md`, or implementation documents when directly relevant. + +Do not substitute historical wording from this repository, memory, another repo, or an older METAPAT commit when current METAPAT is available. + +## Consultation gate + +Consult METAPAT when the work must decide **what relation, distinction, boundary, transformation, scale, or cross-domain correspondence should exist or matter** before downstream implementation can proceed. + +Strong triggers: + +- choosing or revising an architecture-level distinction; +- deciding whether a boundary deserves independent status; +- relating similarly shaped transformations across different domains; +- importing a domain term, formula, metaphor, ontology, or explanatory structure into another layer; +- deciding what remains invariant across scale or representation change; +- separating design choice, aesthetic choice, discovery heuristic, empirical claim, mathematical claim, and implementation dependency when that classification changes architecture; +- an unexplained but productive discovery path is being removed only because its mechanism is not yet known; +- two repositories disagree because they encode different conceptions of the same relation rather than because of an implementation defect; +- deciding whether a simpler independent recovery invalidates, merely verifies, or should replace a richer discovery path; +- examining an object's available observations or metrics to determine which distinctions and projections are actually worth measuring downstream. + +Do not consult METAPAT merely for: + +- routine refactors under fixed contracts; +- dependency or runtime-version updates; +- deterministic ingestion or serialization; +- tests whose expected relation is already declared; +- formatting, packaging, CI, deployment, or syntax repair; +- independent recovery after the discovery result and comparison criterion are already frozen, unless the recovery exposes a new conceptual boundary. + +## Workflow + +1. State the conceptual question that triggered consultation in one sentence. +2. Read current METAPAT source relevant to that question. +3. Distinguish root axiom, postulate, theory, theorem, implementation, example, and `hmmm`; do not transfer status between them. +4. If crossing domains, state what relation or question-form transfers and what does not. +5. Apply only enough METAPAT to resolve the downstream choice. Do not turn consultation into compulsory theory expansion. +6. Return the decision boundary to the calling task, including any unresolved constraint that still matters. +7. When the consultation yields questions whose answers require observation, comparison, or measurement, hand those questions to EDCM as the seed of a measurement design. METAPAT determines what distinctions are worth asking about; EDCM determines how to operationalize and measure them. Do not freeze domain metrics, ratios, thresholds, or instruments into METAPAT merely because they answer a METAPAT-derived question. +8. Continue implementation locally once the conceptual relation is fixed. + +## METAPAT → EDCM measurement bridge + +METAPAT is upstream of measurement selection. It does not choose a metric because the metric is familiar or available; it asks what distinctions, relations, gradients, boundaries, transformations, or scales would make the object legible. + +When those questions are empirically answerable, they become inputs to EDCM. + +```text +thing / domain object + -> METAPAT consultation + -> bounded questions and distinctions + -> EDCM operationalization + -> observables, metrics, ratios, baselines, comparisons, falsifiers + -> domain instrument or implementation +``` + +Rules: + +- Available metrics are evidence about what can be observed, not authority over what matters. +- Prefer the smallest set of questions that preserves the distinctions needed for the downstream decision. +- Redundant questions may remain as derived views, but should not masquerade as independent primitives. +- If an important distinction has no honest observable yet, preserve it as `hmmm`; do not invent a proxy merely to close the measurement surface. +- Domain-specific metric definitions remain downstream. They may influence METAPAT's exploratory tools, but they do not alter METAPAT root authority. + +A coding example is therefore correctly routed as: + +```text +software module + -> METAPAT: what distinctions make its structure legible? + -> questions such as composition, surface utility, graph position + -> EDCM: choose and validate observable measures for those questions + -> ratios: code:comment, consumed:declared, fan-in:fan-out +``` + +The same pattern may seed different EDCMs in other domains without importing coding vocabulary or formulas into METAPAT. + +## Discovery boundary + +METAPAT consultation must not become a requirement that every exploratory architecture justify itself before discovery. + +Interest may select exploration. Discovery may precede explanation. Freeze discoveries before independent recovery. A simpler recovery path tests a result; it does not automatically invalidate the richer path that discovered it. + +Consultation is required when a conceptual commitment would constrain downstream work, not merely because an unusual or complex choice exists. + +## Output + +Keep consultation compact: + +```text +question: +METAPAT standing: +relevant relation: +transfers: +does not transfer: +downstream consequence: +EDCM seed: +hmmm: +``` + +## Validation + +A valid consultation: + +- cites or identifies current METAPAT source rather than remembered doctrine; +- does not promote domain-specific language into METAPAT root authority; +- does not transfer theorem/proof/empirical status across repositories or domains; +- resolves or isolates the conceptual choice that blocked downstream work; +- when measurement is downstream, separates METAPAT's question selection from EDCM's operationalization and domain instrumentation; +- leaves routine implementation outside METAPAT once the boundary is fixed; +- preserves `hmmm` rather than inventing closure. + +## Anti-patterns + +- Duplicating METAPAT doctrine inside skill-lib. +- Consulting METAPAT for every implementation detail. +- Treating an available metric as evidence that its distinction matters. +- Freezing EDCM metrics, ratios, thresholds, or instruments into METAPAT. +- Treating elegance, interest, similarity, or explanatory reach as evidence. +- Treating lack of explanation as evidence that an exploratory choice is invalid. +- Treating independent recovery as proof that the discovery architecture was unnecessary. +- Using a domain's vocabulary to redefine METAPAT because the analogy is convenient. +- Resolving a live conceptual disagreement by silently choosing the more familiar interpretation. + +hmmm + +The calling harness may not support automatic cross-repository retrieval. When current METAPAT cannot be read, report that consultation is required and preserve the unresolved boundary rather than falling back to this skill as theory authority. diff --git a/.agents/skills/msdmd/SKILL.md b/.agents/skills/msdmd/SKILL.md new file mode 100644 index 0000000..15f49d6 --- /dev/null +++ b/.agents/skills/msdmd/SKILL.md @@ -0,0 +1,306 @@ +--- +name: msdmd +description: Module Self-Declared Metadata in Markdown — the foundational convention where each module declares its own structured metadata in a fenced comment block. Other skills in this lib (doc-build, cap-build, deps-build, owner-build, test-build, meta-module-build, risk-boundary-build, ratios, etc.) are thin applications on top of this convention. Load this when authoring a new metadata-driven skill, when extending the block schema, or when building a parser/executor for a new application. +--- + +# msdmd — Module Self-Declared Metadata in Markdown + +## The doctrine + +Every cross-cutting fact a module owns — its behavior obligations, +public documentation, declared capabilities, dependency edges, owner, +runtime boundaries, or executable evidence — should live **in the same +file as the module that owns that fact**, in a structured comment +block. A meta-runner walks the tree, parses every block, and acts on +it. + +Modules without the relevant block surface as visible coverage gaps in +the runner output. Coverage is observable, not implicit. + +This is the inverse of the conventional "keep your docs/tests/configs in +sync with code" approach, which fails because the contract and the +implementation live in different files. Anyone can delete the code and +forget the doc; the lie persists. msdmd makes the lie structurally +visible: when the implementation-owning file disappears, its owned +block disappears in the same diff. + +For tests, ownership is split rather than flattened: source modules own +`CONTRACTS` obligations; test modules own `CHECKS` evidence that +claims to prove those obligations. See +[`test-build/SKILL.md`](../test-build/SKILL.md) and +[`doctrine/msdmd-checks.md`](../doctrine/msdmd-checks.md). + +## Block syntax + +```python +# === === +# id: +# : +# : +# +# id: +# : +# === END === +``` + +### Universal rules + +- **Fence**: `=== ===` opens, `=== END ===` + closes. Block name is uppercase snake_case (e.g. `CONTRACTS`, + `CHECKS`, `DOCS`, `CAPABILITIES`, `OWNERS`). +- **Comment marker**: whatever is idiomatic for the file's language. + `#` for Python / Ruby / Elixir / shell. `//` for TS / JS / Rust / Go / + Java / C / C++ / Swift. `--` for SQL / Lua / Haskell. The marker + appears at the start of every line inside the block. +- **Entry boundary**: every entry begins with `id:`. The id must be + unique within its block and stable across refactors (so it can be + referenced from external tooling). +- **Field lines**: indented one level beneath the id (two spaces of + visible indent inside the comment). Field names are lowercase + snake_case followed by `:` and a value. +- **Multiple blocks per file**: a module may declare more than one + block, of the same or different types. The parser concatenates + entries. +- **Multiple block types per file**: a module may declare both + `CONTRACTS` and `DOCS` (and any others). Each is parsed + independently by its respective application. + +### Example (Python source module) + +```python +# === CONTRACTS === +# id: chat_get_other_owner_404 +# given: GET /api/v1/conversations/{id} with x-user-id != row.user_id +# then: 404 (existence non-disclosure) +# class: security +# === END CONTRACTS === +``` + +### Example (Python test module) + +```python +# === CHECKS === +# id: check_chat_get_other_owner_404_http +# proves: chat_get_other_owner_404 +# call: self::test_chat_get_other_owner_404_http +# requires: python3, posix_shell +# timeout: 20 +# mutates: db +# cleanup: transaction_rollback +# === END CHECKS === +``` + +### Example (TypeScript source module) + +```typescript +// === CONTRACTS === +// id: chat_input_send_disabled_while_pending +// given: a message is in flight +// then: send button is disabled and shows pending state +// class: ux_correctness +// === END CONTRACTS === +``` + +### Example (Elixir) + +```elixir +# === CAPABILITIES === +# id: agent_supervisor_dynamic_spawn +# summary: spawns child agents under a DynamicSupervisor with max_children=cap +# exposes: AgentSupervisor.start_child/1 +# === END CAPABILITIES === +``` + +The block content is identical across languages — only the comment +marker changes. + +## The parser contract + +A msdmd parser is a pure function over file text: + +``` +parse(file_text: str, block_name: str) -> list[Entry] +``` + +where `Entry` is a flat `dict[str, str]` containing at minimum the +`id` field plus whatever fields the entry declared. The parser: + +- Returns all entries from all matching blocks (using + `re.finditer`-style iteration, not just the first block). +- Does not interpret or validate field semantics — that's the + application's job. An entry missing a required field surfaces as an + error in the executor, not in the parser. +- Does not fail on missing block type — returns empty list if no block + of that name exists. + +A reference implementation in pure stdlib Python lives at +`parsers/universal.py`; the TypeScript equivalent at `parsers/universal.ts`. +Both commit to zero non-stdlib dependencies so you can copy them into +any project. + +## Repo collection point and visualizer + +Every consuming repo SHOULD maintain one repo-level collection point named +`_msdmd.ts` (for example, `a0_msdmd.ts`). This file is the +canonical aggregation surface for all parsed msdmd declarations in that +repo. It does not replace module-local blocks; it is generated from them +or maintained as a thin index over them. + +The collection point SHOULD use the shared shapes in `msdmd/collection.ts` +(or a verbatim copy in consuming repos) and export a `MsdmdCollection`: + +```typescript +import { defineMsdmdCollection } from "./.agents/skills/msdmd/collection"; + +export default defineMsdmdCollection({ + repo: "", + declarations: [ + { file: "path/to/module.py", block: "CONTRACTS", id: "...", fields: { summary: "..." } }, + { file: "tests/test_module.py", block: "CHECKS", id: "...", fields: { proves: "..." } }, + ], + gaps: [ + { file: "path/to/module.py", missing: ["CONTRACTS", "DOCS"] }, + ], + edges: [ + { from: "module_a", to: "module_b", kind: "requires", source_block: "DEPENDENCIES", source_id: "..." }, + { from: "check_module_a", to: "module_a_contract", kind: "claims_proves", source_block: "CHECKS", source_id: "..." }, + ], +}); + +export const declarations = []; +export const gaps = []; +``` + +A repo-level msdmd visualizer SHOULD read `_msdmd.ts` and render +relationships between modules using the `MsdmdEdge` shape: +`DEPENDENCIES.requires`, `CAPABILITIES.exposes`, `OWNERS.owner`, +`BOUNDARIES` risk fields, `DOCS.covers`, `CHECKS.call`, +`CHECKS.proves` as `claims_proves`, and any `requires` edges shared +across application skills. The visualizer is a consumer of the +collection point, not a second metadata source. + +If a repo has no collection point or visualizer yet, record that as `hmmm` in +repo-local planning rather than pretending the graph exists. + +A small stdlib generator prototype lives at `msdmd/collect.py`. Consuming repos +can run it directly or copy it as a starting point: + +```bash +python -m msdmd.collect --root . --repo --out _msdmd.ts +``` + +The generator is intentionally conservative: it parses module-local blocks, +emits declarations, optional expected-block gaps, and simple relationship +edges from reserved fields. Repo-specific runners may enrich the output, but +should preserve the `MsdmdCollection` shape. + +A minimal Mermaid visualizer prototype lives at `msdmd/visualize.py` and reads +raw JSON or generated TypeScript collection points: + +```bash +python -m msdmd.visualize _msdmd.ts --out _msdmd.mmd +``` + +The visualizer is deliberately small: it renders declaration nodes, normalized +edge relationships, and visible gap nodes. Rich repo-specific UIs should consume +the same collection shape rather than re-parsing source files. + + +## The runner protocol + +A msdmd runner combines a parser and an executor: + +``` +walk(root: Path, block_name: str) -> Iterator[(file: Path, entries: list[Entry])] +``` + +Implementation rules every runner MUST follow: + +1. **Walk the source tree** under a configurable root, skipping + conventional non-source paths (`__pycache__`, `node_modules`, + `.git`, build outputs, the runner's own test directory). +2. **Detect comment marker by extension**, not by content sniffing. + `.py / .rb / .ex / .sh → #`. `.ts / .js / .tsx / .jsx / .rs / .go / + .java / .c / .cpp / .swift → //`. `.sql / .lua / .hs → --`. +3. **Parse all matching blocks** in each file. Multiple blocks of the + same type concatenate; entries from different blocks are + distinguishable only by id, not by source block. +4. **Visit modules without any block of the requested type** and emit + them as a separate "untested" / "undocumented" / "uncapable" gap + list. Truncate noise (e.g. show first 20, count the rest), but + never silently drop. Visibility is the whole point. +5. **Exit non-zero** when any entry fails the executor's check. The + gap list itself is informational unless the application opts in to + strict mode (in which case missing blocks are also a fail). + +## Field naming conventions + +Reserved field names and their canonical meanings (for cross-skill +consistency): + +| Field | Meaning | +|---|---| +| `id` | Unique stable identifier within the block. Required on every entry. | +| `class` | Free-text tag for grouping (`security`, `correctness`, `idempotency`, etc.). The runner counts entries per class in summaries. | +| `call` | Executable target owned by an evidence/check declaration. Source `CONTRACTS` do not use this field for test topology. | +| `proves` | Comma-separated ids this evidence/check entry claims to prove. The collection edge kind is `claims_proves`; mutation sensitivity is a higher verification rung. | +| `summary` | One-sentence human description. | +| `requires` | Comma-separated dependency ids or host capabilities. Exact semantics are application-specific and must be documented by the skill that consumes it. | +| `owner` | Who is responsible (person, agent role, team). | +| `since` | Version or date this declaration was added. | +| `deprecated` | If present, marks the entry as scheduled for removal. | + +Application-specific fields (`given`, `then`, `expects`, `inputs`, +`outputs`, `mutates`, `cleanup`, `timeout`, etc.) are introduced by +individual SKILLs and documented in their own SKILL.md. + +## Authoring a new msdmd application + +1. **Pick a block name** that doesn't collide with an existing + application. Search the lib README for current names. +2. **Define the field schema** — which fields are required, which + optional, what types they carry. Document in your SKILL.md. +3. **Write the executor** — the function that takes parsed entries + and acts on them. Use the universal parser; do not write a new + one unless your block needs syntax the universal parser can't + express. +4. **Implement the visibility report** — your runner must list + modules without your block type as gaps, and the gap list must + be visible in normal output (not buried behind a flag). +5. **Author a SKILL.md** in this lib with the convention spec, the + executor's behavior, and at least one worked example. + +`test-build/` is the canonical reference application for paired source +`CONTRACTS` and test `CHECKS`. Read its SKILL.md alongside this one to +see the pattern fully realized; read `doc-build/`, `cap-build/`, +`deps-build/`, `owner-build/`, `risk-boundary-build/`, and `ratios/` +for additional applications over the same parser contract. + +## Anti-patterns + +- **Don't define an owned declaration in a detached side file.** The + whole point is that the declaration lives next to the module that + owns that fact. Source obligations belong in source; test evidence + belongs in the test module that owns the evidence. +- **Don't put `call:` in source `CONTRACTS`.** Source modules own + obligations, not test topology. Put executable targets in `CHECKS`. +- **Don't make ids reflect implementation details.** `chat_returns_200` + tells future-you nothing; `chat_get_other_owner_404` tells you what's + protected. Ids are part of the documentation. +- **Don't silently drop modules without blocks.** Coverage gaps must be + visible. If your runner doesn't emit the gap list, it's not a msdmd + runner; it's a test discovery tool with extra steps. +- **Don't introduce parser dialects.** If you need richer syntax than + the universal parser handles, propose an extension to msdmd, not a + fork. The portability of the convention depends on the parser + contract being one thing. + +## Versioning + +- **Block syntax is stable.** Breaking changes (renaming the fence, + changing field-line indentation rules, etc.) go through a major + version bump and a migration note in the lib README. +- **Reserved field names** above are stable. New reserved names are + additive only. +- **Application SKILLs** version independently in their own SKILL.md + files. diff --git a/.agents/skills/msdmd/collect.py b/.agents/skills/msdmd/collect.py new file mode 100644 index 0000000..74b52f6 --- /dev/null +++ b/.agents/skills/msdmd/collect.py @@ -0,0 +1,177 @@ +# ratios: loc_comments=143:7 imports_exports=6:3 calls_definitions=35:6 +"""Generate repo-level msdmd collection-point TypeScript. + +This is a small stdlib helper for consuming repos that want to generate a +`_msdmd.ts` aggregation file from module-local msdmd blocks. +It uses the universal parser and emits data shaped by `msdmd/collection.ts`. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Iterable + +from msdmd.parsers.universal import walk_tree + +DEFAULT_BLOCK_NAMES = ( + "DOCS", + "CAPABILITIES", + "DEPENDENCIES", + "OWNERS", + "CONTRACTS", + "CHECKS", + "MODULE_BUILD", + "BOUNDARIES", + "RATIOS", + "LLMS", + "FRONTEND_META", +) + +EDGE_FIELDS = { + "requires": "requires", + "exposes": "exposes", + "owner": "owns", + "covers": "covers", + "call": "calls", + "proves": "claims_proves", + "boundaries": "risk", +} + + +def _split_targets(value: str) -> list[str]: + return [part.strip() for part in value.split(",") if part.strip()] + + +def _declaration(file: Path, root: Path, block: str, entry: dict) -> dict: + fields = {str(key): str(value) for key, value in entry.items() if key != "id"} + return { + "file": file.relative_to(root).as_posix(), + "block": block, + "id": str(entry["id"]), + "fields": fields, + } + + +def _edges_for(declaration: dict) -> list[dict]: + edges: list[dict] = [] + fields = declaration["fields"] + source = declaration["id"] + for field, kind in EDGE_FIELDS.items(): + value = fields.get(field) + if not value or value == "hmmm": + continue + for target in _split_targets(value): + edges.append( + { + "from": source, + "to": target, + "kind": kind, + "source_block": declaration["block"], + "source_id": source, + } + ) + return edges + + +def collect( + root: Path, + repo: str, + *, + block_names: Iterable[str] = DEFAULT_BLOCK_NAMES, + expected_blocks: Iterable[str] = (), + source_commit: str | None = None, +) -> dict: + """Collect msdmd declarations and optional coverage gaps under ``root``.""" + root = root.resolve() + block_names = tuple(block_names) + expected_blocks = tuple(expected_blocks) + + declarations: list[dict] = [] + missing_by_file: dict[str, set[str]] = {} + + for block in block_names: + annotated, _ = walk_tree(root, block) + for file, entries in annotated: + for entry in entries: + if "id" not in entry: + continue + declarations.append(_declaration(file.resolve(), root, block, entry)) + + for block in expected_blocks: + _, missing_files = walk_tree(root, block) + for file in missing_files: + relative = file.resolve().relative_to(root).as_posix() + missing_by_file.setdefault(relative, set()).add(block) + + declarations.sort(key=lambda item: (item["file"], item["block"], item["id"])) + gaps = [ + {"file": file, "missing": sorted(missing)} + for file, missing in sorted(missing_by_file.items()) + ] + edges = [edge for declaration in declarations for edge in _edges_for(declaration)] + edges.sort(key=lambda item: (item["source_block"], item["source_id"], item["kind"], item["to"])) + + collection = { + "repo": repo, + "declarations": declarations, + "gaps": gaps, + "edges": edges, + } + if source_commit: + collection["source_commit"] = source_commit + return collection + + +def render_typescript(collection: dict, *, import_path: str) -> str: + """Render a collection as a `_msdmd.ts` module.""" + payload = json.dumps(collection, indent=2, sort_keys=True) + return ( + f'import {{ defineMsdmdCollection }} from "{import_path}";\n\n' + f"export default defineMsdmdCollection({payload});\n" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path("."), help="repo root to scan") + parser.add_argument("--repo", required=True, help="repository slug for the collection") + parser.add_argument("--out", type=Path, help="output .ts path; stdout when omitted") + parser.add_argument( + "--block", + action="append", + dest="blocks", + help="block name to collect; may be repeated; defaults to all known blocks", + ) + parser.add_argument( + "--expected-block", + action="append", + default=[], + help="block expected on every source file for gap reporting; may be repeated", + ) + parser.add_argument( + "--import-path", + default="./.agents/skills/msdmd/collection", + help="TypeScript import path for defineMsdmdCollection", + ) + parser.add_argument("--source-commit", help="source commit SHA to record") + args = parser.parse_args() + + collection = collect( + args.root, + args.repo, + block_names=args.blocks or DEFAULT_BLOCK_NAMES, + expected_blocks=args.expected_block, + source_commit=args.source_commit, + ) + rendered = render_typescript(collection, import_path=args.import_path) + if args.out: + args.out.write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=143:7 imports_exports=6:3 calls_definitions=35:6 diff --git a/.agents/skills/msdmd/collection.ts b/.agents/skills/msdmd/collection.ts new file mode 100644 index 0000000..697dca2 --- /dev/null +++ b/.agents/skills/msdmd/collection.ts @@ -0,0 +1,75 @@ +// ratios: loc_comments=67:0 imports_exports=0:0 calls_definitions=1:0 +/** + * Shared TypeScript shapes for repo-level msdmd collection points. + * + * A consuming repo's `_msdmd.ts` file may import or copy these + * types, then export a `MsdmdCollection` generated from module-local msdmd + * blocks. This file is type-only: it does not parse source files or validate + * declarations. + */ +export type MsdmdBlockName = + | "DOCS" + | "CAPABILITIES" + | "DEPENDENCIES" + | "OWNERS" + | "CONTRACTS" + | "CHECKS" + | "MODULE_BUILD" + | "BOUNDARIES" + | "RATIOS" + | "LLMS" + | "FRONTEND_META"; + +export type MsdmdFieldMap = Record; + +export interface MsdmdDeclaration { + /** Repository-relative source file that owns the declaration. */ + file: string; + /** msdmd application block name, such as CONTRACTS, CHECKS, or DOCS. */ + block: MsdmdBlockName; + /** Stable entry id declared inside the block. */ + id: string; + /** Flat parsed fields, excluding id unless a generator intentionally repeats it. */ + fields: MsdmdFieldMap; +} + +export interface MsdmdGap { + /** Repository-relative source file with missing expected block coverage. */ + file: string; + /** Block types expected by local policy but absent from this file. */ + missing: MsdmdBlockName[]; + /** Optional explanation from the collector or policy layer. */ + reason?: string; +} + +export interface MsdmdEdge { + /** Source declaration id or file path. */ + from: string; + /** Target declaration id, capability id, owner, route, file, or external system. */ + to: string; + /** Relationship kind: requires, exposes, owns, covers, calls, claims_proves, risk, etc. */ + kind: string; + /** Block that produced this edge. */ + source_block: MsdmdBlockName; + /** Entry id that produced this edge. */ + source_id: string; +} + +export interface MsdmdCollection { + /** Repository slug, for example a0 or skill-lib. */ + repo: string; + /** Parsed module-local msdmd entries. */ + declarations: MsdmdDeclaration[]; + /** Visible coverage gaps emitted by collectors or local policy. */ + gaps: MsdmdGap[]; + /** Optional normalized relationship graph for visualizers. */ + edges?: MsdmdEdge[]; + /** Optional collector metadata. */ + generated_at?: string; + source_commit?: string; +} + +export function defineMsdmdCollection(collection: MsdmdCollection): MsdmdCollection { + return collection; +} +// ratios: loc_comments=67:0 imports_exports=0:0 calls_definitions=1:0 diff --git a/.agents/skills/msdmd/parsers/__init__.py b/.agents/skills/msdmd/parsers/__init__.py new file mode 100644 index 0000000..13f9bf0 --- /dev/null +++ b/.agents/skills/msdmd/parsers/__init__.py @@ -0,0 +1,7 @@ +# ratios: loc_comments=0:4 imports_exports=0:0 calls_definitions=0:0 +"""msdmd reference parsers. + +`universal` is the canonical Python implementation of the parser +contract defined in ``msdmd/SKILL.md``. Pure stdlib; copy anywhere. +""" +# ratios: loc_comments=0:4 imports_exports=0:0 calls_definitions=0:0 diff --git a/.agents/skills/msdmd/parsers/universal.py b/.agents/skills/msdmd/parsers/universal.py new file mode 100644 index 0000000..7bc86bf --- /dev/null +++ b/.agents/skills/msdmd/parsers/universal.py @@ -0,0 +1,214 @@ +# ratios: loc_comments=128:49 imports_exports=4:7 calls_definitions=51:10 +"""Universal msdmd parser — pure stdlib. + +Implements the parser contract from ``msdmd/SKILL.md``: extracts every +``# === ===`` … ``# === END ===`` block from +a source file and returns its entries as flat dicts. + +Comment marker is auto-detected by file extension. The block syntax +itself is identical across languages; only the per-line marker changes. + +Public API: + + parse_text(text, block_name, marker="#") -> list[dict] + parse_file(path, block_name) -> list[dict] + walk_tree(root, block_name, *, skip=None, extensions=None) -> tuple[annotated, untested] + +RATIOS is the one msdmd declaration that is *not* a fenced block — it is a +single comment line carried on a file's first and last non-blank lines. The +reader for it lives here too, as a sanctioned extension rather than a fork: + + parse_ratios(text, marker="#") -> list[dict] + parse_ratios_file(path) -> list[dict] + ratios_placement(text, marker="#") -> tuple[first_ok, last_ok] + +This module has zero non-stdlib dependencies and is safe to copy +verbatim into any project that wants msdmd support. +""" +from __future__ import annotations +import re +from pathlib import Path +from typing import Iterable + +# extension → comment marker +_MARKERS: dict[str, str] = { + ".py": "#", ".rb": "#", ".ex": "#", ".exs": "#", ".sh": "#", + ".ts": "//", ".tsx": "//", ".js": "//", ".jsx": "//", ".mjs": "//", + ".rs": "//", ".go": "//", ".java": "//", ".c": "//", ".cpp": "//", + ".cc": "//", ".h": "//", ".hpp": "//", ".swift": "//", ".kt": "//", + ".sql": "--", ".lua": "--", ".hs": "--", +} + +_DEFAULT_SKIP = ( + "__pycache__", "node_modules", ".git", ".venv", "venv", + "dist", "build", ".next", ".nuxt", "target", ".pytest_cache", + ".mypy_cache", ".tox", +) + + +def marker_for(path: Path) -> str | None: + """Return the comment marker for a file path, or None if unsupported.""" + return _MARKERS.get(path.suffix.lower()) + + +def _block_regex(block_name: str, marker: str) -> re.Pattern[str]: + m = re.escape(marker) + name = re.escape(block_name) + return re.compile( + rf"^{m} === {name} ===\s*$(?P.*?)^{m} === END {name} ===\s*$", + re.MULTILINE | re.DOTALL, + ) + + +def parse_text(text: str, block_name: str, marker: str = "#") -> list[dict]: + """Extract every entry from every matching block in ``text``. + + Entries are flat ``dict[str, str]`` keyed by field name. The first + line of an entry must be ``id: ``; subsequent lines until + the next ``id:`` (or block end) carry indented ``: `` + pairs. + """ + block_re = _block_regex(block_name, marker) + m = re.escape(marker) + id_re = re.compile(rf"^\s*{m}\s*id:\s*(?P\S+)\s*$") + field_re = re.compile(rf"^\s*{m}\s+(?P[a-z_]+):\s*(?P.+?)\s*$") + + entries: list[dict] = [] + for block in block_re.finditer(text): + current: dict[str, str] | None = None + for line in block.group("body").splitlines(): + line = line.rstrip() + mid = id_re.match(line) + if mid: + if current is not None: + entries.append(current) + current = {"id": mid.group("id")} + continue + if current is None: + continue + mf = field_re.match(line) + if mf: + current[mf.group("key")] = mf.group("val") + if current is not None: + entries.append(current) + return entries + + +def parse_file(path: Path, block_name: str) -> list[dict]: + """Parse a single file. Returns [] if the file's extension has no + known comment marker or if the file can't be read.""" + marker = marker_for(path) + if marker is None: + return [] + try: + return parse_text(path.read_text(encoding="utf-8"), block_name, marker) + except (OSError, UnicodeDecodeError): + return [] + + +def walk_tree( + root: Path, + block_name: str, + *, + skip: Iterable[str] | None = None, + extensions: Iterable[str] | None = None, +) -> tuple[list[tuple[Path, list[dict]]], list[Path]]: + """Walk ``root`` and partition source files into (annotated, untested). + + ``annotated`` is a list of ``(path, entries)`` for every file that + contains at least one entry of ``block_name``. ``untested`` is every + other source file (still filtered by extension and skip-dirs) so + coverage gaps remain observable. + """ + skip_set = set(skip) if skip is not None else set(_DEFAULT_SKIP) + ext_set = ( + set(e.lower() if e.startswith(".") else "." + e.lower() for e in extensions) + if extensions is not None + else set(_MARKERS.keys()) + ) + + def iter_source_files(path: Path) -> Iterable[Path]: + if path.name in skip_set: + return + try: + children = sorted(path.iterdir()) + except OSError: + return + for child in children: + if child.is_dir(): + if child.name in skip_set: + continue + yield from iter_source_files(child) + elif child.is_file() and child.suffix.lower() in ext_set: + yield child + + annotated: list[tuple[Path, list[dict]]] = [] + untested: list[Path] = [] + for path in iter_source_files(root): + entries = parse_file(path, block_name) + if entries: + annotated.append((path, entries)) + else: + untested.append(path) + return annotated, untested + + +# --- RATIOS single-line declaration (msdmd extension) -------------------- +# Unlike every other declaration, RATIOS is not fenced. It is a single +# comment line carrying the three canonical ratios, placed on the file's +# first and last non-blank lines: +# ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M +RATIO_IDS = ("loc_comments", "imports_exports", "calls_definitions") +_RATIOS_TOKEN_RE = re.compile(r"(?P[a-z_]+)=(?P\S+)") + + +def _ratios_line_re(marker: str) -> re.Pattern[str]: + return re.compile(rf"^{re.escape(marker)}\s*ratios:\s*(?P.+?)\s*$") + + +def parse_ratios(text: str, marker: str = "#") -> list[dict]: + """Read single-line RATIOS declarations from ``text``. + + RATIOS is not a fenced block: it is one comment line of the form + `` ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M`` + placed on the file's first and last non-blank lines. Returns one flat + ``{"id", "value"}`` dict per (declaration line x ratio token) so a drift + gate can verify every occurrence. + """ + line_re = _ratios_line_re(marker) + out: list[dict] = [] + for raw in text.splitlines(): + lm = line_re.match(raw.rstrip()) + if not lm: + continue + for tm in _RATIOS_TOKEN_RE.finditer(lm.group("body")): + out.append({"id": tm.group("key"), "value": tm.group("val")}) + return out + + +def parse_ratios_file(path: Path) -> list[dict]: + """``parse_ratios`` for a file path (marker auto-detected); [] on error.""" + marker = marker_for(path) + if marker is None: + return [] + try: + return parse_ratios(path.read_text(encoding="utf-8"), marker) + except (OSError, UnicodeDecodeError): + return [] + + +def ratios_placement(text: str, marker: str = "#") -> tuple[bool, bool]: + """Return ``(first_line_has_ratios, last_non_blank_line_has_ratios)``.""" + line_re = _ratios_line_re(marker) + lines = text.splitlines() + if not lines: + return (False, False) + first_ok = bool(line_re.match(lines[0].rstrip())) + last_ok = False + for raw in reversed(lines): + if raw.strip() == "": + continue + last_ok = bool(line_re.match(raw.rstrip())) + break + return (first_ok, last_ok) +# ratios: loc_comments=128:49 imports_exports=4:7 calls_definitions=51:10 diff --git a/.agents/skills/msdmd/parsers/universal.ts b/.agents/skills/msdmd/parsers/universal.ts new file mode 100644 index 0000000..2db076f --- /dev/null +++ b/.agents/skills/msdmd/parsers/universal.ts @@ -0,0 +1,196 @@ +// ratios: loc_comments=176:0 imports_exports=2:0 calls_definitions=53:0 +/** + * Universal msdmd parser — pure Node stdlib (fs, path). + * + * TypeScript counterpart to parsers/universal.py. Implements the + * parser contract from msdmd/SKILL.md: extracts every + * `// === ===` … `// === END ===` block + * from a source file and returns its entries as flat objects. + * + * Comment marker auto-detected by file extension. The block syntax + * itself is identical across languages; only the per-line marker + * changes. + * + * RATIOS is the one msdmd declaration that is not a fenced block — it is a + * single comment line on a file's first and last non-blank lines. Its reader + * (parseRatios / parseRatiosFile / ratiosPlacement) lives here too, as a + * sanctioned extension rather than a fork. + * + * Zero non-stdlib dependencies. Safe to copy verbatim into any + * Node/Deno/Bun project that wants msdmd support. + */ +import { readFileSync, statSync, readdirSync } from "node:fs"; +import { join, extname } from "node:path"; + +export type Entry = Record; + +const MARKERS: Record = { + ".py": "#", ".rb": "#", ".ex": "#", ".exs": "#", ".sh": "#", + ".ts": "//", ".tsx": "//", ".js": "//", ".jsx": "//", ".mjs": "//", + ".rs": "//", ".go": "//", ".java": "//", ".c": "//", ".cpp": "//", + ".cc": "//", ".h": "//", ".hpp": "//", ".swift": "//", ".kt": "//", + ".sql": "--", ".lua": "--", ".hs": "--", +}; + +const DEFAULT_SKIP = new Set([ + "__pycache__", "node_modules", ".git", ".venv", "venv", + "dist", "build", ".next", ".nuxt", "target", ".pytest_cache", + ".mypy_cache", ".tox", +]); + +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function markerFor(path: string): string | null { + return MARKERS[extname(path).toLowerCase()] ?? null; +} + +export function parseText( + text: string, + blockName: string, + marker: string = "#", +): Entry[] { + const m = escapeRegex(marker); + const name = escapeRegex(blockName); + const blockRe = new RegExp( + `^${m} === ${name} ===\\s*$([\\s\\S]*?)^${m} === END ${name} ===\\s*$`, + "gm", + ); + const idRe = new RegExp(`^\\s*${m}\\s*id:\\s*(\\S+)\\s*$`); + const fieldRe = new RegExp(`^\\s*${m}\\s+([a-z_]+):\\s*(.+?)\\s*$`); + + const entries: Entry[] = []; + let match: RegExpExecArray | null; + while ((match = blockRe.exec(text)) !== null) { + const body = match[1]; + let current: Entry | null = null; + for (const rawLine of body.split("\n")) { + const line = rawLine.replace(/\s+$/, ""); + const mid = idRe.exec(line); + if (mid) { + if (current !== null) entries.push(current); + current = { id: mid[1] }; + continue; + } + if (current === null) continue; + const mf = fieldRe.exec(line); + if (mf) current[mf[1]] = mf[2]; + } + if (current !== null) entries.push(current); + } + return entries; +} + +export function parseFile(path: string, blockName: string): Entry[] { + const marker = markerFor(path); + if (marker === null) return []; + try { + return parseText(readFileSync(path, "utf-8"), blockName, marker); + } catch { + return []; + } +} + +export interface WalkOptions { + skip?: Set; + extensions?: Set; +} + +export function walkTree( + root: string, + blockName: string, + opts: WalkOptions = {}, +): { annotated: Array<[string, Entry[]]>; untested: string[] } { + const skip = opts.skip ?? DEFAULT_SKIP; + const extensions = + opts.extensions ?? new Set(Object.keys(MARKERS)); + + const annotated: Array<[string, Entry[]]> = []; + const untested: string[] = []; + + function visit(dir: string): void { + let names: string[]; + try { + names = readdirSync(dir).sort(); + } catch { + return; + } + for (const name of names) { + if (skip.has(name)) continue; + const full = join(dir, name); + let st; + try { + st = statSync(full); + } catch { + continue; + } + if (st.isDirectory()) { + visit(full); + } else if (st.isFile()) { + if (!extensions.has(extname(full).toLowerCase())) continue; + const entries = parseFile(full, blockName); + if (entries.length > 0) { + annotated.push([full, entries]); + } else { + untested.push(full); + } + } + } + } + + visit(root); + return { annotated, untested }; +} + +// --- RATIOS single-line declaration (msdmd extension) -------------------- +// Unlike every other declaration, RATIOS is not fenced. It is a single +// comment line carrying the three canonical ratios, placed on the file's +// first and last non-blank lines: +// ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M +export const RATIO_IDS = ["loc_comments", "imports_exports", "calls_definitions"] as const; + +function ratiosLineRe(marker: string): RegExp { + return new RegExp(`^${escapeRegex(marker)}\\s*ratios:\\s*(.+?)\\s*$`); +} + +export function parseRatios(text: string, marker: string = "#"): Entry[] { + const lineRe = ratiosLineRe(marker); + const tokenRe = /([a-z_]+)=(\S+)/g; + const out: Entry[] = []; + for (const raw of text.split("\n")) { + const lm = lineRe.exec(raw.replace(/\s+$/, "")); + if (!lm) continue; + let tm: RegExpExecArray | null; + tokenRe.lastIndex = 0; + while ((tm = tokenRe.exec(lm[1])) !== null) { + out.push({ id: tm[1], value: tm[2] }); + } + } + return out; +} + +export function parseRatiosFile(path: string): Entry[] { + const marker = markerFor(path); + if (marker === null) return []; + try { + return parseRatios(readFileSync(path, "utf-8"), marker); + } catch { + return []; + } +} + +export function ratiosPlacement(text: string, marker: string = "#"): [boolean, boolean] { + const lineRe = ratiosLineRe(marker); + const lines = text.split("\n"); + if (lines.length === 0) return [false, false]; + const firstOk = lineRe.test(lines[0].replace(/\s+$/, "")); + let lastOk = false; + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].trim() === "") continue; + lastOk = lineRe.test(lines[i].replace(/\s+$/, "")); + break; + } + return [firstOk, lastOk]; +} +// ratios: loc_comments=176:0 imports_exports=2:0 calls_definitions=53:0 diff --git a/.agents/skills/msdmd/visualize.py b/.agents/skills/msdmd/visualize.py new file mode 100644 index 0000000..7b3665f --- /dev/null +++ b/.agents/skills/msdmd/visualize.py @@ -0,0 +1,209 @@ +# ratios: loc_comments=167:13 imports_exports=5:3 calls_definitions=70:8 +"""Render an msdmd collection as a small Mermaid relationship graph. + +The input may be raw JSON, the generated TypeScript shape emitted by +``msdmd.collect.render_typescript``, or a hand-authored collection point +(unquoted keys, trailing commas, ``//`` comments, single-quoted strings, +and the ``ratios:`` seal after the closing ``});`` all parse). This helper +is intentionally minimal: it visualizes the normalized ``edges`` array from +a ``MsdmdCollection`` and adds gap nodes for visible coverage gaps. +""" +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +_SAFE_NODE_RE = re.compile(r"[^A-Za-z0-9_]") +_IDENT_RE = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*") +_CALL_MARKER = "defineMsdmdCollection(" + + +def _strip_comments(text: str) -> str: + """Remove ``//`` and ``/* */`` comments outside string literals.""" + out: list[str] = [] + i, n = 0, len(text) + quote = "" + while i < n: + ch = text[i] + if quote: + out.append(ch) + if ch == "\\" and i + 1 < n: + out.append(text[i + 1]) + i += 2 + continue + if ch == quote: + quote = "" + i += 1 + continue + if ch in "\"'": + quote = ch + out.append(ch) + i += 1 + continue + if ch == "/" and text[i + 1 : i + 2] == "/": + while i < n and text[i] != "\n": + i += 1 + continue + if ch == "/" and text[i + 1 : i + 2] == "*": + end = text.find("*/", i + 2) + i = n if end < 0 else end + 2 + continue + out.append(ch) + i += 1 + return "".join(out) + + +def _extract_payload(text: str, path: Path) -> str: + """Return the argument of ``defineMsdmdCollection(...)`` in ``text``.""" + start = text.find(_CALL_MARKER) + if start < 0: + raise ValueError(f"{path} is not JSON or a defineMsdmdCollection TypeScript collection point") + i = start + len(_CALL_MARKER) + depth, j, quote = 1, i, "" + while j < len(text): + ch = text[j] + if quote: + if ch == "\\": + j += 2 + continue + if ch == quote: + quote = "" + elif ch in "\"'": + quote = ch + elif ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + return text[i:j] + j += 1 + raise ValueError(f"{path} has an unterminated defineMsdmdCollection call") + + +def _object_literal_to_json(text: str) -> str: + """Convert a comment-free TS/JS object literal to parseable JSON.""" + out: list[str] = [] + i, n = 0, len(text) + while i < n: + ch = text[i] + if ch in "\"'": + buf: list[str] = [] + j = i + 1 + while j < n: + c = text[j] + if c == "\\" and j + 1 < n: + buf.append(c) + buf.append(text[j + 1]) + j += 2 + continue + if c == ch: + j += 1 + break + buf.append(c) + j += 1 + content = "".join(buf) + if ch == "'": + content = content.replace("\\'", "'").replace('"', '\\"') + out.append(f'"{content}"') + i = j + continue + if ch in "}]": + k = len(out) - 1 + while k >= 0 and out[k].isspace(): + k -= 1 + if k >= 0 and out[k] == ",": + del out[k] + out.append(ch) + i += 1 + continue + match = _IDENT_RE.match(text, i) + if match: + ident = match.group(0) + j = match.end() + while j < n and text[j].isspace(): + j += 1 + out.append(f'"{ident}"' if j < n and text[j] == ":" else ident) + i = match.end() + continue + out.append(ch) + i += 1 + return "".join(out) + + +def load_collection(path: Path) -> dict: + """Load a collection from JSON, generated, or hand-authored TypeScript.""" + text = _strip_comments(path.read_text(encoding="utf-8")).strip() + if text.startswith("{"): + return json.loads(text) + + payload = _extract_payload(text, path) + try: + return json.loads(payload) + except json.JSONDecodeError: + return json.loads(_object_literal_to_json(payload)) + + +def _node_id(value: str) -> str: + normalized = _SAFE_NODE_RE.sub("_", value).strip("_") + return normalized or "hmmm" + + +def _label(value: str) -> str: + return value.replace('"', "'") + + +def render_mermaid(collection: dict) -> str: + """Render ``collection`` as Mermaid flowchart text.""" + lines = ["flowchart TD"] + repo = collection.get("repo", "repo") + lines.append(f' repo["{_label(str(repo))}"]') + + emitted_nodes = {"repo"} + for declaration in collection.get("declarations", []): + node = _node_id(str(declaration["id"])) + label = f'{declaration["id"]}\\n{declaration["block"]}\\n{declaration["file"]}' + if node not in emitted_nodes: + lines.append(f' {node}["{_label(label)}"]') + lines.append(f" repo --> {node}") + emitted_nodes.add(node) + + for edge in collection.get("edges", []): + source = _node_id(str(edge["from"])) + target = _node_id(str(edge["to"])) + if source not in emitted_nodes: + lines.append(f' {source}["{_label(str(edge["from"]))}"]') + emitted_nodes.add(source) + if target not in emitted_nodes: + lines.append(f' {target}["{_label(str(edge["to"]))}"]') + emitted_nodes.add(target) + lines.append(f' {source} -- "{_label(str(edge["kind"]))}" --> {target}') + + for index, gap in enumerate(collection.get("gaps", []), start=1): + node = f"gap_{index}" + missing = ", ".join(gap.get("missing", [])) + label = f'{gap.get("file", "hmmm")}\\nmissing: {missing or "hmmm"}' + lines.append(f' {node}[["{_label(label)}"]]') + lines.append(f" repo -. gap .-> {node}") + + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("collection", type=Path, help="collection .json or generated .ts file") + parser.add_argument("--out", type=Path, help="output .mmd path; stdout when omitted") + args = parser.parse_args() + + rendered = render_mermaid(load_collection(args.collection)) + if args.out: + args.out.write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=167:13 imports_exports=5:3 calls_definitions=70:8 diff --git a/.agents/skills/owner-build/SKILL.md b/.agents/skills/owner-build/SKILL.md new file mode 100644 index 0000000..ac8f212 --- /dev/null +++ b/.agents/skills/owner-build/SKILL.md @@ -0,0 +1,93 @@ +--- +name: owner-build +description: Self-declaring module stewardship built on msdmd. Each module declares who owns, reviews, and escalates changes in a `# === OWNERS ===` block; a runner reports unowned modules, unresolved `hmmm` owners, and missing review coverage for sensitive modules. Load this when assigning module ownership, routing reviews, auditing unowned code, or wiring stewardship coverage into CI. +--- + +# owner-build — Module stewardship on msdmd + +`owner-build` is an application of [msdmd](../msdmd/SKILL.md). It records +who is responsible for a module in the same file as the implementation, so +agents do not invent authority or edit sensitive code without a review path. + +Implementation status: this skill defines the `OWNERS` block and runner +contract. This repo does not currently ship an OWNERS runner script; consuming +repos should implement the contract below against their review policy. + +Read `msdmd/SKILL.md` first if you have not. The block syntax, parser +contract, and visible gap rule are inherited. + +## The block + +```python +# === OWNERS === +# id: chat_route_owner +# owner: platform-runtime +# steward: erin +# review_required_for: auth, storage, user_data +# escalation: platform-runtime +# since: 2026-06-04 +# === END OWNERS === +``` + +## Field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Stable ownership declaration id. | +| `owner` | Person, role, team, or `hmmm` if unresolved. | + +Optional: + +| Field | Meaning | +|---|---| +| `steward` | Person or role currently tending the module; use `hmmm` if unresolved. | +| `review_required_for` | Comma-separated change classes requiring review (`auth`, `storage`, `network`, `user_data`, `admin`, `public_api`, `docs`, etc.). | +| `escalation` | Person, role, team, channel, or `hmmm` for unresolved escalation. | +| `backup_owner` | Secondary owner or team. | +| `requires` | Comma-separated ids whose ownership affects this module. | +| `since` | Version or date the owner declaration was added. | +| `deprecated` | If present, marks ownership as scheduled for replacement. | + +## Runner contract + +An OWNERS runner MUST: + +1. Parse every `OWNERS` block with the universal msdmd parser. +2. Report `owner: hmmm`, `steward: hmmm`, or `escalation: hmmm` as pending. +3. Report modules without OWNERS blocks as visible stewardship gaps. +4. Cross-check sensitive modules against `review_required_for` when + BOUNDARIES metadata is available. +5. Exit non-zero for malformed required fields or missing owners in strict + mode. Coverage gaps fail only in strict mode. + +## Agent behavior + +When this skill is loaded before edits: + +- Read OWNERS before making changes. +- If the intended edit touches a class named in `review_required_for`, call + out the review requirement in the handoff or PR summary. +- Do not replace `hmmm` with a guessed person, role, or team. +- If ownership is absent, preserve the gap in output rather than pretending + the committer or agent owns the file. + +## Reporting shape + +- `OWNED`: owner is declared. +- `PENDING`: owner, steward, or escalation is `hmmm`. +- `REVIEW_REQUIRED`: edit class requires explicit review. +- `GAP`: module has no OWNERS block. + +## Anti-patterns + +- Treating Git author, last committer, or PR opener as owner. +- Recording owner only in a central CODEOWNERS-like file while omitting the module-local declaration. +- Using ownership metadata to bypass review; it routes review, not permission. +- Guessing a team from a filename. Unknown is `hmmm`. + +hmmm +- whether repo-level CODEOWNERS should generate suggested OWNERS blocks +- whether strict mode should require owners for all modules or only public/sensitive ones +- how to represent temporary stewardship during incidents diff --git a/.agents/skills/plain-lens/SKILL.md b/.agents/skills/plain-lens/SKILL.md new file mode 100644 index 0000000..a265931 --- /dev/null +++ b/.agents/skills/plain-lens/SKILL.md @@ -0,0 +1,153 @@ +--- +name: plain-lens +description: Building a plain-language, multi-lens companion view of dense canonical text — an easier on-ramp that does not replace or talk down to the source. Load this when you are asked to make an informationally dense document (canon, spec, articles, legal/normative text) easier to approach for newcomers; when building an "explain it through the lens of X" selector (domain, audience, or role); when designing progressive-disclosure or layered ELI-not-stupid reading UX; when a dynamic, data-driven site must keep its existing static page as a graceful fallback; or when you need an EDCM-style two-speaker tension reading between a body text and its footnotes/caveats. Use this when the risk is either drowning readers in density or insulting them with oversimplification. +--- + +# plain-lens — easier on-ramps to dense canon, without talking down + +Some documents are dense on purpose. The Interdependent Way is one: every +clause is load-bearing and the footnotes hold deliberate tension against +the lines they annotate. People bounce off such texts — not because they +are incapable, but because there is no on-ramp. The usual "fix" is a +dumbed-down summary that quietly throws away the load-bearing parts and +makes the reader feel managed. + +This skill is the third path: build a **companion view** that re-expresses +the source in concrete, familiar vocabulary, hands the reader straight +back to the original, and never pretends to be the canon. The paraphrase +is scaffolding; the source is the building. + +## When to load + +Load this skill when any of these are true: + +- You are making a dense document approachable for newcomers or + outsiders without rewriting the canon. +- You are building a selector that re-explains the same material through + a chosen **lens** — a domain of work (agriculture, medicine, + construction, distribution, storage, education, academia, government, + first responders), an audience, or a role. +- You are designing layered / progressive-disclosure reading UX. +- You are adding a dynamic, data-driven page that must degrade to an + existing static page if scripts or fetches fail. +- You need an EDCM-style tension reading between a primary text and its + footnotes, caveats, or dissent treated as a second speaker. + +Do not load this skill to edit the canon itself. Companion views never +become the source of truth. If a paraphrase and the canon disagree, the +canon wins and the paraphrase is wrong. + +## Workflow: the five rules + +1. **On-ramp, not replacement.** The plain reading is the default view, + but the original text and its footnotes are always one interaction + away and never hidden behind the paraphrase. Link back to the full + source from every view. +2. **Concrete over condescending.** Lower the reading load by swapping + abstraction for concrete, domain-native examples — not by removing + substance, hedging, or adding "don't worry" tone. Respect the reader's + intelligence; assume only that they lack *this* context, not capacity. + Keep sentences short and familiar; that helps dyslexic, ADHD, + non-native, and screen-reader readers without insulting anyone. +3. **Preserve the operators.** A paraphrase must carry the source's + negations, quantifiers, conditions, and named obligations intact + ("none", "only", "save those", "tantamount to"). Dropping an operator + is not simplification; it is a different claim. (See `char-compress` + for the bone/flesh discipline this rule borrows.) +4. **One source of truth, generated views.** Keep the explanations in a + structured data file (one entry per article/section, one field per + lens) and render every view from it. Do not hand-maintain parallel + copies that can drift. +5. **Mark what you invented.** A companion view is commentary. Label it + as generated, attribute it (accreditation doctrine), and write `hmmm` + where a mapping is a stretch rather than guessing a clean answer. + +## The lens pattern + +A *lens* is a vocabulary, not a new claim. The same article is re-said in +the working language of a domain the reader already inhabits, so the +structure transfers by analogy. + +- Keep a `general` / plain lens as the default and floor. Every other + lens is a sibling of it, not a replacement. +- Each lens entry is short (1–3 sentences), concrete, and structurally + faithful to the source — same obligation, same exception, same actor. +- The lens menu is a flat, low-commitment selector (chips / buttons), not + a deep navigation tree. Persist the reader's choice. +- A lens may not add an obligation or exception the source does not + contain. If a domain genuinely has no clean mapping, say so plainly + rather than inventing one. + +## Progressive disclosure UX + +Layer the page so the first screen is calm and the depth is opt-in. + +- Surface: the lens menu + the plain/lens reading of each entry. +- One click down: the verbatim original article. +- One click down: the footnotes / caveats. +- One click down: the EDCM-style tension panel. + +Collapse the deeper layers by default; build their contents lazily on +open. The goal is low cognitive load on arrival and full fidelity on +demand — never fidelity *or* approachability, always both, layered. + +## Static fallback discipline + +"Dynamic" must never mean "blank without scripts." A data-driven +companion page must keep a complete, readable static version of at least +the plain reading in the served HTML, plus a `noscript` note and a link +to the full source. The script reveals the interactive view only once its +data is in hand, and on any failure leaves the static fallback visible. +Treat the pre-existing static site as the floor the dynamic layer rests +on, not something it overwrites. + +## EDCM-style two-speaker reading + +When a text carries footnotes, caveats, or dissent, treat the body as one +speaker and the annotations as a second, and report the tension between +them. This is an **EDCM-style heuristic** (Energy–Dissonance Circuit +Model framing), illustrative only — it is not an edcmbone metric runtime, +and you must not claim edcmbone status for it. Compute it transparently +(ship the math next to the output) over families such as: + +- intensity / valence per speaker (caps, terminal punctuation, charged + lexicon); +- constraint mismatch (vocabulary overlap between body and notes); +- drift (cosine distance between body and notes term vectors); +- dissonance (density of negation / tension markers in the notes); +- divergence (topic scatter across multiple notes); +- turn balance (length asymmetry between the two speakers). + +Label the readings as a heuristic, declare what context each used, and +keep the panel behind progressive disclosure — it is for the curious, not +the first screen. + +## Output shape + +A good plain-lens deliverable is: + +- a structured data file: one entry per source unit, with the verbatim + source, its footnotes, a `general` reading, and one field per lens; +- a render layer that builds the lens menu, the cards, and the disclosure + layers from that data; +- a complete static fallback in the served markup; +- an accreditation line and a visible link back to the full canon; +- a `hmmm` wherever a mapping is uncertain. + +## Anti-patterns / things to refuse + +- Do not let the companion view drift from or override the canon. +- Do not drop negations, quantifiers, conditions, or named obligations + in the name of simplicity. +- Do not adopt a reassuring, hand-holding, or "explained for dummies" + register; concreteness is the tool, not condescension. +- Do not present the EDCM-style reading as an edcmbone measurement or + claim unearned theorem/metric status. + +## hmmm + +- The EDCM-style reading here is an illustrative heuristic, not an + edcmbone runtime; precise cross-speaker metric formulas live in + `edcmbone` / `a0`, not in this skill. +- "Without talking down" is a judgement call; this skill gives rules and + registers, not a measurable condescension score. diff --git a/.agents/skills/project-incubation-graduation/SKILL.md b/.agents/skills/project-incubation-graduation/SKILL.md new file mode 100644 index 0000000..c9d7c6b --- /dev/null +++ b/.agents/skills/project-incubation-graduation/SKILL.md @@ -0,0 +1,422 @@ +--- +name: project-incubation-graduation +description: Project incubation and graduation doctrine for emergent components born inside an integration, stack, laboratory, or incubator repository. Load this when several existing projects compose into a new candidate capability; when assessing whether that candidate should remain incubated or become its own repository/package; when extracting it with provenance; when establishing a new implementation authority boundary; when publishing the extracted project through a registry such as PyPI; or when making the former incubator consume the released artifact instead of its local copy. Do not load for an ordinary new repository with no incubation history or for a routine package release whose authority boundary is already established. +--- + +# project-incubation-graduation — let the forge survive its products + +Use this procedural skill when a new thing is born by composing existing things inside a repository that is intentionally allowed to incubate novel integrations. + +The skill governs the transition from **candidate inside a forge** to **independent project with its own implementation and public-contract authority**. It does not decide scientific truth, semantic canon, theorem status, proof status, certification, measurement validity, or empirical validity. + +## Core contract + +```text +composition may create a candidate +candidate != independent project +qualification != authorization to mutate external systems +graduation creates a scoped implementation/public-contract authority transition +stable publication follows compatibility proof for the exact candidate bytes +graduation is complete only after the forge reconsumes the immutable released artifact +``` + +- Incubation is a legitimate ownership state, not architectural debt by definition. +- A candidate remains owned by its forge until graduation completes. +- Extraction alone is not graduation. +- Publishing alone is not graduation. +- A human-readable version is not an immutable artifact identity. +- The former forge must successfully consume the registry/release artifact by immutable identity before graduation is complete. +- Once graduated, the independent repository owns the candidate's implementation and public contract; the forge becomes a consumer. +- No semantic, theorem, proof, certification, measurement, empirical, or domain-validity status transfers merely because implementation authority moves. +- Unknown non-blocking facts remain `hmmm`. Unknown license/distribution rights, release ownership, required authorization, or artifact identity are blocking and may not be promoted through a gate. + +## Non-trigger + +Do not load this skill for: + +- creating an ordinary repository that was independent from inception; +- splitting a repository only for size, permissions, or team organization when no emergent candidate is being graduated; +- publishing a routine new version of an already-independent package; +- vendoring or mirroring code where authority does not change; +- a cross-repository task that only needs coordination; use `interdependent-work-graph` for that. + +When graduation crosses repository boundaries, load `interdependent-work-graph` with this skill and use its exact identity, provenance, relation, and non-transfer discipline. + +## Assessment versus execution + +Assessment is read-only unless the user or owning authority explicitly authorizes mutation. + +```text +assess / stabilize / qualify -> may be read-only +create repository -> explicit authorization required +reserve or publish package name -> explicit authorization required +mutate forge dependency/imports -> explicit authorization required +sever incubated implementation -> explicit authorization required +transfer implementation authority -> explicit authorization required +``` + +A request such as “is this ready to graduate?” does not authorize repository creation, registry publication, consumer mutation, deletion, or authority transfer. Record missing authorization as a blocking `hmmm`; do not infer it from readiness. + +## Lifecycle + +Use these states. Do not skip a state by renaming an unfinished candidate. + +```text +incubating + -> stabilizing + -> qualified + -> extracted + -> released + -> reconsumed + -> graduated +``` + +The `released` gate contains a mandatory pre-publication candidate verification sequence; public stable publication is the end of that gate, not its beginning. + +### `incubating` + +The candidate is legitimately implemented inside the forge. Its API may change. The forge owns implementation authority. + +Require: + +- candidate name or temporary identifier; +- originating composition and participating projects; +- current owning repository/path; +- purpose and scope; +- unresolved `hmmm`. + +### `stabilizing` + +The candidate has a coherent purpose and is being separated from incidental forge structure. + +Require: + +- explicit public surface; +- explicit forge-private surface; +- permitted upstream dependencies; +- forbidden dependencies or cycles; +- provenance of originating inputs and decisions; +- stated compatibility expectations; +- independent purpose that survives removal from the forge. + +### `qualified` + +The candidate has earned extraction readiness. Qualification is still read-only unless execution is separately authorized. + +Require evidence that: + +- its public API is explicit enough to version; +- tests run without importing forge-private modules; +- a clean environment can build/install it; +- permitted upstream dependencies resolve through public contracts; +- no hidden path, checkout, editable-install, environment-variable, or local-fixture dependency is required unless deliberately part of the contract; +- **license and redistribution rights are resolved and permit the intended extraction/distribution**; +- **ownership/release authority is resolved for the intended repository and distribution surface**; +- security and release boundaries are explicit; +- at least one downstream fixture demonstrates intended value independently of source-folder location. + +`license_distribution_rights` and `release_ownership_authority` are hard gates: `hmmm` means **not qualified**. Other `hmmm` may survive only when explicitly non-blocking to the transition being attempted. + +Qualification does **not** transfer authority. The forge remains authoritative until graduation completes. + +### `extracted` + +External mutation begins here, so explicit authorization is required before creating the independent repository. + +Preserve: + +- relevant history when practical; +- exact source commit and source path used for extraction; +- provenance for copied or transformed material; +- license obligations; +- dependency identities; +- unresolved non-blocking issues and `hmmm`; +- a migration note stating that the new repository is not yet graduated. + +The new repository is the intended future authority, not yet the completed authority transition. + +### `released` + +A stable public release must not be the first forge-compatibility test. + +Required sequence: + +1. Build the release candidate from the independent repository. +2. Record an immutable artifact identity: SHA-256, registry-provided immutable digest, or equivalent byte identity. A version/tag alone is insufficient. +3. Install **that exact candidate artifact** in a clean environment and run project tests. +4. Install **that same exact candidate artifact** in the forge and run the relevant integration suite before stable publication. +5. If compatibility fails, do not publish it as a stable public release. Repair and rebuild a new candidate, or use an explicitly staging/prerelease channel. +6. Publish the already-verified candidate bytes through the declared distribution surface. +7. Record the published artifact's immutable registry/file identity and prove it is the verified candidate bytes. For registries that transform artifacts, record the registry's immutable identity plus the explicit mapping from candidate to published object. + +For Python, normally require `pyproject.toml`, wheel/sdist checks, built-artifact tests, a version/release note, and the immutable hash of each artifact actually consumed. PyPI is an example distribution surface, not universal doctrine. + +### `reconsumed` + +The forge proves the separation is real by replacing its local implementation path with the published artifact. + +Require: + +- the forge dependency points at the released version **and immutable artifact identity** where the ecosystem supports pinning/verification; +- the recorded registry/file digest matches the artifact verified before publication; +- local/source-tree imports of the candidate implementation are removed from the consuming path; +- integration tests run against the published artifact, not a local checkout or editable install; +- cross-repository behavior remains compatible; +- rollback is defined if the published artifact cannot reproduce the verified behavior. + +Post-publication reconsumption is a terminal smoke/integration check on the public distribution path. It does not substitute for the pre-publication forge verification required by the `released` gate. + +### `graduated` + +Graduation is earned only when all prior gates are evidenced and authority transfer is explicitly authorized. + +Then: + +```text +implementation/public-contract authority: independent repository +forge relation: consumer/integration forge +ordinary downstream consumption: released immutable artifact +incubated implementation: removed, archived as provenance, or mechanically unreachable from production/import paths +``` + +The independent project may continue to depend on its originating projects. Independence means an independent authority and release boundary, not dependencylessness. + +## Authority boundary and transition receipt + +`interdependent-work-graph` snapshots keep their existing non-transfer invariant. Do **not** encode this graduation by setting an existing work-graph `authority_transfer` field to true. + +Instead, use two exact graph identities plus a separate scoped transition receipt: + +```yaml +schema: the-interdependency.project-graduation-transition +version: 1.0.0 +candidate: +before: + work_graph_sha256: + implementation_public_contract_authority: +after: + work_graph_sha256: + implementation_public_contract_authority: +transition: + implementation_public_contract_authority_transfer: true + semantic_authority_transfer: false + theorem_status_transfer: false + proof_status_transfer: false + certification_status_transfer: false + measurement_status_transfer: false + empirical_status_transfer: false +authorization: + authority_transfer: authorized +artifact: + version: + immutable_identity: +hmmm: [] +``` + +The receipt describes the authorized transition **between** two valid graph snapshots. Each snapshot still records its own ordinary non-transfer boundaries. The transition does not rewrite upstream authorities. + +Before graduation: + +```text +forge owns candidate implementation +candidate consumes upstream authorities +``` + +After graduation: + +```text +new repository owns candidate implementation and public contract +forge consumes candidate through its released interface +upstream repositories retain their own authorities +``` + +Never infer semantic/proof/measurement promotion from repository or package graduation. + +## Graduation record + +Maintain a small machine- or human-readable record in the forge or extraction PR. Minimum shape: + +```yaml +candidate: +state: +mode: +authorization: + external_mutation: + authority_transfer: +forge: + repository: + source_commit: + source_path: +future_authority: + repository: +distribution: + kind: + artifact: + version: + candidate_immutable_identity: + published_immutable_identity: + published_matches_verified_candidate: +upstream: + - repository: + relation: + authority_transfer: false +gates: + public_api: + independent_tests: + clean_build_install: + license_distribution_rights: + release_ownership_authority: + provenance_preserved: + exact_candidate_forge_verification: + stable_release: + downstream_reconsumption: +transition_receipt: +hmmm: [] +``` + +A graduation record may cite a shared work graph, but the scoped transition receipt remains distinct because it records a lifecycle event rather than pretending one work-graph snapshot transferred all authority. + +## Workflow + +1. **Identify the forge and candidate.** State why the candidate is legitimately incubated there and what composition produced it. +2. **Resolve authorities.** Load `interdependent-work-graph` once multiple repositories participate. Pin exact identities and preserve non-transfer boundaries. +3. **Classify lifecycle state.** Choose the highest state for which evidence already exists; do not classify by aspiration. +4. **Stabilize the seam.** Separate public candidate contracts from forge-private conveniences. Declare allowed upstream dependencies and prohibited cycles. +5. **Run qualification gates.** Test independent purpose, public API, clean build/install, independent tests, provenance, downstream fixture behavior, license/distribution rights, and release ownership. Blocking unknowns stop at `qualified`. +6. **Check authorization before mutation.** If repository creation, namespace reservation, publication, consumer mutation, severance, or authority transfer is not explicitly authorized, stop that action boundary as `hmmm` while retaining the completed assessment. +7. **Extract with provenance.** Create the independent repository from the qualified source while preserving origin and exact extraction identity. +8. **Build and identify the candidate.** Build the would-be release artifact and record its immutable identity. +9. **Verify before stable publication.** Test the exact candidate in a clean environment and in the forge. A failure returns to repair/rebuild; it does not publish a known-bad stable release. +10. **Publish verified bytes.** Publish the verified candidate through the declared distribution surface and record the immutable published identity. +11. **Reconsume the publication.** Replace local forge consumption with the published artifact and rerun integration evidence. +12. **Sever the old implementation path.** Remove or mechanically disable ordinary consumption of the incubated copy while retaining provenance where useful. +13. **Record and authorize the transition.** Emit the scoped before/after authority-transition receipt, update ownership/dependency records, and only then declare graduation. +14. **Carry `hmmm`.** Non-blocking unresolveds remain visible; blocking unresolveds do not masquerade as passed gates. + +## Output shape + +When this skill is active, report: + +```markdown +## Candidate +- name: +- forge: +- current state: +- intended independent authority: +- mode: assessment | execution + +## Composition and authority +- upstream: exact identity — authority — relation +- non-transfer boundaries: + +## Graduation gates +- public API: +- independent tests: +- clean build/install: +- license/distribution rights: +- release ownership: +- exact candidate identity: +- pre-publication forge verification: +- published immutable identity: +- reconsumption: + +## Authorization +- external mutation: +- authority transfer: + +## Actions +- delivered/read-only: +- authorized mutation: +- blocked: + +## hmmm +- ... +``` + +## Validation + +A successful graduation demonstrates all of the following: + +- the candidate's origin and source identity are preserved; +- the candidate has an independently coherent public contract; +- tests and clean installation succeed outside forge-private paths; +- license/distribution rights and release ownership are resolved, not `hmmm`; +- upstream dependencies are explicit and semantic/proof/measurement authority does not silently transfer; +- external mutations and authority transfer were explicitly authorized; +- the independent repository builds an immutable candidate artifact; +- the exact candidate passes forge integration **before** stable public publication; +- the published immutable identity is bound to those verified candidate bytes; +- a clean consumer can install the exact published artifact; +- the forge reconsumes that published artifact successfully; +- ordinary local imports of the incubated implementation no longer determine runtime behavior; +- the scoped transition receipt binds exact before/after graph identities without changing the work-graph non-transfer invariant; +- rollback and remaining non-blocking `hmmm` stay visible; +- the new repository is the sole implementation/public-contract authority after graduation. + +For Python/PyPI graduation, terminal evidence should resemble: + +```text +build wheel/sdist +-> hash candidate artifact +-> clean-install exact candidate + run project tests +-> install exact candidate in forge + run integration tests +-> publish those verified bytes +-> verify registry/file digest +-> install published immutable artifact in forge +-> rerun integration smoke +-> sever local implementation path +-> record authorized authority transition +``` + +## Anti-patterns + +- Treating creation of a new repository as proof of graduation. +- Publishing a stable artifact before the exact candidate has passed forge integration. +- Publishing an artifact while the forge still imports its local copy. +- Calling a version/tag immutable evidence without an artifact digest or equivalent registry identity. +- Copying code without preserving source commit/path provenance. +- Freezing an unstable API merely to satisfy a calendar date. +- Allowing unresolved license/distribution rights or release ownership through qualification. +- Treating a readiness assessment as permission to create repositories, publish packages, mutate consumers, or transfer authority. +- Requiring an independent project to have zero dependencies. +- Allowing the graduated package to import forge-private modules. +- Maintaining two writable implementations after graduation with no declared synchronization authority. +- Setting an existing work-graph `authority_transfer` field true to represent this lifecycle event. +- Calling an editable install, local path dependency, unreleased branch, or mutable tag a released consumption path. +- Letting package/repository graduation imply semantic, theorem, proof, measurement, certification, or empirical promotion. +- Deleting unresolved migration problems instead of recording `hmmm`. + +## Minimal example + +```text +metapat + ucns + edcm + | + v +stack/incubator/epac + | + | read-only qualification gates pass + | explicit mutation authorization + v +The-Interdependency/epac + | + | build candidate + immutable digest + | stack integration-tests exact candidate + v +publish verified EPAC artifact + | + | stack installs published immutable artifact + v +sever local EPAC path + authorized transition receipt + | + v +EPAC graduated; stack resumes its role as forge/consumer +``` + +The example describes implementation lifecycle only. METAPAT, UCNS, EDCM, and EPAC each retain the authority and evidentiary status of their own domains. + +## hmmm + +- Whether repeated real graduations justify promoting the graduation record and transition receipt into versioned schema/helper files rather than keeping them as procedural reference contracts. +- Whether history extraction should become deterministic tooling; repository histories differ enough that the skill currently requires provenance preservation without mandating one Git surgery. +- Whether package publication should later split into a separate ecosystem-specific release skill after repeated use proves substantial independent complexity. +- A forge that cannot let go of its products is a warehouse with sparks; a product that cannot survive leaving the forge is still hot metal. diff --git a/.agents/skills/ratios/SKILL.md b/.agents/skills/ratios/SKILL.md new file mode 100644 index 0000000..cf2b7cd --- /dev/null +++ b/.agents/skills/ratios/SKILL.md @@ -0,0 +1,281 @@ +--- +name: ratios +description: Self-declaring module composition ratios on msdmd — a single comment line on a file's first and last line (never a fenced block). The canonical seal is `The-Interdependency/a0`'s compact positional `N:M C:D I:O` annotation (code:comment · consumed:declared · fan-in:fan-out), computed by a0's `scripts/annotate.py`; the named `loc_comments=… imports_exports=… calls_definitions=…` line is a portable, per-file adaptation for standalone libraries, verified by the stdlib `ratios_check.py` (drift/misplacement failures, visible gaps). JSON/Markdown are out of scope. Load this when recording a module's composition ratios, when authoring or extending the ratio registry, or when wiring ratio verification into CI. +--- + +# ratios — Module composition ratios on msdmd + +`ratios` is an application of [msdmd](../msdmd/SKILL.md). The foundational +skill defines the comment-block convention, the universal parser, and the +gap-reporting requirement; this skill applies the convention to a module's +own composition ratios and defines the executor contract. + +Read `msdmd/SKILL.md` first if you haven't — the parser contract and the +visibility rules below are inherited from there and not redefined. + +A ratio is a fact an executable/source module owns about its own shape. Like a contract, it +belongs in the file it describes, not in a side report that can drift out +of sync. Unlike a contract, it is not asserted by a human — it is +*recomputed from the source*, so a recorded ratio that no longer matches +the file is a build failure, not a stale comment nobody noticed. + +## The canonical seal: a0's `N:M C:D I:O` + +`The-Interdependency/a0` is the **canonical origin** of the ratios seal — the +convention every other form adapts, not the other way round. a0 stamps three +composition metrics on the first and last line of every Python / TypeScript +file, written and verified by its own `scripts/annotate.py`: + +```text +# N:M C:D I:O (Python) +// N:M C:D I:O (TypeScript / TSX) +``` + +| pair | meaning | how computed | +|---|---|---| +| `N:M` | code lines : comment+docstring lines (internal density; `N` budget ≤ 400) | per-file | +| `C:D` | consumed : declared (surface utility — `D` = declared `# DOC` endpoints / exported symbols; `C` = those actually consumed elsewhere in the repo) | repo-wide index | +| `I:O` | fan-in : fan-out (graph position — `I` = files importing this one; `O` = project-internal modules it imports) | repo-wide index | + +`C:D` and `I:O` are **not single-file computable**: they need the inverted +import/usage index built across the whole repo. a0's `scripts/annotate.py` is +the in-repo canonical computer. + +### Portable computer: `ratios/annotate_index.py` + +A shared, stdlib port of that index ships here as `ratios/annotate_index.py`, so +the canonical seal can be recomputed or stamped in **any** repo, not just a0: + +```bash +python ratios/annotate_index.py --root . # report drift/misplacement +python ratios/annotate_index.py --root . --write # stamp the N:M C:D I:O seal +python ratios/annotate_index.py --root . --check # CI gate (non-zero on drift) +``` + +It reproduces a0's metric definitions exactly (`build_index` → per-file +`{code, comment, consumed, declared, fan_in, fan_out}`; `seal_line` renders the +compact line). The one a0-specific input — the consumer dirs scanned for `C` +(default `client/src`, `server`) — is a `consumer_dirs=` parameter. Honest +caveat carried in the module docstring: `C:D` is route/surface-oriented, so a +pure library with no `# DOC endpoint:` routes and no TS exports reads +`C:D = 0:0` (correct, not a gap), and fan-in/out follow a0's relative-import +stem graph (absolute-only imports are not counted). + +## The portable named form (per-file adaptation) + +The verbose `loc_comments=… imports_exports=… calls_definitions=…` line below is +a **portable, per-file adaptation** of the canon for standalone libraries that +lack a0's app structure (no `client/src` / `server` to measure "consumed" +against, no meaningful repo-wide fan-in). Its three ids are single-file +computable, so the stdlib `ratios_check.py` verifies them with no repo index. +`loc_comments` is exactly a0's `N:M`; `imports_exports` and `calls_definitions` +are per-file stand-ins for the surface/graph intent of `C:D` and `I:O`. Repos +already stamped in this form (skill-lib, aimmh, edcmbone, pcna, pcta, ptca, +pcea) remain valid and are **not** required to reconvert. + +Both forms keep one seal discipline: a single line on the file's first and last +non-blank line, with nothing above it. + +### The single line, and the first/last rule + +RATIOS is the one msdmd declaration that is **not a fenced block**. It applies +to executable/source files with a language comment marker (`#`, `//`, or `--`), +not to JSON, Markdown, or other data/documentation files. In covered source +files it is a single comment line carrying all three ratios, placed on the +file's **literal first line and its last non-blank line**: + +```python +# ratios: loc_comments=128:49 imports_exports=4:7 calls_definitions=51:10 +"""The module body lives between the two ratio lines.""" +... +# ratios: loc_comments=128:49 imports_exports=4:7 calls_definitions=51:10 +``` + +The form is: + +```text + ratios: loc_comments=N:M imports_exports=N:M calls_definitions=N:M +``` + +- `` is the language-idiomatic comment marker (`#`, `//`, `--`) for executable/source files only; JSON and Markdown are intentionally not covered by this rule. +- The three ids are fixed: `loc_comments`, `imports_exports`, + `calls_definitions`. Each carries an `A:B` value, or `hmmm` if the ratio is + declared-but-not-yet-resolved. +- The same line opens and closes the file. The file is a self-measuring + object; its boundary lines carry the measurement, opening and closing. +- Scope: executable source files only. `json` and `.md` files are out of + scope for first/last-line RATIOS bookends. + +There is no fenced `# === RATIOS === … # === END RATIOS ===` block. An earlier +draft of this skill described one; that was wrong. Tooling reads the single +line from both ends and verifies they agree. + +## Field requirements + +The required ratio ids are exactly `loc_comments`, `imports_exports`, and +`calls_definitions`. Each covered executable/source file records all three ids +on both boundary lines. Unknown ratio values are written `hmmm`; unknown ratio +ids stay unresolved until a matching computer is defined. + +## How it is parsed + +The reader lives in the msdmd universal parser as a sanctioned extension, not +a fork (`msdmd/parsers/universal.py`): + +```python +from msdmd.parsers.universal import parse_ratios, ratios_placement, RATIO_IDS + +parse_ratios(text, marker) # -> [{"id": "loc_comments", "value": "128:49"}, ...] +ratios_placement(text, marker) # -> (first_line_ok, last_non_blank_line_ok) +``` + +`parse_ratios` returns one flat `{"id", "value"}` dict per (declaration line × +ratio token), so the drift gate can verify every occurrence. It interprets no +semantics — that is the runner's job. A file with no `ratios:` line yields `[]` +and surfaces as a coverage gap. + +## The registry and the verify contract + +A ratio is *verified* when its `id` maps to a computer in the runner's +registry. A computer is a pure function `file_text -> "A:B"`. The runner: + +- recomputes the ratio from the source, **excluding every `ratios:` line** + (and any tolerated legacy fence) so a measuring line never inflates its own + measurement; +- compares the recomputed value to the recorded value; +- on mismatch, emits a **drift** error and exits non-zero; +- on a declaration that is not on both the first and last line, emits a + **misplaced** error and exits non-zero; +- on `value: hmmm`, reports a living continuation (pending), never a failure — + the transition out of `hmmm` is the owner's decision; +- on an id with no registered computer, reports it as recorded-but- + unverifiable (informational), so unknown ratios stay visible rather than + silently trusted. + +Covered source files with no `ratios:` line surface as coverage gaps, exactly +as in the build checker. JSON, Markdown, and other files with no supported +source comment marker are skipped rather than reported as gaps. The gap list is +informational unless `--strict`. + +## The three ratios + +### 1. `loc_comments` — lines of code to lines commented + +`loc_comments` is `lines_of_code : lines_commented`. + +- **N (lines of code)**: physical lines carrying a code token — not blank, + not a pure comment, not a docstring-only line. +- **M (lines commented)**: strict `#`-comment lines plus docstring lines + (`"""` / `'''` blocks). +- **self-exclusion**: every `ratios:` line is excluded from both counts. + +Diagnostic signal: a ratio that drifts toward very high N:M (many code +lines, few comments) is approaching a code budget with low documentation +coverage. A ratio drifting toward very low N:M may indicate grounding load — +overhead accumulating faster than implementation. + +--- + +### 2. `imports_exports` — import statements to public exports + +`imports_exports` is `import_count : export_count`. + +- **import_count**: lines whose stripped content matches + `^(import |from \S+ import)`. Continuation lines of a multi-line import + are not counted separately — only the opening `import` / `from` line. +- **export_count**: count of top-level `def` and `class` declarations + whose names carry no leading underscore (public surface), **plus 1** if + `__all__` appears anywhere in the file. +- **self-exclusion**: `ratios:` lines excluded from both counts. + +Diagnostic signal: a high imports:exports ratio (many imports, few public +symbols) suggests the module is a consumer or orchestrator — low surface, +high dependency. A low ratio suggests a leaf module or utility layer. + +--- + +### 3. `calls_definitions` — call sites to definitions + +`calls_definitions` is `call_count : definition_count`. + +- **definition_count**: top-level `def` and `class` lines plus one level of + method nesting beneath a class. Closures nested inside another `def` do not + count. +- **call_count**: non-definition, non-comment, non-blank, non-string lines + containing at least one call expression matching `\w+\(`. Each physical + line counts once regardless of how many calls it contains. +- **self-exclusion**: `ratios:` lines excluded from both counts. + +Diagnostic signal: a very high calls:definitions ratio suggests a dense +orchestration file — close coupling. A low ratio suggests mostly definitions +with few call sites — a library or schema module. + +--- + +## The runner + +The reference runner ships here as `ratios_check.py` (pure stdlib; it reuses +`parse_ratios` from the msdmd universal parser): + +```bash +# verify one file's recorded ratios against its source +python ratios_check.py path/to/module.py + +# walk a tree, verifying executable/source files and listing source gaps +python ratios_check.py --root . + +# strict: files with no ratios: line also fail (CI gate) +python ratios_check.py --root . --strict +``` + +Exit codes: `0` all recorded ratios match and are correctly placed (gaps +allowed unless `--strict`); `1` a ratio drifted from source, a declaration was +misplaced, or — under `--strict` — a coverage gap. + +## a0 is the canon, not a dialect + +The compact `N:M C:D I:O` form under **The canonical seal** above is the origin +the named form adapts — not a dialect *of* it. The mapping back to the portable +ids: `N:M` ≡ `loc_comments` (identical); `C:D` (consumed:declared) and `I:O` +(fan-in:fan-out) are the repo-wide surface/graph measures that `imports_exports` +and `calls_definitions` only approximate per-file. + +Do **not** teach the canonical `parse_ratios` reader to parse a0's compact line: +a0's `scripts/annotate.py` owns reading and verifying the canonical seal (its +`C:D` / `I:O` need the repo-wide index `parse_ratios` deliberately does not +build). Pointing `ratios_check.py` at a0 and seeing the compact lines reported +as gaps is expected, not a bug. + +## Anti-patterns + +- Writing RATIOS as a fenced `# === RATIOS === … # === END RATIOS ===` block. + It is a single `ratios:` line; the block form was a mistake. +- Recording a ratio by hand instead of recomputing it. The point is that the + file measures itself; a hand-typed value is a contract that drifts. +- Placing the line anywhere but the file's first and last line. The first/last + placement is the convention; a mid-file `ratios:` line defeats the + at-a-glance reading and fails the placement gate. +- Counting a `ratios:` line in its own ratio. Always self-exclude. +- Inventing ratio ids whose computer does not exist and recording a number + for them. If there's no computer, the value cannot be verified — record + `hmmm` until a computer is registered. +- Introducing a parser dialect for ratios. The single-line reader is an msdmd + extension (`parse_ratios`); if richer syntax is needed, extend msdmd, do + not fork it. + +## Completion criteria + +A run is complete when every covered executable/source file carries a correctly placed `ratios:` +line on its first and last line, the registry's three computers +(`loc_comments`, `imports_exports`, `calls_definitions`) recompute each +recorded value with no drift, and any unresolved ratio is recorded as `hmmm` +rather than guessed. + +hmmm +- the reference computers implement the Python counting rules; language-aware + computers for TypeScript/other markers are a documented extension point, not + yet implemented in `ratios_check.py` +- whether ratios verification joins CI beside the other msdmd checks +- calls_definitions: whether lambda assignments count as definitions +- imports_exports: whether re-exported names from `__init__.py` aggregate + files count once or per-name diff --git a/.agents/skills/ratios/annotate_index.py b/.agents/skills/ratios/annotate_index.py new file mode 100644 index 0000000..e4d7e59 --- /dev/null +++ b/.agents/skills/ratios/annotate_index.py @@ -0,0 +1,312 @@ +# ratios: loc_comments=232:33 imports_exports=7:4 calls_definitions=83:14 +"""Portable computer for the canonical ratios seal — a0's `N:M C:D I:O`. + +This is the shared, stdlib port of `The-Interdependency/a0`'s +`scripts/annotate.py`. The compact positional line + + # N:M C:D I:O (Python) + // N:M C:D I:O (TypeScript / TSX) + +is the *canonical* ratios seal (see `ratios/SKILL.md`). Unlike the portable +per-file `loc_comments=…` form (verified by `ratios_check.py`), the canonical +`C:D` and `I:O` are **not single-file computable** — they need a repo-wide +inverted index. This module builds that index in a single linear pass and can +recompute or stamp the seal for any repo, not just a0. + +Metric definitions (identical to a0): + + N code lines : M comment/docstring lines (internal density) + C consumed : D declared (surface utility) + I fan-in : O fan-out (graph position) + + D = declared `# DOC endpoint:` routes (.py); exported symbols (.ts/.tsx) + C = declared routes referenced in the consumer dirs (.py); fan-in (.ts) + I = repo files that import this module (relative-import stem match) + O = distinct project-internal modules this file imports (relative imports) + +Honest scope note: `C:D` is route/surface-oriented. A pure library that +declares no `# DOC endpoint:` routes and no TS exports reads `C:D = 0:0` — that +is the correct canonical value, not a gap. Fan-in/out follow a0's relative- +import stem graph; modules wired purely by absolute import are not counted +(that mirrors a0's own annotator). The consumer dirs are the one a0-specific +bit made configurable here. + +Public API: + build_index(files, root, *, consumer_dirs=("client/src", "server")) -> dict + seal_line(metrics, marker="#") -> str + collect_files(root, *, skip=None, extensions=(".py",".ts",".tsx")) -> list[Path] + +Pure stdlib; safe to copy verbatim into any consuming repo. +""" +from __future__ import annotations +import os +import re +from pathlib import Path +from typing import Iterable + +DEFAULT_SKIP = { + ".git", "node_modules", "__pycache__", "dist", ".cache", ".local", + ".venv", "venv", ".agents", "attached_assets", ".pythonlibs", "build", + ".next", ".nuxt", "target", ".pytest_cache", ".mypy_cache", ".tox", +} +DEFAULT_CONSUMER_DIRS = ("client/src", "server") +_ANN_PY = re.compile(r"^#\s*\d+:\d+(\s+\d+:\d+){0,2}\s*$") +_ANN_TS = re.compile(r"^//\s*\d+:\d+(\s+\d+:\d+){0,2}\s*$") + + +def _is_seal(line: str, ext: str) -> bool: + s = line.strip() + return bool((_ANN_PY if ext == ".py" else _ANN_TS).match(s)) + + +def _strip_seal(lines: list[str], ext: str) -> list[str]: + w = lines[:] + if w and _is_seal(w[0], ext): + w = w[1:] + if w and _is_seal(w[-1], ext): + w = w[:-1] + return w + + +def _count_python(lines: list[str]) -> tuple[int, int]: + code = comment = 0 + in_triple = False + triple = None + for line in lines: + s = line.strip() + if not s: + continue + if in_triple: + comment += 1 + if triple in s: + in_triple = False + elif s.startswith('"""') or s.startswith("'''"): + comment += 1 + t = s[:3] + if s.count(t) < 2: + in_triple = True + triple = t + elif s.startswith("#"): + comment += 1 + else: + code += 1 + return code, comment + + +def _count_ts(lines: list[str]) -> tuple[int, int]: + code = comment = 0 + in_block = False + for line in lines: + s = line.strip() + if not s: + continue + if in_block: + comment += 1 + if "*/" in s: + in_block = False + elif s.startswith("/*"): + comment += 1 + if "*/" not in s[2:]: + in_block = True + elif s.startswith("//"): + comment += 1 + else: + code += 1 + return code, comment + + +def _py_endpoints(lines: list[str]) -> list[str]: + paths = [] + for line in lines: + m = re.match(r"\s*#\s*DOC\s+endpoint:\s+\w+\s+(/[^\s|]+)", line) + if m: + paths.append(m.group(1).rstrip("/")) + return paths + + +def _py_fanout(lines: list[str]) -> int: + mods: set[str] = set() + for line in lines: + m = re.match(r"\s*from\s+(\.[\w.]*)\s+import", line) + if m: + mods.add(m.group(1)) + return len(mods) + + +def _ts_declared(lines: list[str]) -> int: + count = 0 + for line in lines: + s = line.strip() + if re.match( + r"^export\s+(default\s+)?(function|const|class|interface|type|enum)\b", s + ): + count += 1 + elif re.match(r"^export default [^{]", s): + count += 1 + return count + + +def _ts_fanout(lines: list[str]) -> int: + mods: set[str] = set() + for line in lines: + m = re.match(r"""\s*import\s+.*\s+from\s+['"]([.@][^'"]+)['"]""", line) + if m: + mods.add(m.group(1)) + return len(mods) + + +def _read_consumer_text(root: Path, consumer_dirs: Iterable[str]) -> str: + parts: list[str] = [] + for subdir in consumer_dirs: + d = root / subdir + if not d.exists(): + continue + for fp in d.rglob("*"): + if fp.suffix in (".ts", ".tsx", ".js"): + try: + parts.append(fp.read_text(encoding="utf-8")) + except OSError: + pass + return "\n".join(parts) + + +def build_index( + files: list[Path], + root: Path, + *, + consumer_dirs: Iterable[str] = DEFAULT_CONSUMER_DIRS, +) -> dict: + """Compute N:M C:D I:O for every file. Fan-in via one inverted-index pass.""" + index: dict[str, dict] = {} + texts: dict[str, str] = {} + + for path in files: + try: + text = path.read_text(encoding="utf-8") + except OSError: + text = "" + texts[str(path)] = text + ext = path.suffix + working = _strip_seal(text.splitlines(), ext) + if ext == ".py": + code, comment = _count_python(working) + endpoints = _py_endpoints(working) + fan_out = _py_fanout(working) + declared = len(endpoints) + else: + code, comment = _count_ts(working) + endpoints = [] + fan_out = _ts_fanout(working) + declared = _ts_declared(working) + index[str(path)] = { + "ext": ext, "code": code, "comment": comment, + "declared": declared, "consumed": 0, + "fan_out": fan_out, "fan_in": 0, "endpoints": endpoints, + } + + # inverted import index: stem -> importer files (relative imports only) + stem_importers: dict[str, set[str]] = {} + for src_path in files: + src_str = str(src_path) + src_ext = src_path.suffix + for line in texts.get(src_str, "").splitlines(): + s = line.strip() + if src_ext == ".py": + m = re.match(r"from\s+([.]+[\w.]*)\s+import", s) + if m: + parts = [p for p in m.group(1).split(".") if p] + if parts: + stem_importers.setdefault(parts[-1], set()).add(src_str) + elif src_ext in (".ts", ".tsx"): + m = re.match(r"""\s*import\s+.*from\s+['"]([.][^'"]+)['"]""", s) + if m: + seg = m.group(1).rstrip("/").split("/")[-1] + stem = re.sub(r"\.\w+$", "", seg) + if stem: + stem_importers.setdefault(stem, set()).add(src_str) + + for path in files: + importers = stem_importers.get(path.stem, set()) - {str(path)} + index[str(path)]["fan_in"] = len(importers) + + consumer_text = _read_consumer_text(root, consumer_dirs) + for data in index.values(): + if data["ext"] == ".py" and data["endpoints"]: + data["consumed"] = sum(1 for ep in data["endpoints"] if ep in consumer_text) + elif data["ext"] in (".ts", ".tsx"): + data["consumed"] = data["fan_in"] + return index + + +def seal_line(metrics: dict, marker: str = "#") -> str: + """Render the canonical `N:M C:D I:O` seal line for one file's metrics.""" + return ( + f"{marker} {metrics['code']}:{metrics['comment']} " + f"{metrics['consumed']}:{metrics['declared']} " + f"{metrics['fan_in']}:{metrics['fan_out']}" + ) + + +def collect_files( + root: Path, + *, + skip: Iterable[str] | None = None, + extensions: Iterable[str] = (".py", ".ts", ".tsx"), +) -> list[Path]: + skip_set = set(skip) if skip is not None else set(DEFAULT_SKIP) + ext_set = set(extensions) + out: list[Path] = [] + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in skip_set] + for fn in filenames: + p = Path(dirpath) / fn + if p.suffix in ext_set: + out.append(p) + return sorted(out) + + +def _marker_for(ext: str) -> str: + return "//" if ext in (".ts", ".tsx", ".js", ".jsx") else "#" + + +def main(argv: list[str] | None = None) -> int: + import sys + argv = list(argv if argv is not None else sys.argv[1:]) + write = "--write" in argv + check = "--check" in argv + argv = [a for a in argv if a not in ("--write", "--check")] + root = Path(argv[argv.index("--root") + 1]) if "--root" in argv else Path(".") + root = root.resolve() + + files = collect_files(root) + index = build_index(files, root) + drift = 0 + for path in files: + m = index[str(path)] + ext = path.suffix + want = seal_line(m, _marker_for(ext)) + try: + lines = path.read_text(encoding="utf-8").splitlines() + except OSError: + continue + have = lines[0].strip() if lines else "" + placed_ok = bool(lines) and _is_seal(lines[0], ext) and _is_seal(lines[-1], ext) + if write: + working = _strip_seal(lines, ext) + new = "\n".join([want] + working + [want]) + "\n" + if new != path.read_text(encoding="utf-8"): + path.write_text(new, encoding="utf-8") + print(f" stamped {path.relative_to(root)} [{want}]") + else: + if not placed_ok or have != want: + drift += 1 + print(f" DRIFT {path.relative_to(root)}: have '{have}' want '{want}'") + if check: + print(f"annotate_index: {len(files)} files, {drift} drift/misplaced") + return 1 if drift else 0 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=232:33 imports_exports=7:4 calls_definitions=83:14 diff --git a/.agents/skills/ratios/ratios_check.py b/.agents/skills/ratios/ratios_check.py new file mode 100644 index 0000000..12f470e --- /dev/null +++ b/.agents/skills/ratios/ratios_check.py @@ -0,0 +1,256 @@ +# ratios: loc_comments=190:27 imports_exports=6:7 calls_definitions=78:10 +"""ratios skill executor — recompute the canonical ratios and gate on drift. + +Reference runner for the ``ratios`` skill. It reads the single-line RATIOS +declaration (`` ratios: loc_comments=N:M imports_exports=N:M +calls_definitions=N:M``) from a file's first and last non-blank lines, +recomputes each ratio from the source, and fails on: + + * drift — a recorded ratio no longer matches what the source computes; + * misplaced — a RATIOS declaration not on both the first and last line; + * gaps — (only under ``--strict``) source files with no RATIOS at all. + +``value: hmmm`` is reported as pending, never a failure. A recorded id with no +registered computer is reported as unverifiable (informational). Pure stdlib; +the single-line reader is reused from the msdmd universal parser, not forked. + +Usage: + python ratios_check.py path/to/module.py # verify one file + python ratios_check.py --root . # walk a tree + python ratios_check.py --root . --strict # gaps also fail (CI gate) +""" +from __future__ import annotations + +import os +import re +import sys +from pathlib import Path + +# Make the sibling ``msdmd`` skill importable whether run from a repo root or +# from a vendored ``.agents/skills/ratios/`` directory. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from msdmd.parsers.universal import ( # noqa: E402 + RATIO_IDS, + marker_for, + parse_ratios, + ratios_placement, +) + +_SKIP = { + "tests", "__pycache__", "node_modules", ".git", ".venv", "venv", + "dist", "build", ".next", ".nuxt", "target", ".pytest_cache", + ".mypy_cache", ".tox", +} + +# Legacy fenced block (tolerated so transitional files don't pollute the +# counts) plus the canonical single-line form, both self-excluded. +_RATIOS_FENCE = re.compile( + r"^#\s*===\s*RATIOS\s*===.*?^#\s*===\s*END\s+RATIOS\s*===", + re.MULTILINE | re.DOTALL, +) +_RATIOS_LINE = re.compile(r"^\s*(?:#|//|--)\s*ratios:.*$", re.MULTILINE) + +_IMPORT_RE = re.compile(r"^\s*(?:import\s|from\s+\S+\s+import\s)") +_TOP_DEF_RE = re.compile(r"^(?:async\s+)?def\s+(\w+)|^class\s+(\w+)") +_NESTED_METHOD_RE = re.compile(r"^\s{4}(?:async\s+)?def\s+\w+") +_CALL_RE = re.compile(r"\b\w+\(") +_DOCSTRING_OPEN = re.compile(r'^\s*([rRbBuUfF]{0,2})("""|\'\'\')') + + +def _strip_ratios_lines(text: str) -> str: + """Remove every RATIOS declaration so ratios self-exclude from counts.""" + text = _RATIOS_FENCE.sub("", text) + text = _RATIOS_LINE.sub("", text) + return text + + +def _classify_lines(text: str) -> dict[str, list[int]]: + """Partition line indices into code / comment / docstring / blank.""" + lines = text.splitlines() + out: dict[str, list[int]] = {"code": [], "comment": [], "docstring": [], "blank": []} + in_doc = False + quote: str | None = None + for i, raw in enumerate(lines): + stripped = raw.strip() + if not stripped: + out["blank"].append(i) + continue + if in_doc: + out["docstring"].append(i) + if quote and quote in raw: + in_doc = False + quote = None + continue + m = _DOCSTRING_OPEN.match(raw) + if m and stripped.startswith(("'''", '"""')): + q = m.group(2) + out["docstring"].append(i) + rest = raw[m.end():] + if q in rest: + continue + in_doc = True + quote = q + continue + if stripped.startswith("#"): + out["comment"].append(i) + continue + out["code"].append(i) + return out + + +def compute_loc_comments(text: str) -> str: + """N:M where N = code lines, M = comment + docstring lines.""" + cls = _classify_lines(_strip_ratios_lines(text)) + n = len(cls["code"]) + m = len(cls["comment"]) + len(cls["docstring"]) + return f"{n}:{m}" + + +def compute_imports_exports(text: str) -> str: + """import_count : public-export count (+1 if __all__ present).""" + text = _strip_ratios_lines(text) + lines = text.splitlines() + import_count = sum(1 for line in lines if _IMPORT_RE.match(line)) + export_count = 0 + for line in lines: + m = _TOP_DEF_RE.match(line) + if m: + name = m.group(1) or m.group(2) + if name and not name.startswith("_"): + export_count += 1 + if "__all__" in text: + export_count += 1 + return f"{import_count}:{export_count}" + + +def compute_calls_definitions(text: str) -> str: + """call-site lines : definition lines (top-level + one nesting level).""" + text = _strip_ratios_lines(text) + def_count = 0 + call_count = 0 + for line in text.splitlines(): + stripped = line.strip() + if _TOP_DEF_RE.match(line) or _NESTED_METHOD_RE.match(line): + def_count += 1 + continue + if not stripped or stripped.startswith("#"): + continue + if stripped.startswith(("'''", '"""', "'", '"')): + continue + if _CALL_RE.search(line): + call_count += 1 + return f"{call_count}:{def_count}" + + +COMPUTERS = { + "loc_comments": compute_loc_comments, + "imports_exports": compute_imports_exports, + "calls_definitions": compute_calls_definitions, +} + + +def _iter_source(root: Path): + """Yield every source file under ``root`` with a known comment marker.""" + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in _SKIP] + for fn in sorted(filenames): + p = Path(dirpath) / fn + if marker_for(p) is not None: + yield p + + +def _verify_file(path: Path, base: Path, rep: dict) -> None: + marker = marker_for(path) + if marker is None: + return + rel = str(path.relative_to(base)) if path != base else path.name + text = path.read_text(encoding="utf-8", errors="ignore") + entries = parse_ratios(text, marker) + if not entries: + rep["gaps"].append(rel) + return + rep["covered"] += 1 + + first_ok, last_ok = ratios_placement(text, marker) + if not (first_ok and last_ok): + rep["misplaced"].append({"file": rel, "first_line": first_ok, "last_line": last_ok}) + + for entry in entries: + cid = entry.get("id", "") + value = (entry.get("value") or "").strip() + if value == "hmmm": + rep["pending"].append({"file": rel, "id": cid}) + continue + comp = COMPUTERS.get(cid) + if comp is None: + rep["unverifiable"].append({"file": rel, "id": cid, "value": value}) + continue + try: + actual = comp(text) + except Exception as ex: # pragma: no cover - defensive + rep["drift"].append({"file": rel, "id": cid, "recorded": value, "computed": f""}) + continue + if actual != value: + rep["drift"].append({"file": rel, "id": cid, "recorded": value, "computed": actual}) + else: + rep["verified"].append({"file": rel, "id": cid, "value": value}) + + +def run(target: Path) -> dict: + """Verify one file or every source file under a directory.""" + target = target.resolve() + rep: dict = { + "skill": "ratios", "root": str(target), "scanned": 0, "covered": 0, + "gaps": [], "drift": [], "misplaced": [], "pending": [], + "verified": [], "unverifiable": [], + } + if target.is_file(): + rep["scanned"] = 1 + _verify_file(target, target, rep) + else: + for path in _iter_source(target): + rep["scanned"] += 1 + _verify_file(path, target, rep) + rep["gaps_count"] = len(rep["gaps"]) + rep["drift_count"] = len(rep["drift"]) + rep["misplaced_count"] = len(rep["misplaced"]) + rep["pending_count"] = len(rep["pending"]) + rep["verified_count"] = len(rep["verified"]) + return rep + + +def summary(rep: dict) -> str: + return ( + f"ratios . {rep['scanned']} files . " + f"{rep['covered']} covered / {rep['gaps_count']} gaps . " + f"{rep['verified_count']} verified . {rep['drift_count']} drift . " + f"{rep['misplaced_count']} misplaced . " + f"{rep['pending_count']} hmmm . {len(rep['unverifiable'])} unverifiable" + ) + + +def main(argv: list[str] | None = None) -> int: + argv = list(argv if argv is not None else sys.argv[1:]) + strict = "--strict" in argv + argv = [a for a in argv if a != "--strict"] + if argv and argv[0] == "--root": + argv = argv[1:] + target = Path(argv[0]) if argv else Path(".") + + rep = run(target) + print(summary(rep)) + for d in rep["drift"][:30]: + print(f" drift: {d['file']} :: {d['id']}: recorded {d['recorded']} != computed {d['computed']}") + for m in rep["misplaced"][:30]: + print(f" misplaced: {m['file']}: first_line={m['first_line']} last_line={m['last_line']}") + if strict: + for g in rep["gaps"][:30]: + print(f" gap: {g}") + + fail = rep["drift_count"] or rep["misplaced_count"] or (strict and rep["gaps_count"]) + return 1 if fail else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +# ratios: loc_comments=190:27 imports_exports=6:7 calls_definitions=78:10 diff --git a/.agents/skills/repo-audit-repair/SKILL.md b/.agents/skills/repo-audit-repair/SKILL.md new file mode 100644 index 0000000..70013e0 --- /dev/null +++ b/.agents/skills/repo-audit-repair/SKILL.md @@ -0,0 +1,244 @@ +--- +name: repo-audit-repair +description: Evidence-led repository audit and repair from exact repository identity through authoritative verification. Use this when asked to audit, assess, harden, clean up, or audit and repair an existing code repository; when a green build may conceal placeholder gates, deprecated paths, or partial publication; or when repair must distinguish repository defects from environment, external-service, policy, and unresolved conditions. Do not load for an ordinary fixed-scope edit, a narrow review, or diagnosis of one already-localized failure unless the user requests a broader repository audit. +--- + +# repo-audit-repair — green must mean what it says + +Audit establishes what is true. Repair changes only evidenced repository-owned +faults. A successful command, merged change, and healthy deployed product are +different claims and require different evidence. + +## Core contract + +```text +exact repository identity -> reproducible baseline -> applicable audit surfaces +-> classified findings -> authorized repair -> repeated verification +-> authoritative terminal state +``` + +- Resolve the repository, default branch, exact starting commit, governing + instructions, dirty state, and release/deployment model before judging it. +- Treat audit-only requests as read-only. An `audit and repair` request permits + necessary repository changes, not unrelated cleanup, policy changes, + destructive data operations, credential acquisition, or broader publication. +- A green check is evidence only for what it actually executes. Placeholder, + skipped, pending, empty, or non-asserting gates remain gaps. +- Distinguish repository defects from host-environment failures, external + service failures, deliberate owner policy, and insufficient evidence. +- Repair the owning layer. Do not rewrite an upstream source to conceal a + consumer renderer, workflow, packaging, or deployment defect. +- Remove demonstrated deprecated paths and replace any still-needed capability + in the same repair boundary. Dead parallel architectures are not harmless. +- Re-run the relevant full gate after repairs. If merge, release, or deployment + is within scope, verify that terminal surface before calling the work complete. +- Carry unresolved constraints as `hmmm`; do not turn incomplete verification + into success prose. + +This skill owns audit closure. Load security, publication, accessibility, +language, framework, hosting, or organization skills only when their own +triggers apply; their domain doctrine remains theirs. + +## Finding classes + +Split compound failures into separate findings first. Every material finding +then receives exactly one standing class until new evidence changes it: + +| Class | Meaning | Permitted response | +|---|---|---| +| `DEFECT` | Reproducible repository-owned behavior violates a declared or necessary contract. | Repair in the owning layer and prove the repair. | +| `ENVIRONMENT` | The current host cannot execute the check or reproduce the product. | Repair the environment when authorized, relocate the check, or retain `hmmm`; do not patch product code to satisfy the host. | +| `EXTERNAL` | A remote service, source, registry, or network boundary failed independently of repository logic. | Retry only within declared bounds; use explicit fallback where authorized; do not call partial data complete. | +| `POLICY` | The behavior is an explicit owner decision rather than an implementation fault. | Preserve it or present a separate policy decision; do not silently "repair" it. | +| `HMMM` | Available evidence cannot yet distinguish the classes above. | Preserve the uncertainty and name the decisive next observation. | + +Passing evidence may be recorded as `HEALTHY`, but absence of a finding is not +proof that an unexamined surface is healthy. + +## Applicability gate + +Do not impose one universal checklist. Select surfaces from repository claims +and actual exposure: + +| Surface | Apply when | +|---|---| +| Build and tests | The repository declares a runnable product, package, generator, or test suite. | +| CI truthfulness | Workflows claim validation, release readiness, security, publication, or deployment. | +| Dependencies and security | The repository consumes dependencies, handles untrusted input, secrets, auth, network, or privileged operations. | +| Deprecated architecture | More than one implementation path, stale scaffold, removed dependencies, or superseded routes remain. | +| Links and provenance | The product publishes, indexes, renders, or retrieves source-owned material. | +| Performance | The repository exposes a latency-, size-, memory-, or browser-sensitive product and a defensible budget exists. | +| Browser and accessibility | A human-facing web interface is part of the product. | +| Release and deployment | The task or repository workflow ships artifacts or a live service. | + +For every selected surface, identify the claim, executable witness, and terminal +condition. For every omitted surface, the reason should be obvious from scope or +recorded briefly; do not manufacture tests merely to populate the table. + +## Workflow + +### 1. Resolve identity and authority + +- Confirm the exact repository instead of guessing from a generic project name. +- Record the starting commit and branch; preserve unrelated user changes. +- Read repository instructions and applicable skills before acting. +- Identify which repository or system owns source content, rendering, tests, + workflow, packaging, deployment, and policy decisions. +- Inspect current release/deployment status when it is part of the product. + +If repository identity or governing authority cannot be resolved, stop that +boundary as `hmmm`. + +### 2. Establish a clean baseline + +- Run declared checks from the pinned starting state with an environment capable + of hosting them. +- Separate dependency-install, browser/runtime, permission, network, and quota + failures from product failures before editing source. +- Inspect recent authoritative CI/release results where available, but do not + substitute status color for understanding what each job proves. +- Preserve generated snapshots, traces, caches, and refresh output as audit + evidence outside the intended patch when possible. + +### 3. Audit claims, not filenames + +- Trace each declared release or quality claim to an executable assertion. +- Open scripts behind umbrella commands; reject checks that merely print + `pending`, tolerate all failures, or never inspect the built artifact. +- Search for stale parallel implementations, imports without dependencies, + dead entry points, obsolete workflow versions, and superseded configuration. +- For remote-input builds, compare requested/discovered inputs with resolved and + published inputs. Partial retrieval must fail closed or become explicit, + identity-bearing fallback; it may not silently shrink the product. +- Inspect artifact behavior, not only source syntax: generated links, routes, + manifests, package contents, browser behavior, and deployment identity as + applicable. + +### 4. Classify before repairing + +For each finding, record: + +```text +claim -> evidence -> class -> owner -> proposed action -> verification +``` + +Do not repair `ENVIRONMENT`, `EXTERNAL`, or `POLICY` findings as if they were +repository defects. Reclassify only when new evidence supports the change. + +### 5. Repair the smallest complete boundary + +- Change only evidenced faults and the tests/contracts necessary to prevent + recurrence. +- Place the fix where the broken responsibility lives. For example, a + publication consumer that mis-resolves source-relative links fixes its + renderer; it does not edit the source repository's valid prose. +- Replace placeholder gates with bounded checks over actual outputs. +- Remove obsolete implementations once their replacement is proven. +- Keep dependency and lockfile changes exact and attributable. +- Use `repo_loto` when available to declare intent, mutation scope, and test + evidence; its presence is useful instrumentation, not a prerequisite for this + skill. + +### 6. Verify through the authorized terminal state + +- Re-run the same full applicable gate from a clean repaired state. +- Run focused regression checks plus the repository's broader release gate. +- Remove or restore audit-generated side effects so the patch contains only + intentional changes. +- Let authoritative CI test the branch where local infrastructure cannot. +- If CI exposes a real incompatibility, repair the same branch and repeat rather + than reporting a merely open pull request as completion. +- If merge is authorized, confirm mergeability and required checks before + merging. If deployment or release follows and is in scope, verify the public + artifact, version, route, or service identity afterward. +- State `merged`, `released`, and `deployed` separately. Never infer one from + another. + +## Output shape + +For audit-only work: + +```markdown +## Baseline +- repository, starting commit, governing instructions, environment + +## Findings +- claim — evidence — class — owner — decisive next action + +## Verified healthy +- only surfaces with positive evidence + +## hmmm +- unresolved or unexecutable boundaries +``` + +For audit-and-repair work, add: + +```markdown +## Repairs +- owning fault — changed files — regression protection + +## Verification +- local checks — CI — merge — release/deployment, each with exact status +``` + +Lead with the achieved state. Include commands only when they help another +operator reproduce or continue the work. + +## Usage guidance + +- `Audit this repository` activates read-only classification and reporting. +- `Audit and repair this repository` authorizes necessary in-repo repairs and + their verification within the existing repository workflow. +- `Fix this failing unit test` is a narrow repair and does not activate the + broad audit unless wider repository health is requested or the localized + premise is disproven. +- For a distributed publication, load `interdependent-work-graph` and + `distributed-publication`; this skill then owns the enclosing audit/repair + lifecycle, not source authority. + +## Validation + +A correct use demonstrates: + +- exact starting identity and governing authority; +- explicit applicability decisions; +- findings classified before mutation; +- no product patch justified solely by an environment failure; +- no green placeholder represented as verification; +- repair at the owning layer; +- deprecated paths removed when replacement is proven; +- clean repeated gates and artifact hygiene; +- distinct PR, merge, release, and deployment claims; and +- visible `hmmm` for every unfinished boundary. + +## Anti-patterns + +- Guessing which repository the user meant. +- Treating a failed local installation as a source defect. +- Treating recent green CI as proof without opening the claimed gates. +- Adding fashionable checks unrelated to repository claims. +- Repairing upstream content to compensate for consumer behavior. +- Leaving invalid deprecated scaffolding because the active build ignores it. +- Publishing a partial remote-source graph while claiming current completeness. +- Mixing generated audit artifacts into the repair diff. +- Opening a pull request and calling the repository repaired before its + authoritative checks settle. +- Calling a merge a deployment. + +## Canon basis + +The initial accepted workflow was distilled from the audit and repair of +`The-Interdependency/The-Interdependency.github.io`, merged in +[PR #50](https://github.com/The-Interdependency/The-Interdependency.github.io/pull/50) +at `238595b`. That case supplies implemented evidence for the distinctions +above; it does not make its Eleventy, GitHub Pages, or publication-specific +commands universal. + +## hmmm + +- A portable executor that discovers repository-specific audit commands across + ecosystems is not yet selected; this skill governs agent behavior, while each + repository remains authoritative for its executable gates. +- Manual semantic review can expose false-green checks that static workflow + inspection misses; the minimum automatable depth remains repository-specific. diff --git a/.agents/skills/risk-boundary-build/SKILL.md b/.agents/skills/risk-boundary-build/SKILL.md new file mode 100644 index 0000000..c333bca --- /dev/null +++ b/.agents/skills/risk-boundary-build/SKILL.md @@ -0,0 +1,100 @@ +--- +name: risk-boundary-build +description: Self-declaring runtime risk and permission boundaries built on msdmd. Each module records auth, storage, network, user-data, admin, and operational effects in a `# === BOUNDARIES ===` block; a runner audits sensitive files, reports unresolved `hmmm` boundaries, and surfaces visible coverage gaps. Load this when touching code with permissions, persistence, network calls, user data, admin behavior, migrations, or other risk-bearing effects. +--- + +# risk-boundary-build — Runtime boundaries on msdmd + +`risk-boundary-build` is an application of [msdmd](../msdmd/SKILL.md). It +turns hidden permission, storage, network, and user-data effects into +module-local declarations that can be reviewed before an agent edits a +sensitive file. + +This complements `meta-module-build`: MODULE_BUILD describes intended +boundaries before new work starts; BOUNDARIES records the actual runtime +boundary of an existing module. + +Implementation status: this skill defines the `BOUNDARIES` block and runner +contract. This repo does not currently ship a BOUNDARIES runner script; +consuming repos should implement the contract below with local risk heuristics. + +## The block + +```python +# === BOUNDARIES === +# id: chat_route_user_data_boundary +# summary: reads user-owned chat rows for the authenticated requester +# auth_boundary: read +# storage_boundary: read +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# pii: possible +# owner: platform-runtime +# === END BOUNDARIES === +``` + +## Field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Stable boundary declaration id. | +| `summary` | One-sentence description of the sensitive behavior. | +| `auth_boundary` | `none`, `read`, `write`, `admin`, or `hmmm`. | +| `storage_boundary` | `none`, `read`, `write`, `delete`, `migration`, or `hmmm`. | +| `network_boundary` | `none`, `internal`, `external`, or `hmmm`. | +| `user_data_boundary` | `none`, `read`, `write`, `delete`, or `hmmm`. | +| `admin_only` | `true`, `false`, or `hmmm`. | + +Optional: + +| Field | Meaning | +|---|---| +| `pii` | `none`, `possible`, `direct`, `sensitive`, or `hmmm`. | +| `secrets` | `none`, `read`, `write`, or `hmmm`. | +| `side_effects` | Comma-separated side effects (`email`, `webhook`, `billing`, `job`, `cache`, etc.). | +| `review_required` | Person, role, team, or condition required before edits. | +| `owner` | Person, role, or team responsible for the boundary declaration. | +| `requires` | Comma-separated BOUNDARIES or MODULE_BUILD ids this declaration depends on. | +| `since` | Version or date the declaration was added. | + +## Runner contract + +A BOUNDARIES runner MUST: + +1. Parse every `BOUNDARIES` block with the universal msdmd parser. +2. Report required fields containing `hmmm` as unresolved boundary objects. +3. Report modules with likely sensitive imports or filenames but no + BOUNDARIES block as visible gaps. +4. Support strict mode where gaps or any required `hmmm` boundary fail. +5. Exit non-zero for malformed required fields, invalid enum values, or + strict-mode unresolved boundaries. + +Sensitive-file heuristics MAY include auth/session imports, database clients, +network clients, migration filenames, admin routes, payment/billing modules, +secret managers, and user-data models. Heuristics are advisory: they create +review visibility, not proof of risk. + +## Agent behavior + +When this skill is loaded before editing code: + +- Read the BOUNDARIES block before changing implementation. +- If a required boundary is `hmmm`, preserve that uncertainty and call it out. +- Do not relax a boundary value (`admin` → `read`, `external` → `internal`, etc.) + unless the code change actually removes the effect. +- If the edit adds a new sensitive effect, update the block in the same diff. + +## Anti-patterns + +- Treating `none` as a default. Unknown is `hmmm`, not `none`. +- Recording intended boundaries in BOUNDARIES before code exists; use MODULE_BUILD first. +- Hiding risk in prose comments instead of structured fields. +- Letting heuristic gap detection replace explicit owner review. + +hmmm +- exact sensitive-import heuristic lists per framework +- whether strict mode should fail all `hmmm` boundaries or only user-data/admin ones +- how to represent read-only analytics on anonymized aggregate data diff --git a/.agents/skills/skill-build/SKILL.md b/.agents/skills/skill-build/SKILL.md new file mode 100644 index 0000000..d3778e8 --- /dev/null +++ b/.agents/skills/skill-build/SKILL.md @@ -0,0 +1,122 @@ +--- +name: skill-build +description: Skill authoring and skill compliance workflow for The Interdependency skill-lib. Load this when creating a new SKILL.md, revising an existing skill, bringing repo skills into a shared compliance shape, designing a skill-specific test suite, deciding whether a skill is metadata-block or procedural, or asking the question set required before a skill can be accepted. +--- + +# skill-build — build skills that can build better skills + +Use this skill when authoring or auditing skills in `skill-lib` or repo-local `.agents/skills/` copies. It is itself the worked example: concise trigger frontmatter, bounded workflow, reusable question set, individualized test-suite prompts, explicit output shape, and an honest `hmmm` continuation boundary. + +## Core contract + +- Start from existing examples in this repo before inventing a new shape. +- Keep `SKILL.md` lean: put only activation rules, doctrine, workflow, output shape, validation, and essential examples in the main file. +- Prefer references, examples, templates, or scripts only when they reduce repeated context or make validation more reliable. +- Make the description load-bearing: say exactly when to load the skill. +- Every skill must answer: **what task triggers it, what context it needs, what it changes, what it refuses to guess, how output should look, and how success is tested**. +- Unknown or unresolved fields become `hmmm`, not silence and not invention. + +## Existing examples to inspect first + +Choose the closest existing sibling before writing: + +- **Foundation / parser convention**: `msdmd/SKILL.md`. +- **Metadata-block skill with runner contract**: `test-build/SKILL.md`, then `doc-build/SKILL.md`, `cap-build/SKILL.md`, `deps-build/SKILL.md`, `owner-build/SKILL.md`, `risk-boundary-build/SKILL.md`, `llms-build/SKILL.md`, or `typed-meta-frontend/SKILL.md`. +- **Metadata-first module planning**: `meta-module-build/SKILL.md`. +- **Procedural doctrine skill**: `canon/SKILL.md`, `visitor-intro/SKILL.md`, `char-compress/SKILL.md`, `plain-lens/SKILL.md`, `loop-eng/SKILL.md`, or `the-interdependency/SKILL.md`. +- **Repo-distribution guidance**: `AGENTS.md`, `README.md`, `skills.json`, `ORG_DISTRIBUTION.md`, and `CLAUDE.md`. + +Do not copy a sibling mechanically. Extract its structure, then individualize the questions, tests, and boundaries for the new skill. + +## Required question set + +Ask or answer these before writing a new skill or compliance patch: + +1. **Trigger** — What exact user requests, repo contexts, file types, or phrases should load this skill? +2. **Non-trigger** — What similar requests should *not* load it? +3. **Kind** — Is this a metadata-block skill, a procedural skill, or a rare helper-only skill? If metadata-block, what block name does it own? +4. **Source of truth** — Which existing file, repo, doctrine, API, schema, or workflow is authoritative? +5. **Inputs** — What files, user facts, environment facts, or external references must the agent inspect before acting? +6. **Workflow** — What ordered steps must the agent follow? Which steps are mandatory versus situational? +7. **Outputs** — What final artifact shape should the agent produce: patch, report, generated file, checklist, command output, handoff, UI, or test result? +8. **Validation** — What tests, checks, drift gates, snapshots, or human-review prompts prove the skill worked? +9. **Failure modes** — What common bad outputs should the skill prevent? +10. **Degree of freedom** — Should the skill give high-level heuristics, a constrained recipe, or deterministic scripts? +11. **Progressive disclosure** — What belongs in `SKILL.md`, and what should move into `references/`, `examples/`, `assets/`, or `scripts/`? +12. **Security / safety / permissions** — Does the skill touch secrets, user data, money, auth, network, deployment, destructive writes, or policy-sensitive claims? +13. **Accessibility / usability** — Does the skill need plain-language output, keyboard/form accessibility, static fallback, examples, or newcomer guidance? +14. **Canon boundary** — Which claims are declared, implemented, inferred, desired, or `hmmm`? +15. **Maintenance** — Which indexes, README tables, generated files, propagation docs, or drift checks must change with the skill? + +If an answer is unknown, record `hmmm` and design the skill so the unknown remains visible. + +## Individualized test-suite question set + +A useful skill has tests shaped to its behavior. Ask these before declaring it done: + +1. **Activation test** — Can a test or reviewer verify that the frontmatter description contains concrete load triggers? +2. **Structure test** — Does the skill include the sections needed for its kind: load trigger, workflow, output shape, validation, anti-patterns, and `hmmm`? +3. **Example test** — Is there at least one minimal example, fixture, or sibling citation showing the intended pattern? +4. **Negative test** — Is there a prompt or fixture where the skill should *not* apply, and does the skill state that boundary? +5. **Coverage test** — For metadata-block skills, can a runner report modules/files with missing blocks as visible gaps? +6. **Schema test** — For metadata-block skills, are required and optional fields machine-checkable or at least checklist-checkable? +7. **Round-trip test** — If the skill generates or edits artifacts, can output be regenerated or checked for drift? +8. **Failure-mode test** — Is at least one likely bad output named and blocked by the skill? +9. **hmmm test** — Are unresolved constraints preserved in a visible `hmmm` section rather than erased? +10. **Repo-index test** — Does adding the skill keep `skills.json`, `README.md`, `ORG_DISTRIBUTION.md`, `AGENTS.md`, and `CLAUDE.md` in sync? +11. **Command test** — What exact local command should pass after the change? Prefer existing stdlib checks before adding dependencies. +12. **Human approval test** — What decision remains for the human, and how is that decision isolated from already-delivered work? + +## Compliance workflow for existing skills + +1. Inventory every skill from `skills.json` and every root directory containing `SKILL.md`. +2. Classify each skill as metadata-block or procedural. +3. Compare each skill against the required question set and its individualized test-suite questions. +4. Patch only one family at a time unless the user approves a broader normalization: + - metadata-block skills; + - procedural workflow skills; + - canon/theory-heavy procedural skills; + - repo index and propagation docs. +5. Keep semantic doctrine stable unless the user explicitly approves doctrinal changes. +6. Run repo drift and unit checks after every patch family. +7. Report remaining `hmmm` as living continuation work, not as failure. + +## Output shape when this skill is active + +For a proposed skill or compliance patch, answer in this shape: + +```markdown +## Fit check +- Correct / correction: ... +- Skill kind: metadata-block | procedural | hmmm + +## Questions answered +- Trigger: ... +- Source of truth: ... +- Validation: ... +- hmmm: ... + +## Proposed patch +- Files to create/update: ... +- Tests to run: ... + +## Approval needed +- ... +``` + +When editing, replace the proposal with a concise summary, tests, commit hash, and PR note. + +## Anti-patterns + +- Writing a skill that says what the topic is but not when to load or how to act. +- Copying another skill's tests without asking what success means for this skill. +- Putting long doctrine in `SKILL.md` when a reference file would preserve context better. +- Treating `hmmm` as a TODO list to hide rather than a boundary object to preserve. +- Updating a skill directory without updating the repo index and distribution docs. +- Adding a deterministic runner contract without either shipping a runner or clearly saying it is a consuming-repo contract. + +hmmm +- Baseline compliance is now checkable with `python tools/check_skill_compliance.py`; fuller family-by-family normalization still needs human approval before doctrine-shaped edits. +- Whether future compliance should be enforced by a new checker script, by extending existing drift checks, or by human review only. +- The exact minimum section set for every existing historical skill is not yet canon; this skill supplies the question set first, then lets the compliance pass reveal the honest shape. +- A skill that teaches skill-building is a ladder carrying a small pocket ladder; suspicious, but surprisingly useful near roofs. diff --git a/.agents/skills/skill-usage/SKILL.md b/.agents/skills/skill-usage/SKILL.md new file mode 100644 index 0000000..3e1e422 --- /dev/null +++ b/.agents/skills/skill-usage/SKILL.md @@ -0,0 +1,98 @@ +--- +name: skill-usage +description: Record and report evidence-bearing usage maturity for skills installed from The Interdependency skill-lib. Load this when any other skill-lib skill is invoked, and when asked for skill counts, usage history, maturity, reliability, experimental status, field-test status, operational status, or daily-use status. +--- + +# skill-usage — count exposure without counterfeiting trust + +Use this procedural skill alongside every invoked `skill-lib` skill. It records +local usage in the plugin's writable data directory and derives an effective +maturity designation from both exposure and available outcome evidence. + +## Workflow + +1. Identify each `skill-lib` skill actually loaded for the task. Do not count a + skill merely because its metadata appeared in context. +2. After the skill has materially shaped the work, record one use: + + ```bash + python "$PLUGIN_ROOT/tools/skill_usage.py" record \ + --state "$PLUGIN_DATA/usage.json" \ + --outcome + ``` + +3. Use `hmmm` when the task outcome is not yet observable. Never translate + silence, continuation, or lack of complaint into success. +4. Add `--critical` only when the use produced or failed to prevent a + load-bearing error. A critical failure caps effective maturity at + `field-test` until explicitly resolved. +5. To resolve a previously recorded critical failure: + + ```bash + python "$PLUGIN_ROOT/tools/skill_usage.py" resolve-critical \ + --state "$PLUGIN_DATA/usage.json" + ``` + +6. Report the spectrum with: + + ```bash + python "$PLUGIN_ROOT/tools/skill_usage.py" status \ + --state "$PLUGIN_DATA/usage.json" + ``` + +Outside an installed plugin, omit `--state`; the runner writes +`.skill-lib/usage.json` under the current working directory. + +## Designations + +| Designation | Exposure threshold | +|---|---:| +| `experimental` | 0–9 uses | +| `field-test` | 10–24 uses | +| `operational` | 25–49 uses | +| `reliable` | 50–99 uses | +| `daily-use` | 100+ uses | + +The exposure threshold is the nominal designation. The effective designation +may be lower: + +- fewer than five assessed outcomes caps maturity at `field-test`; +- assessed success below 80% caps maturity at `field-test`; +- assessed success below 90% caps maturity at `operational`; +- assessed success below 95% caps maturity at `reliable`; +- an unresolved critical failure caps maturity at `field-test`. + +`success` contributes to assessed success. `corrected`, `failed`, and +`abandoned` are assessed non-success outcomes. `hmmm` is counted exposure but +does not enter the success-rate denominator. + +## Output + +Return the skill name, use count, outcome counts, nominal designation, +effective designation, unresolved critical failures, last-used time, and state +path. Keep nominal exposure separate from evidence-qualified maturity. + +## Validation + +Run: + +```bash +python -m unittest tests.test_skill_usage +python tools/skill_usage.py status --state /tmp/skill-lib-usage-test.json +``` + +## Anti-patterns + +- Counting metadata visibility as use. +- Incrementing more than once for one skill's contribution to one task. +- Recording success before the result is observable. +- Treating popularity as reliability. +- Committing personal usage state to the canonical repository. + +hmmm + +- Codex does not currently provide a documented, authoritative + `SkillActivated` lifecycle-hook event. Recording therefore depends on the + loaded skill following this protocol. +- Cross-device aggregation requires a future consent-bearing writable service; + the current counter is local to each plugin installation. diff --git a/.agents/skills/sql-queries/SKILL.md b/.agents/skills/sql-queries/SKILL.md new file mode 100644 index 0000000..ae1653f --- /dev/null +++ b/.agents/skills/sql-queries/SKILL.md @@ -0,0 +1,454 @@ +--- +name: sql-queries +description: Write correct, performant SQL across all major data warehouse dialects (Snowflake, BigQuery, Databricks, PostgreSQL, etc.). Use this when writing queries, optimizing slow SQL, translating between dialects, or building complex analytical queries with CTEs, window functions, or aggregations. +--- + +# SQL Queries Skill + +Write correct, performant, readable SQL across all major data warehouse dialects. + +## Dialect-Specific Reference + +### PostgreSQL (including Aurora, RDS, Supabase, Neon) + +**Date/time:** +```sql +-- Current date/time +CURRENT_DATE, CURRENT_TIMESTAMP, NOW() + +-- Date arithmetic +date_column + INTERVAL '7 days' +date_column - INTERVAL '1 month' + +-- Truncate to period +DATE_TRUNC('month', created_at) + +-- Extract parts +EXTRACT(YEAR FROM created_at) +EXTRACT(DOW FROM created_at) -- 0=Sunday + +-- Format +TO_CHAR(created_at, 'YYYY-MM-DD') +``` + +**String functions:** +```sql +-- Concatenation +first_name || ' ' || last_name +CONCAT(first_name, ' ', last_name) + +-- Pattern matching +column ILIKE '%pattern%' -- case-insensitive +column ~ '^regex_pattern$' -- regex + +-- String manipulation +LEFT(str, n), RIGHT(str, n) +SPLIT_PART(str, delimiter, position) +REGEXP_REPLACE(str, pattern, replacement) +``` + +**Arrays and JSON:** +```sql +-- JSON access +data->>'key' -- text +data->'nested'->'key' -- json +data#>>'{path,to,key}' -- nested text + +-- Array operations +ARRAY_AGG(column) +ANY(array_column) +array_column @> ARRAY['value'] +``` + +**Performance tips:** +- Use `EXPLAIN ANALYZE` to profile queries +- Create indexes on frequently filtered/joined columns +- Use `EXISTS` over `IN` for correlated subqueries +- Partial indexes for common filter conditions +- Use connection pooling for concurrent access + +--- + +### Snowflake + +**Date/time:** +```sql +-- Current date/time +CURRENT_DATE(), CURRENT_TIMESTAMP(), SYSDATE() + +-- Date arithmetic +DATEADD(day, 7, date_column) +DATEDIFF(day, start_date, end_date) + +-- Truncate to period +DATE_TRUNC('month', created_at) + +-- Extract parts +YEAR(created_at), MONTH(created_at), DAY(created_at) +DAYOFWEEK(created_at) + +-- Format +TO_CHAR(created_at, 'YYYY-MM-DD') +``` + +**String functions:** +```sql +-- Case-insensitive by default (depends on collation) +column ILIKE '%pattern%' +REGEXP_LIKE(column, 'pattern') + +-- Parse JSON +column:key::string -- dot notation for VARIANT +PARSE_JSON('{"key": "value"}') +GET_PATH(variant_col, 'path.to.key') + +-- Flatten arrays/objects +SELECT f.value FROM table, LATERAL FLATTEN(input => array_col) f +``` + +**Semi-structured data:** +```sql +-- VARIANT type access +data:customer:name::STRING +data:items[0]:price::NUMBER + +-- Flatten nested structures +SELECT + t.id, + item.value:name::STRING as item_name, + item.value:qty::NUMBER as quantity +FROM my_table t, +LATERAL FLATTEN(input => t.data:items) item +``` + +**Performance tips:** +- Use clustering keys on large tables (not traditional indexes) +- Filter on clustering key columns for partition pruning +- Set appropriate warehouse size for query complexity +- Use `RESULT_SCAN(LAST_QUERY_ID())` to avoid re-running expensive queries +- Use transient tables for staging/temp data + +--- + +### BigQuery (Google Cloud) + +**Date/time:** +```sql +-- Current date/time +CURRENT_DATE(), CURRENT_TIMESTAMP() + +-- Date arithmetic +DATE_ADD(date_column, INTERVAL 7 DAY) +DATE_SUB(date_column, INTERVAL 1 MONTH) +DATE_DIFF(end_date, start_date, DAY) +TIMESTAMP_DIFF(end_ts, start_ts, HOUR) + +-- Truncate to period +DATE_TRUNC(created_at, MONTH) +TIMESTAMP_TRUNC(created_at, HOUR) + +-- Extract parts +EXTRACT(YEAR FROM created_at) +EXTRACT(DAYOFWEEK FROM created_at) -- 1=Sunday + +-- Format +FORMAT_DATE('%Y-%m-%d', date_column) +FORMAT_TIMESTAMP('%Y-%m-%d %H:%M:%S', ts_column) +``` + +**String functions:** +```sql +-- No ILIKE, use LOWER() +LOWER(column) LIKE '%pattern%' +REGEXP_CONTAINS(column, r'pattern') +REGEXP_EXTRACT(column, r'pattern') + +-- String manipulation +SPLIT(str, delimiter) -- returns ARRAY +ARRAY_TO_STRING(array, delimiter) +``` + +**Arrays and structs:** +```sql +-- Array operations +ARRAY_AGG(column) +UNNEST(array_column) +ARRAY_LENGTH(array_column) +value IN UNNEST(array_column) + +-- Struct access +struct_column.field_name +``` + +**Performance tips:** +- Always filter on partition columns (usually date) to reduce bytes scanned +- Use clustering for frequently filtered columns within partitions +- Use `APPROX_COUNT_DISTINCT()` for large-scale cardinality estimates +- Avoid `SELECT *` -- billing is per-byte scanned +- Use `DECLARE` and `SET` for parameterized scripts +- Preview query cost with dry run before executing large queries + +--- + +### Redshift (Amazon) + +**Date/time:** +```sql +-- Current date/time +CURRENT_DATE, GETDATE(), SYSDATE + +-- Date arithmetic +DATEADD(day, 7, date_column) +DATEDIFF(day, start_date, end_date) + +-- Truncate to period +DATE_TRUNC('month', created_at) + +-- Extract parts +EXTRACT(YEAR FROM created_at) +DATE_PART('dow', created_at) +``` + +**String functions:** +```sql +-- Case-insensitive +column ILIKE '%pattern%' +REGEXP_INSTR(column, 'pattern') > 0 + +-- String manipulation +SPLIT_PART(str, delimiter, position) +LISTAGG(column, ', ') WITHIN GROUP (ORDER BY column) +``` + +**Performance tips:** +- Design distribution keys for collocated joins (DISTKEY) +- Use sort keys for frequently filtered columns (SORTKEY) +- Use `EXPLAIN` to check query plan +- Avoid cross-node data movement (watch for DS_BCAST and DS_DIST) +- `ANALYZE` and `VACUUM` regularly +- Use late-binding views for schema flexibility + +--- + +### Databricks SQL + +**Date/time:** +```sql +-- Current date/time +CURRENT_DATE(), CURRENT_TIMESTAMP() + +-- Date arithmetic +DATE_ADD(date_column, 7) +DATEDIFF(end_date, start_date) +ADD_MONTHS(date_column, 1) + +-- Truncate to period +DATE_TRUNC('MONTH', created_at) +TRUNC(date_column, 'MM') + +-- Extract parts +YEAR(created_at), MONTH(created_at) +DAYOFWEEK(created_at) +``` + +**Delta Lake features:** +```sql +-- Time travel +SELECT * FROM my_table TIMESTAMP AS OF '2024-01-15' +SELECT * FROM my_table VERSION AS OF 42 + +-- Describe history +DESCRIBE HISTORY my_table + +-- Merge (upsert) +MERGE INTO target USING source +ON target.id = source.id +WHEN MATCHED THEN UPDATE SET * +WHEN NOT MATCHED THEN INSERT * +``` + +**Performance tips:** +- Use Delta Lake's `OPTIMIZE` and `ZORDER` for query performance +- Leverage Photon engine for compute-intensive queries +- Use `CACHE TABLE` for frequently accessed datasets +- Partition by low-cardinality date columns + +--- + +## Common SQL Patterns + +### Window Functions + +```sql +-- Ranking +ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) +RANK() OVER (PARTITION BY category ORDER BY revenue DESC) +DENSE_RANK() OVER (ORDER BY score DESC) + +-- Running totals / moving averages +SUM(revenue) OVER (ORDER BY date_col ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as running_total +AVG(revenue) OVER (ORDER BY date_col ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) as moving_avg_7d + +-- Lag / Lead +LAG(value, 1) OVER (PARTITION BY entity ORDER BY date_col) as prev_value +LEAD(value, 1) OVER (PARTITION BY entity ORDER BY date_col) as next_value + +-- First / Last value +FIRST_VALUE(status) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +LAST_VALUE(status) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) + +-- Percent of total +revenue / SUM(revenue) OVER () as pct_of_total +revenue / SUM(revenue) OVER (PARTITION BY category) as pct_of_category +``` + +### CTEs for Readability + +```sql +WITH +-- Step 1: Define the base population +base_users AS ( + SELECT user_id, created_at, plan_type + FROM users + WHERE created_at >= DATE '2024-01-01' + AND status = 'active' +), + +-- Step 2: Calculate user-level metrics +user_metrics AS ( + SELECT + u.user_id, + u.plan_type, + COUNT(DISTINCT e.session_id) as session_count, + SUM(e.revenue) as total_revenue + FROM base_users u + LEFT JOIN events e ON u.user_id = e.user_id + GROUP BY u.user_id, u.plan_type +), + +-- Step 3: Aggregate to summary level +summary AS ( + SELECT + plan_type, + COUNT(*) as user_count, + AVG(session_count) as avg_sessions, + SUM(total_revenue) as total_revenue + FROM user_metrics + GROUP BY plan_type +) + +SELECT * FROM summary ORDER BY total_revenue DESC; +``` + +### Cohort Retention + +```sql +WITH cohorts AS ( + SELECT + user_id, + DATE_TRUNC('month', first_activity_date) as cohort_month + FROM users +), +activity AS ( + SELECT + user_id, + DATE_TRUNC('month', activity_date) as activity_month + FROM user_activity +) +SELECT + c.cohort_month, + COUNT(DISTINCT c.user_id) as cohort_size, + COUNT(DISTINCT CASE + WHEN a.activity_month = c.cohort_month THEN a.user_id + END) as month_0, + COUNT(DISTINCT CASE + WHEN a.activity_month = c.cohort_month + INTERVAL '1 month' THEN a.user_id + END) as month_1, + COUNT(DISTINCT CASE + WHEN a.activity_month = c.cohort_month + INTERVAL '3 months' THEN a.user_id + END) as month_3 +FROM cohorts c +LEFT JOIN activity a ON c.user_id = a.user_id +GROUP BY c.cohort_month +ORDER BY c.cohort_month; +``` + +### Funnel Analysis + +```sql +WITH funnel AS ( + SELECT + user_id, + MAX(CASE WHEN event = 'page_view' THEN 1 ELSE 0 END) as step_1_view, + MAX(CASE WHEN event = 'signup_start' THEN 1 ELSE 0 END) as step_2_start, + MAX(CASE WHEN event = 'signup_complete' THEN 1 ELSE 0 END) as step_3_complete, + MAX(CASE WHEN event = 'first_purchase' THEN 1 ELSE 0 END) as step_4_purchase + FROM events + WHERE event_date >= CURRENT_DATE - INTERVAL '30 days' + GROUP BY user_id +) +SELECT + COUNT(*) as total_users, + SUM(step_1_view) as viewed, + SUM(step_2_start) as started_signup, + SUM(step_3_complete) as completed_signup, + SUM(step_4_purchase) as purchased, + ROUND(100.0 * SUM(step_2_start) / NULLIF(SUM(step_1_view), 0), 1) as view_to_start_pct, + ROUND(100.0 * SUM(step_3_complete) / NULLIF(SUM(step_2_start), 0), 1) as start_to_complete_pct, + ROUND(100.0 * SUM(step_4_purchase) / NULLIF(SUM(step_3_complete), 0), 1) as complete_to_purchase_pct +FROM funnel; +``` + +### Deduplication + +```sql +-- Keep the most recent record per key +WITH ranked AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY entity_id + ORDER BY updated_at DESC + ) as rn + FROM source_table +) +SELECT * FROM ranked WHERE rn = 1; +``` + +## Error Handling and Debugging + +When a query fails: + +1. **Syntax errors**: Check for dialect-specific syntax (e.g., `ILIKE` not available in BigQuery, `SAFE_DIVIDE` only in BigQuery) +2. **Column not found**: Verify column names against schema -- check for typos, case sensitivity (PostgreSQL is case-sensitive for quoted identifiers) +3. **Type mismatches**: Cast explicitly when comparing different types (`CAST(col AS DATE)`, `col::DATE`) +4. **Division by zero**: Use `NULLIF(denominator, 0)` or dialect-specific safe division +5. **Ambiguous columns**: Always qualify column names with table alias in JOINs +6. **Group by errors**: All non-aggregated columns must be in GROUP BY (except in BigQuery which allows grouping by alias) + +## Workflow + +1. Identify the target dialect first; never emit dialect-blind SQL. +2. Confirm schema facts (tables, columns, types, partitioning) before writing joins or filters. +3. Draft with CTEs for readability; qualify every column in multi-table queries. +4. Apply performance practices: sargable predicates, partition pruning, early filtering. +5. Validate: run or explain the query where possible; walk the debugging checklist on failure. + +## Anti-patterns + +- `SELECT *` in production queries — column drift silently breaks downstream contracts. +- Copy-pasting syntax across dialects without translation (e.g., `ILIKE` into BigQuery). +- Non-sargable predicates — wrapping filtered columns in functions and defeating indexes/pruning. +- Unqualified column names in joins, inviting ambiguity errors and wrong-table reads. + +## Provenance + +Imported from `anthropics/knowledge-work-plugins` @ `94e1a08` (`data/skills/sql-queries/`), Apache-2.0. +Local modifications: trigger phrasing normalized to skill-lib convention; this +Workflow/Anti-patterns/Provenance/hmmm bookend appended. Upstream body above is +otherwise unmodified. See `ATTRIBUTION.md` at repo root. + +hmmm +- Dialect coverage drifts as warehouses evolve; no pinned version matrix yet. +- Advisory skill — no runner ships; whether example queries should become tested fixtures is open. +- Upstream re-sync cadence with `anthropics/knowledge-work-plugins` is undecided; drift against upstream is currently invisible. diff --git a/.agents/skills/ssh-automation/SKILL.md b/.agents/skills/ssh-automation/SKILL.md new file mode 100644 index 0000000..edb4fba --- /dev/null +++ b/.agents/skills/ssh-automation/SKILL.md @@ -0,0 +1,450 @@ +--- +name: ssh-automation +description: Fail-closed SSH automation and shell-script delivery for OpenSSH and Google Cloud. Load this when writing, reviewing, or troubleshooting non-interactive SSH scripts, CI/CD remote commands, scp/sftp/rsync-over-SSH workflows, Google Cloud IAP or OS Login automation, remote heredocs, host-key bootstrap, retries and timeouts, or one-box Cloud Shell commands that must not terminate the user's interactive shell. Do not load for one-off interactive login, server-side sshd hardening alone, or MCP-based VM control planes. +--- + +# ssh-automation — prove the endpoint before crossing it + +`ssh-automation` is a procedural skill for generating and reviewing SSH-based +operations that must remain secure, non-interactive, bounded, diagnosable, and +safe to paste into an existing terminal. It covers the local shell, the SSH +transport, the remote shell, and the target operation as distinct interpreters +with distinct failure modes. + +The central boundary is: + +```text +local shell != SSH option parser != remote shell != privileged operation +``` + +Every boundary must receive either fixed syntax or explicitly validated data. +Encryption does not make an unverified endpoint trustworthy, and correct local +quoting does not preserve remote argument boundaries automatically. + +## Trigger / non-trigger + +Load this skill for: + +- Bash or shell scripts that invoke `ssh`, `scp`, `sftp`, or `rsync -e ssh`; +- CI/CD, deployment, backup, maintenance, or diagnostic commands over SSH; +- host-key provisioning, `known_hosts`, SSH certificates, jump hosts, or IAP; +- remote commands, quoted heredocs, stdin forwarding, PTY selection, or sudo; +- bounded retry, timeout, idempotency, locking, rollback, or SSH exit handling; +- a large Cloud Shell or terminal paste that contains SSH or remote setup work; +- review of a script that uses `ssh-keyscan`, `sshpass`, agent forwarding, or + relaxed host-key checking. + +Do not load it for: + +- one ordinary interactive login with no generated or repeatable automation; +- server-side `sshd_config` hardening when no client automation is involved; +- a generic deployment design that uses no SSH transport; +- an MCP-based VM control plane; use `vm-mcp` when that skill exists and is the + actual requested boundary. + +## Kind and source of truth + +This is a **procedural skill**. It defines no new msdmd block and ships no SSH +wrapper whose defaults could silently become environment authority. + +Source priority: + +1. observed target identity, operating system, account, privilege model, and + recovery path; +2. the exact OpenSSH client version and its current official manual pages; +3. the cloud provider's current official access, identity, tunnel, and audit + documentation; +4. this skill's doctrine and reviewed templates; +5. `hmmm` for unresolved endpoint identity, host-key provenance, privilege, or + rollback behavior. + +Primary-source research used to establish this version is recorded in +[`references/primary-sources.md`](references/primary-sources.md). + +## Required declarations before generating a script + +Resolve or visibly mark all of these: + +```text +execution_mode: saved-script | CI | one-box-paste | interactive-assisted +local_shell: bash version or hmmm +target: exact hostname / instance identity / account +target_selection: fixed | validated allowlist | hmmm +network_path: direct | ProxyJump | VPN | Google IAP | other +host_trust: verified known_hosts | host CA | bounded TOFU | hmmm +authentication: explicit identity | short-lived certificate | OS Login | other +remote_shell: bash | POSIX sh | fixed program | hmmm +stdin_use: none | remote script | data stream +pty: disabled | required with reason +privilege: none | sudo -n named command | other +operation_idempotent: true | false | hmmm +retry_contract: none | bounded transport retry +rollback: exact action | not applicable | hmmm +secrets: locations and redaction boundary +``` + +Do not silently choose a username, project, zone, host, key, `known_hosts` +source, remote shell, or privileged command. + +## Canonical client posture + +Start from an option array so each local argument remains one argument: + +```bash +SSH_OPTS=( + -T + -o BatchMode=yes + -o IdentitiesOnly=yes + -o StrictHostKeyChecking=yes + -o "UserKnownHostsFile=${KNOWN_HOSTS}" + -o ConnectTimeout=10 + -o ConnectionAttempts=1 + -o ServerAliveInterval=15 + -o ServerAliveCountMax=3 + -o ForwardAgent=no + -o ClearAllForwardings=yes +) +``` + +Adjust values to the observed environment. Preserve these rules: + +- `BatchMode=yes` makes missing credentials or host confirmation fail instead of + prompting inside automation. +- `IdentitiesOnly=yes` limits authentication to configured identity and + certificate files instead of offering every identity in an agent. +- `StrictHostKeyChecking=yes` is the production default. Supply a dedicated, + verified `known_hosts` file or host-certificate authority. +- `ForwardAgent=no` is the default. Prefer `ProxyJump`, a VPN, or a provider + tunnel to exposing the local authentication agent to another host. +- `-T` / `RequestTTY=no` is the automation default. Do not allocate a PTY merely + to make an interactive sudo policy appear to work. +- `ConnectionAttempts=1` leaves retry policy to one visible outer loop rather + than multiplying nested retries. +- encrypted server-alive probes bound dead sessions; they are not an + application-level health check. +- `ClearAllForwardings=yes` prevents inherited client configuration from opening + tunnels the script did not declare. When forwarding is intentionally used, + declare it explicitly and add `ExitOnForwardFailure=yes`. + +Inspect the effective configuration before relying on it: + +```bash +ssh -G "${SSH_OPTS[@]}" "${TARGET}" >ssh-effective-config.txt +``` + +Review at least `hostname`, `user`, `port`, identity files, proxy/jump settings, +host-key files, strict-host-key policy, forwarding, PTY, and agent forwarding. + +## Host identity and trust + +|∆|Host-key verification is endpoint authentication, not optional noise.|∆| + +Use one of these, strongest first: + +1. a trusted SSH host certificate authority; +2. exact host keys provisioned through an authenticated, independent channel; +3. provider-managed identity and access flow whose trust boundary has been + explicitly inspected; +4. `StrictHostKeyChecking=accept-new` only as a declared, bounded trust-on-first- + use bootstrap where substitution risk is accepted. It must still reject a + changed host key. + +`ssh-keyscan` retrieves what the network presents. It does **not** authenticate +that result. Compare its fingerprints through an independent trusted channel +before installing them. Do not create `known_hosts` with an unverified +`ssh-keyscan` pipeline and then describe the connection as verified. + +Reject these defaults: + +```text +StrictHostKeyChecking=no +StrictHostKeyChecking=off +UserKnownHostsFile=/dev/null +an unverified ssh-keyscan result +``` + +A changed host key is a stop condition. Investigate and rotate trust through a +separate authenticated path; do not delete the old entry simply to make the +script continue. + +## Authentication and privilege + +- Use a dedicated least-privileged automation principal. +- Prefer short-lived certificates, hardware-backed identities, or managed + identity such as Google OS Login over long-lived copied private keys. +- Never place a password, private key, service-account JSON key, OAuth refresh + token, or passphrase in source, arguments, logs, or prompt text. +- Do not use `sshpass` as an automation foundation. +- Set `umask 077`; create temporary credential/config directories with + `mktemp -d`; remove them in a trap; disable tracing around secrets. +- Use `sudo -n` so privilege failure is immediate and non-interactive. Grant the + narrow named operation, not `sudo bash -c `. +- Never solve sudo prompting by forcing a PTY and piping a password. + +## Remote command and data boundary + +OpenSSH remote command arguments are joined with spaces before sending one command +string to the server. A local array therefore does **not** become a remote argv +array. Treat the remote side as a second shell parse. + +Preferred shape: fixed remote interpreter plus a locally literal script. + +```bash +ssh "${SSH_OPTS[@]}" "${TARGET}" 'bash -se' <<'REMOTE' +set -Eeuo pipefail + +printf 'remote host: %s\n' "$(hostname)" +# Fixed reviewed operations only. +REMOTE +``` + +The quoted `REMOTE` delimiter prevents the local shell from expanding remote +variables, substitutions, or backslashes. Use `sh -se` only when the script is +actually POSIX shell. Name the interpreter explicitly; do not depend on an +unknown login shell or profile. + +For dynamic data: + +- validate identifiers against a closed allowlist whenever possible; +- transfer structured data as a separate file with `sftp`, `scp`, `rsync`, or a + checked stream, then invoke a fixed remote command that consumes it; +- verify a digest before activation when the file controls deployment; +- never append untrusted text to a remote command string; +- do not use `eval` on either side; +- treat `printf %q` as Bash-specific encoding that still requires the remote + Bash version and tests; it is not the default cross-shell protocol. + +## stdin and PTY rules + +- Use `-T` for scripts unless a reviewed command genuinely requires a terminal. +- Use `-n` / `StdinNull=yes` only when SSH must not consume stdin. +- Do **not** use `-n` when stdin carries a remote heredoc, archive, or data + stream. +- Do not combine a remote script and unrelated data on the same stdin without a + defined framing protocol. +- A transparent no-PTY session is preferred for machine-readable and binary + streams. + +## Retry, timeout, and exit semantics + +OpenSSH returns the remote command's status, or `255` when SSH itself reports an +error. A remote program can also return `255`; that value alone cannot always +prove a transport failure. + +Retry only when all are true: + +```text +operation is idempotent or protected by a durable operation identifier +the failure is classified as retryable by an explicit contract +attempt count is bounded +backoff is bounded +the final failure is returned unchanged +``` + +Use one retry layer. Do not combine OpenSSH connection retries, shell loops, +workflow-engine retries, and supervisor retries without calculating their +product. When a remote operation may legitimately return `255`, use a structured +receipt or a wrapper with reserved application statuses before treating `255` +as transport-only. + +## Idempotency, locking, activation, and rollback + +For mutations, separate the operation into: + +```text +inspect -> acquire lock -> stage -> verify -> atomically activate -> health check + -> record receipt -> release lock + \-> rollback on failed verification/health +``` + +Recommended practices: + +- use `flock` or an application-native lock to prevent concurrent deployment; +- upload to a temporary path on the destination filesystem; +- verify digest, ownership, mode, and expected identity; +- use an atomic rename for activation when the filesystem permits it; +- restart only the named service through `sudo -n`; +- run a real readiness/health check after activation; +- preserve the previous known-good release until the new release passes; +- run the script twice in testing and prove the second execution is safe; +- emit an operation receipt with target identity, source identity, action, + status, timestamps, and rollback result—never secrets. + +## Safe one-box terminal delivery + +A large paste must not install shell options, traps, `exit`, or `exec` into the +user's current interactive shell. Run the paste inside a child shell: + +```bash +bash <<'LOCAL' +set -Eeuo pipefail +trap 'rc=$?; printf "failed: %s (status %s)\n" "$BASH_COMMAND" "$rc" >&2' ERR + +# Commands, including SSH, run in this child shell. + +LOCAL +``` + +Rules for copy-paste boxes: + +- quote the `LOCAL` delimiter so the current shell does not pre-expand content; +- do not place `set -e`, `exit`, `exec`, option-changing `shopt`, or broad traps + directly in the user's interactive shell; +- do not put `read`, `select`, or another stdin prompt inside a multi-line paste: + following pasted lines may be consumed as the answer; +- perform any necessary confirmation as a separate command before the large + paste, or derive confirmation from an already selected, visibly printed + project/target; +- keep the child shell open only for the operation; returning or exiting from it + must leave the parent Cloud Shell alive; +- print the exact failing command and preserve its status without printing + secrets. + +## Google Cloud profile + +For Google Compute Engine: + +- prefer OS Login for account lifecycle and Identity and Access Management + authorization; +- require multi-factor authentication where applicable; +- prefer Identity-Aware Proxy TCP forwarding and VMs without public SSH ingress; +- force the path with `gcloud compute ssh ... --tunnel-through-iap` when IAP is + the declared network route; +- scope the firewall to IAP's documented TCP-forwarding source range and port + `22`; remove broad default SSH ingress when not required; +- enable IAP Data Access logs and monitor successful and failed IAP/OS Login + attempts; +- do not put service-account keys on a phone, in Cloud Shell history, or in the + script; +- preflight the active project, account, instance, zone, and access route before + mutation; +- do not add `--quiet` merely to suppress an unresolved authentication or host- + trust prompt. Make the prerequisite non-interactive first. + +IAP is an access transport, not a bulk-transfer service. Use an artifact store +or another designed transfer path for large images and datasets. + +## When shell is no longer the right implementation + +Use shell for small wrappers around established commands. Move to Python, Go, +Ansible, Terraform, cloud-init, or another structured system when: + +- the script exceeds roughly 100 lines; +- control flow, state reconciliation, parsing, or rollback becomes complex; +- multiple operating systems or shells must be supported; +- durable transactions, rich receipts, or concurrent orchestration are needed; +- correctness depends on safely transporting arbitrary structured input. + +SSH may remain the transport while a structured remote program owns semantics. +Persistent machine state should normally move into reviewed infrastructure or +configuration management rather than an ever-growing SSH paste. + +## Workflow + +1. **Classify the execution mode.** Saved script, CI job, assisted interactive + command, and one-box paste have different stdin and parent-shell boundaries. +2. **Resolve the endpoint.** Pin target, account, port, network path, host-key + provenance, and authentication identity. +3. **Resolve the remote contract.** Name interpreter/program, stdin use, PTY, + privilege, expected statuses, and application receipt. +4. **Choose the mutation contract.** Establish idempotency, lock, staged + activation, health verification, rollback, and retry eligibility. +5. **Generate the smallest command.** Use arrays locally, a fixed remote entry + point, quoted heredocs, and separately transferred dynamic data. +6. **Contain paste effects.** For a multi-line copy-paste, wrap the complete + operation in a quoted child-shell heredoc and include no embedded prompt. +7. **Validate statically.** Run syntax checks, ShellCheck, and inspect `ssh -G`. +8. **Attack the failure paths.** Exercise unknown/changed host keys, wrong + identity, unavailable network, timeout, remote nonzero, interruption, + concurrency, and rollback. +9. **Run against a disposable target.** Prove first run, second run, and failure + recovery before production. +10. **Report observed evidence.** Distinguish commands actually executed from + commands derived for another environment; carry all remaining unknowns as + `hmmm`. + +## Output shape + +When this skill is active, return or maintain: + +```text +mode: +target identity: +network path: +host trust and provenance: +authentication identity: +effective SSH options: +stdin / PTY contract: +remote command and data protocol: +privilege boundary: +idempotency / lock / retry: +verification / rollback: +copy-paste or script: +validation actually executed: +commands not yet executed: +hmmm: +``` + +Usage guidance must accompany every generated script: prerequisites, exact +invocation, expected output, failure interpretation, retry boundary, rollback, +and how to remove temporary material. + +## Validation + +Run the applicable gates: + +```bash +bash -n path/to/script.sh +shellcheck path/to/script.sh +ssh -G -F path/to/ssh_config target > /tmp/ssh-effective.txt +``` + +Then test against a disposable target: + +- verified new host, unknown host, and deliberately changed host key; +- correct identity, wrong identity, and no available identity; +- unavailable address, handshake timeout, dead session, and interrupted client; +- remote success, ordinary nonzero, and remote `255`; +- no-PTY output, required stdin, and intentionally null stdin; +- concurrent execution and lock refusal; +- first run, second run, failed health check, and successful rollback; +- logs and receipts for secret leakage; +- one-box paste failure proving the parent interactive shell remains alive. + +Static checks do not prove endpoint identity, provider authorization, remote +sudo policy, idempotency, health semantics, or rollback. Those require the +actual target or an honest `hmmm`. + +## Anti-patterns + +- `StrictHostKeyChecking=no`, `off`, or `UserKnownHostsFile=/dev/null` as a + convenience default; +- trusting `ssh-keyscan` without independent fingerprint verification; +- `sshpass`, passwords in arguments, or private keys in repositories/prompts; +- agent forwarding to avoid installing proper jump/tunnel access; +- forcing a PTY for every command; +- building a remote command string from untrusted input; +- assuming local arrays preserve remote argv; +- combining `-n` with a heredoc that must reach the remote process; +- unlimited retries, retrying non-idempotent mutation, or stacking retry layers; +- `curl ... | ssh host bash`, mutable-branch execution, or activation without a + content identity; +- passwordless generic root shells or Docker-socket access; +- placing `set -e`, `exit`, `exec`, or a `read` prompt directly in a large paste + intended for an existing interactive terminal; +- claiming success, host verification, backup, health, or rollback when the + command was only written and not executed. + +## hmmm + +- Host-key bootstrap is necessarily environment-specific; this skill refuses to + invent an authenticated channel where none has been identified. +- OpenSSH behavior is the primary client contract. PuTTY, Dropbear, platform + wrappers, and vendor-specific SSH clients need their own observed mapping. +- Exit `255` remains ambiguous when the remote application itself may emit it; + reserve application statuses or add a structured receipt where the distinction + is load-bearing. +- Exact OS Login, IAP, SSH-certificate, and audit configuration remains owned by + the current provider/project policy and must be rechecked before deployment. +- A tunnel can be secure while the command crossing it is nonsense; encryption + has never been a substitute for knowing which shell is speaking. diff --git a/.agents/skills/ssh-automation/references/primary-sources.md b/.agents/skills/ssh-automation/references/primary-sources.md new file mode 100644 index 0000000..e5801c3 --- /dev/null +++ b/.agents/skills/ssh-automation/references/primary-sources.md @@ -0,0 +1,128 @@ +# Primary sources for `ssh-automation` + +**Research date:** 2026-08-07 + +**Status:** source record for the current skill version; re-check current client +and provider documentation before changing operational defaults. + +## OpenSSH / OpenBSD manuals + +### `ssh_config(5)` + +Source: https://man.openbsd.org/ssh_config + +Load-bearing observations: + +- `BatchMode=yes` disables interactive password and host-key confirmation + prompts and is intended for scripts and batch jobs. +- `StrictHostKeyChecking=yes` refuses unknown automatic additions and changed + host keys; `accept-new` adds previously unseen keys but still rejects changed + keys; `no`/`off` can proceed with changed keys subject to restrictions. +- `IdentitiesOnly=yes` limits authentication to configured identities and + certificates rather than every identity offered by an agent. +- `ConnectTimeout`, `ConnectionAttempts`, `ServerAliveInterval`, and + `ServerAliveCountMax` expose bounded connection and dead-session behavior. +- `StdinNull=yes` / `-n` prevents SSH from reading stdin. +- `ControlPath` should contain `%h/%p/%r` or `%C` and live in a directory not + writable by other users. +- `ForwardAgent` carries explicit risk because a remote attacker able to access + the forwarded socket can use the loaded identities. +- `ClearAllForwardings` and `ExitOnForwardFailure` make forwarding state + explicit and fail when requested forwarding cannot be established. + +### `ssh(1)` + +Source: https://man.openbsd.org/ssh + +Load-bearing observations: + +- additional remote command arguments are joined with spaces before being sent + to the server; this is not a preserved remote argv boundary; +- `-T` disables pseudo-terminal allocation; +- without a PTY, the session is transparent and can reliably carry binary data; +- agent forwarding should be used cautiously and a jump host may be safer; +- SSH returns the remote command's status, or `255` when SSH reports an error. + +### `ssh-keyscan(1)` + +Source: https://man.openbsd.org/ssh-keyscan + +Load-bearing observation: + +- a `known_hosts` file built from unverified `ssh-keyscan` output leaves users + vulnerable to man-in-the-middle attacks. Scanning discovers presented keys; + verification requires an independent trusted channel. + +## Shell language and analysis + +### GNU Bash manual — redirections / here documents + +Source: https://www.gnu.org/software/bash/manual/html_node/Redirections.html + +Load-bearing observation: + +- quoting a here-document delimiter prevents expansion in the body; an unquoted + delimiter permits parameter, command, and arithmetic expansion. + +### Google Shell Style Guide + +Source: https://google.github.io/styleguide/shellguide.html + +Load-bearing observations: + +- shell is appropriate for small utilities and wrappers, not complex systems; +- scripts over roughly 100 lines or with non-straightforward control flow should + move to a structured language; +- arrays with quoted `"${array[@]}"` expansion preserve argument boundaries; +- ShellCheck is recommended for scripts large and small. + +### ShellCheck + +Source: https://github.com/koalaman/shellcheck + +Load-bearing observation: + +- ShellCheck is a static-analysis tool for shell scripts and belongs in the + validation gate; it does not prove remote identity or runtime semantics. + +## Google Cloud access and audit + +### Securing SSH access to virtual machines + +Source: https://docs.cloud.google.com/compute/docs/connect/ssh-best-practices + +Load-bearing observations: + +- use zero-trust network controls; +- restrict and promptly revoke login access; +- protect credentials with multiple factors; +- maintain a reliable SSH audit trail. + +### Identity-Aware Proxy TCP forwarding + +Source: https://docs.cloud.google.com/iap/docs/using-tcp-forwarding + +Load-bearing observations: + +- IAP provides an authenticated TCP-forwarding path; +- the documented IPv4 forwarding source range is `35.235.240.0/20`; +- port `22` may be limited to that range rather than broad public ingress; +- IAP is not intended for bulk data transfer. + +### Auditing SSH access + +Source: https://docs.cloud.google.com/compute/docs/connect/ssh-best-practices/auditing + +Load-bearing observations: + +- enable IAP Data Access logs, which are disabled by default; +- monitor successful and failed IAP and OS Login access attempts; +- export operating-system SSH logs when a complete host activity picture is + required. + +## hmmm + +- Exact enterprise host-certificate enrollment and automated rotation differ by + environment and are not specified by these generic sources. +- Provider wrappers can add behavior above OpenSSH; their evaluated effective + configuration must be observed rather than inferred. diff --git a/.agents/skills/statistical-analysis/SKILL.md b/.agents/skills/statistical-analysis/SKILL.md new file mode 100644 index 0000000..746c80a --- /dev/null +++ b/.agents/skills/statistical-analysis/SKILL.md @@ -0,0 +1,271 @@ +--- +name: statistical-analysis +description: Apply statistical methods including descriptive stats, trend analysis, outlier detection, and hypothesis testing. Use this when analyzing distributions, testing for significance, detecting anomalies, computing correlations, or interpreting statistical results. +--- + +# Statistical Analysis Skill + +Descriptive statistics, trend analysis, outlier detection, hypothesis testing, and guidance on when to be cautious about statistical claims. + +## Descriptive Statistics Methodology + +### Central Tendency + +Choose the right measure of center based on the data: + +| Situation | Use | Why | +|---|---|---| +| Symmetric distribution, no outliers | Mean | Most efficient estimator | +| Skewed distribution | Median | Robust to outliers | +| Categorical or ordinal data | Mode | Only option for non-numeric | +| Highly skewed with outliers (e.g., revenue per user) | Median + mean | Report both; the gap shows skew | + +**Always report mean and median together for business metrics.** If they diverge significantly, the data is skewed and the mean alone is misleading. + +### Spread and Variability + +- **Standard deviation**: How far values typically fall from the mean. Use with normally distributed data. +- **Interquartile range (IQR)**: Distance from p25 to p75. Robust to outliers. Use with skewed data. +- **Coefficient of variation (CV)**: StdDev / Mean. Use to compare variability across metrics with different scales. +- **Range**: Max minus min. Sensitive to outliers but gives a quick sense of data extent. + +### Percentiles for Business Context + +Report key percentiles to tell a richer story than mean alone: + +``` +p1: Bottom 1% (floor / minimum typical value) +p5: Low end of normal range +p25: First quartile +p50: Median (typical user) +p75: Third quartile +p90: Top 10% / power users +p95: High end of normal range +p99: Top 1% / extreme users +``` + +**Example narrative**: "The median session duration is 4.2 minutes, but the top 10% of users spend over 22 minutes per session, pulling the mean up to 7.8 minutes." + +### Describing Distributions + +Characterize every numeric distribution you analyze: + +- **Shape**: Normal, right-skewed, left-skewed, bimodal, uniform, heavy-tailed +- **Center**: Mean and median (and the gap between them) +- **Spread**: Standard deviation or IQR +- **Outliers**: How many and how extreme +- **Bounds**: Is there a natural floor (zero) or ceiling (100%)? + +## Trend Analysis and Forecasting + +### Identifying Trends + +**Moving averages** to smooth noise: +```python +# 7-day moving average (good for daily data with weekly seasonality) +df['ma_7d'] = df['metric'].rolling(window=7, min_periods=1).mean() + +# 28-day moving average (smooths weekly AND monthly patterns) +df['ma_28d'] = df['metric'].rolling(window=28, min_periods=1).mean() +``` + +**Period-over-period comparison**: +- Week-over-week (WoW): Compare to same day last week +- Month-over-month (MoM): Compare to same month prior +- Year-over-year (YoY): Gold standard for seasonal businesses +- Same-day-last-year: Compare specific calendar day + +**Growth rates**: +``` +Simple growth: (current - previous) / previous +CAGR: (ending / beginning) ^ (1 / years) - 1 +Log growth: ln(current / previous) -- better for volatile series +``` + +### Seasonality Detection + +Check for periodic patterns: +1. Plot the raw time series -- visual inspection first +2. Compute day-of-week averages: is there a clear weekly pattern? +3. Compute month-of-year averages: is there an annual cycle? +4. When comparing periods, always use YoY or same-period comparisons to avoid conflating trend with seasonality + +### Forecasting (Simple Methods) + +For business analysts (not data scientists), use straightforward methods: + +- **Naive forecast**: Tomorrow = today. Use as a baseline. +- **Seasonal naive**: Tomorrow = same day last week/year. +- **Linear trend**: Fit a line to historical data. Only for clearly linear trends. +- **Moving average forecast**: Use trailing average as the forecast. + +**Always communicate uncertainty**. Provide a range, not a point estimate: +- "We expect 10K-12K signups next month based on the 3-month trend" +- NOT "We will get exactly 11,234 signups next month" + +**When to escalate to a data scientist**: Non-linear trends, multiple seasonalities, external factors (marketing spend, holidays), or when forecast accuracy matters for resource allocation. + +## Outlier and Anomaly Detection + +### Statistical Methods + +**Z-score method** (for normally distributed data): +```python +z_scores = (df['value'] - df['value'].mean()) / df['value'].std() +outliers = df[abs(z_scores) > 3] # More than 3 standard deviations +``` + +**IQR method** (robust to non-normal distributions): +```python +Q1 = df['value'].quantile(0.25) +Q3 = df['value'].quantile(0.75) +IQR = Q3 - Q1 +lower_bound = Q1 - 1.5 * IQR +upper_bound = Q3 + 1.5 * IQR +outliers = df[(df['value'] < lower_bound) | (df['value'] > upper_bound)] +``` + +**Percentile method** (simplest): +```python +outliers = df[(df['value'] < df['value'].quantile(0.01)) | + (df['value'] > df['value'].quantile(0.99))] +``` + +### Handling Outliers + +Do NOT automatically remove outliers. Instead: + +1. **Investigate**: Is this a data error, a genuine extreme value, or a different population? +2. **Data errors**: Fix or remove (e.g., negative ages, timestamps in year 1970) +3. **Genuine extremes**: Keep them but consider using robust statistics (median instead of mean) +4. **Different population**: Segment them out for separate analysis (e.g., enterprise vs. SMB customers) + +**Report what you did**: "We excluded 47 records (0.3%) with transaction amounts >$50K, which represent bulk enterprise orders analyzed separately." + +### Time Series Anomaly Detection + +For detecting unusual values in a time series: + +1. Compute expected value (moving average or same-period-last-year) +2. Compute deviation from expected +3. Flag deviations beyond a threshold (typically 2-3 standard deviations of the residuals) +4. Distinguish between point anomalies (single unusual value) and change points (sustained shift) + +## Hypothesis Testing Basics + +### When to Use + +Use hypothesis testing when you need to determine whether an observed difference is likely real or could be due to random chance. Common scenarios: + +- A/B test results: Is variant B actually better than A? +- Before/after comparison: Did the product change actually move the metric? +- Segment comparison: Do enterprise customers really have higher retention? + +### The Framework + +1. **Null hypothesis (H0)**: There is no difference (the default assumption) +2. **Alternative hypothesis (H1)**: There is a difference +3. **Choose significance level (alpha)**: Typically 0.05 (5% chance of false positive) +4. **Compute test statistic and p-value** +5. **Interpret**: If p < alpha, reject H0 (evidence of a real difference) + +### Common Tests + +| Scenario | Test | When to Use | +|---|---|---| +| Compare two group means | t-test (independent) | Normal data, two groups | +| Compare two group proportions | z-test for proportions | Conversion rates, binary outcomes | +| Compare paired measurements | Paired t-test | Before/after on same entities | +| Compare 3+ group means | ANOVA | Multiple segments or variants | +| Non-normal data, two groups | Mann-Whitney U test | Skewed metrics, ordinal data | +| Association between categories | Chi-squared test | Two categorical variables | + +### Practical Significance vs. Statistical Significance + +**Statistical significance** means the difference is unlikely due to chance. + +**Practical significance** means the difference is large enough to matter for business decisions. + +A difference can be statistically significant but practically meaningless (common with large samples). Always report: +- **Effect size**: How big is the difference? (e.g., "Variant B improved conversion by 0.3 percentage points") +- **Confidence interval**: What's the range of plausible true effects? +- **Business impact**: What does this translate to in revenue, users, or other business terms? + +### Sample Size Considerations + +- Small samples produce unreliable results, even with significant p-values +- Rule of thumb for proportions: Need at least 30 events per group for basic reliability +- For detecting small effects (e.g., 1% conversion rate change), you may need thousands of observations per group +- If your sample is small, say so: "With only 200 observations per group, we have limited power to detect effects smaller than X%" + +## When to Be Cautious About Statistical Claims + +### Correlation Is Not Causation + +When you find a correlation, explicitly consider: +- **Reverse causation**: Maybe B causes A, not A causes B +- **Confounding variables**: Maybe C causes both A and B +- **Coincidence**: With enough variables, spurious correlations are inevitable + +**What you can say**: "Users who use feature X have 30% higher retention" +**What you cannot say without more evidence**: "Feature X causes 30% higher retention" + +### Multiple Comparisons Problem + +When you test many hypotheses, some will be "significant" by chance: +- Testing 20 metrics at p=0.05 means ~1 will be falsely significant +- If you looked at many segments before finding one that's different, note that +- Adjust for multiple comparisons with Bonferroni correction (divide alpha by number of tests) or report how many tests were run + +### Simpson's Paradox + +A trend in aggregated data can reverse when data is segmented: +- Always check whether the conclusion holds across key segments +- Example: Overall conversion goes up, but conversion goes down in every segment -- because the mix shifted toward a higher-converting segment + +### Survivorship Bias + +You can only analyze entities that "survived" to be in your dataset: +- Analyzing active users ignores those who churned +- Analyzing successful companies ignores those that failed +- Always ask: "Who is missing from this dataset, and would their inclusion change the conclusion?" + +### Ecological Fallacy + +Aggregate trends may not apply to individuals: +- "Countries with higher X have higher Y" does NOT mean "individuals with higher X have higher Y" +- Be careful about applying group-level findings to individual cases + +### Anchoring on Specific Numbers + +Be wary of false precision: +- "Churn will be 4.73% next quarter" implies more certainty than is warranted +- Prefer ranges: "We expect churn between 4-6% based on historical patterns" +- Round appropriately: "About 5%" is often more honest than "4.73%" + +## Workflow + +1. State the question and the decision it informs before touching data. +2. Check assumptions (distribution, independence, sample size) before choosing a test. +3. Run descriptive stats first; only then inferential tests. +4. Report effect sizes and uncertainty alongside p-values. +5. Interpret in plain language, naming what the analysis cannot conclude. + +## Anti-patterns + +- Multiple comparisons without correction — significance by sheer volume of tests. +- Reporting p-values without effect sizes, letting trivial effects masquerade as findings. +- Reading correlation as causation without design that supports it. +- Choosing the test after seeing which one flatters the data. + +## Provenance + +Imported from `anthropics/knowledge-work-plugins` @ `94e1a08` (`data/skills/statistical-analysis/`), Apache-2.0. +Local modifications: trigger phrasing normalized to skill-lib convention; this +Workflow/Anti-patterns/Provenance/hmmm bookend appended. Upstream body above is +otherwise unmodified. See `ATTRIBUTION.md` at repo root. + +hmmm +- No stance yet on Bayesian vs frequentist framing for org analyses. +- Whether standard fixtures (known-answer datasets) should validate each method is open. +- Upstream re-sync cadence with `anthropics/knowledge-work-plugins` is undecided; drift against upstream is currently invisible. diff --git a/.agents/skills/test-build/SKILL.md b/.agents/skills/test-build/SKILL.md new file mode 100644 index 0000000..1276ba4 --- /dev/null +++ b/.agents/skills/test-build/SKILL.md @@ -0,0 +1,250 @@ +--- +name: test-build +description: Self-declaring contract tests built on msdmd. Source modules own behavior obligations in `# === CONTRACTS ===` blocks; test modules own executable evidence in `# === CHECKS ===` blocks. Load this when adding tests that ride the msdmd convention, when refactoring a module with CONTRACTS/CHECKS declarations, or when authoring a contract/check audit or executor. +--- + +# test-build — Contract tests on msdmd + +`test-build` is an application of [msdmd](../msdmd/SKILL.md). The +foundational skill defines the comment-block convention, the universal +parser, and the visible-gap requirement; this skill applies the +convention to behavior contracts and their executable witnesses. + +Read `msdmd/SKILL.md` first if you haven't — the block syntax, +parser contract, and visibility rules below are inherited from there +and not redefined. + +For the ratified doctrine behind this split, see +[`doctrine/msdmd-checks.md`](../doctrine/msdmd-checks.md). + +## The split + +```text +CONTRACTS are obligations. +CHECKS are accountable witnesses. +audit reconciles the witness list against the obligation list. +``` + +Source modules own promises. Test modules own evidence. Neither owns +the other's declarations. + +## Source block: CONTRACTS + +Every module that promises behavior declares those obligations in a +`CONTRACTS` block. A contract says what must remain true; it does not +name the test topology. + +```python +# === CONTRACTS === +# id: chat_create_owner_isolation +# given: POST /api/v1/conversations with x-user-id=A and body.user_id=B +# then: stored row has user_id=A; smuggled value is dropped +# class: security +# +# id: chat_get_other_owner_404 +# given: GET /api/v1/conversations/{id} where conv.user_id != caller +# then: returns 404 (existence non-disclosure, not 403) +# class: security +# === END CONTRACTS === +``` + +### CONTRACTS field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Unique snake_case identifier, stable across refactors. Becomes the contract handle in reports. | +| `given` | Plain-English precondition / request shape. State the input, not the implementation. | +| `then` | The asserted post-condition — the actual contract, not the steps to verify it. | + +Optional: + +| Field | Meaning | +|---|---| +| `class` | Free-text tag (`security`, `correctness`, `idempotency`, `auth`, `regression`, `doctrine`, `evidence`, `safety`). The runner counts entries per class in summaries. | +| `requires` | Comma-separated list of other contract ids this contract depends on. | +| `since` | Version or date the contract was added. | +| `deprecated` | If present, the runner skips and reports the entry as deprecated. | + +`call:` is not a CONTRACTS field in skill-lib. The call belongs to the +CHECKS entry that owns the executable evidence. + +## Test block: CHECKS + +A test module declares the checks it contributes in a `CHECKS` block. +A check is an evidentiary procedure: an executable claim to prove one +or more named contracts. + +```python +# === CHECKS === +# id: check_chat_create_owner_isolation_http +# proves: chat_create_owner_isolation +# call: self::test_chat_create_owner_isolation_http +# requires: python3, posix_shell +# timeout: 20 +# mutates: db +# cleanup: transaction_rollback +# +# id: check_chat_get_other_owner_404_http +# proves: chat_get_other_owner_404 +# call: self::test_chat_get_other_owner_404_http +# requires: python3, posix_shell +# timeout: 20 +# mutates: db +# cleanup: transaction_rollback +# === END CHECKS === +``` + +### CHECKS field schema + +Required: + +| Field | Meaning | +|---|---| +| `id` | Unique snake_case identifier for this evidentiary procedure. | +| `proves` | Comma-separated contract ids this check claims to prove. "Proves" means claims-to-prove; audit verifies linkage, not mutation sensitivity. | +| `call` | Executable target resolved by the runner. In Python skill-lib checks, the sanctioned no-exec audit form is `self::fn`. | +| `mutates` | Declared side-effect surface (`none`, `filesystem`, `db`, `network`, `external_service`, etc.). | +| `cleanup` | Cleanup/isolation obligation (`none`, `tempdir_teardown`, `transaction_rollback`, `finally_delete_created_rows`, etc.). | + +Conditionally required when consumed by the runner: + +| Field | Meaning | +|---|---| +| `requires` | Comma-separated host capabilities. A runner that reads this field must refuse execution when requirements are missing. | +| `timeout` | Per-check execution bound. A runner that reads this field must apply it to the spawned work, not merely print it. | + +Fields enter the schema in the same change that makes a runner consume +them. Declared-but-unread metadata is decorative and should be treated +as a defect, not diligence. + +## The contract for check functions + +A check function: + +- Is resolvable at the path declared in `call:`. +- Takes no required arguments. The executor does not inject fixtures + or context; the check is self-contained or pulls from the language's + standard environment (env vars, a known service URL, etc.). +- Returns `None` on pass. +- Raises `AssertionError` on behavior violation. The runner reports + this as `FAIL`. +- Lets unexpected exceptions escape. The runner reports these as + `ERROR` (infrastructure/harness failure) rather than `FAIL` + (contract violation). +- Cleans up any persistent state it creates. Isolation is the check's + responsibility unless the runner explicitly provides a fixture. + +## Authoring an audit + +Audit is the cheapest runner mode: reconcile declarations without +executing checks. Resolve `self::fn` against the declaring file's +**parsed** function definitions — never by importing it or reading +loaded callables, since import executes module top level and an audit +that executes is not an audit: + +```python +import ast + +def defined_functions(source_path: str) -> set[str]: + tree = ast.parse(open(source_path, encoding="utf-8").read()) + return { + node.name + for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + +def resolve_self_call(spec: str, defined: set[str]) -> str: + if not spec.startswith("self::"): + raise LookupError(f"only self::fn resolves without execution: {spec}") + name = spec[len("self::"):] + if name not in defined: + raise LookupError(f"self:: target not defined in file: {spec}") + return name +``` + +(The bundled `tests/test_repo_loto.py` reads `globals()` instead — it +can, because its audit runs *as* that module, so its own `def`s are +already in scope. A central audit walking other test files has no such +shortcut and must parse, as above.) + +An audit MUST report, at minimum: + +```text +GAP has no CHECKS entry claiming to prove it +GAP claims unknown contract: +GAP call does not resolve: +GAP executable check has no resolving CHECKS declaration +``` + +Exit nonzero on any gap. A reconciler that has only ever said +"closed" is itself unverified; negative-test it by planting an orphan +contract, a phantom `proves` target, and an unresolvable call, then +observing the GAP. + +## Authoring an executor + +A full executor runs after audit or as part of the same command. It +should: + +1. Parse source `CONTRACTS` and test `CHECKS` using the msdmd parser. +2. Reconcile the graph before execution. +3. Refuse execution when consumed `requires` fields are unmet. +4. Apply consumed `timeout` fields to the actual spawned work. +5. Report per-check `PASS`, `FAIL`, and `ERROR` without aborting the + remaining checks on a single harness error. +6. Surface source contracts with no proving checks, checks proving + unknown contracts, and executable checks with no declaration. + +The visibility-of-gaps requirement is mandatory per msdmd. Drop it and +the runner stops being a msdmd application. + +## Semantics of "proves" + +`proves:` means claims-to-prove. Audit verifies linkage and call +resolution. A passing check demonstrates the declared witness ran +successfully. It does not prove the check is sensitive to every +possible breakage of the contract. + +Status vocabulary: + +```text +[implemented-prototype] runs; verified by session contact only +[test-backed] suite passes and audit closes the graph +[mutation-verified] checks demonstrated to notice planted breakage +``` + +Do not claim one rung above the evidence. + +## Anti-patterns + +- **Contracts in test files instead of source files.** The contract + belongs to the module that promises the behavior; the test file owns + the check. +- **`call:` in CONTRACTS.** Source modules should not know test + topology. Put executable targets in CHECKS. +- **Executable tests with no CHECKS entry.** They may still run through + ad hoc tooling, but they are invisible to the msdmd evidence graph. +- **CHECKS proving unknown CONTRACTS.** This is an orphan witness; fix + the target id or declare the source contract. +- **Implementation-shaped ids.** `chat_create_returns_200` tells you + little; `chat_create_owner_isolation` tells you what's protected. +- **Importing during audit.** Python imports execute module top level. + Use no-exec resolution such as `self::fn`, or make import execution + an explicit non-audit mode. +- **Catching unexpected exceptions in the check to "make it pass".** + Let the exception escape so the runner can mark `ERROR` honestly. + +## Versioning + +The `CONTRACTS` block name remains stable for source-owned +obligations. `CHECKS` is the paired test-owned evidence block. +Field additions are non-breaking only when they are additive and +consumed by a runner. Field renames or removals are breaking; bump the +major version and note the migration in the lib README. + +hmmm +- The block type for harness/infrastructure tests that prove no product contract remains unnamed is still unsettled. +- Mutation-level verification is defined but not yet generalized across skills. +- Slow/flaky/quarantined states should enter only when a runner consumes them rather than as decorative labels. diff --git a/.agents/skills/the-interdependency/SKILL.md b/.agents/skills/the-interdependency/SKILL.md new file mode 100644 index 0000000..9f7ba4c --- /dev/null +++ b/.agents/skills/the-interdependency/SKILL.md @@ -0,0 +1,154 @@ +--- +name: the-interdependency +description: Protocol and workflow for all tasks involving The Interdependency organization, its repositories, The Interdependent Way projects, EDCMBONE transcript analysis, code building, research, GitHub maintenance and updates. Load this whenever the task or context touches The-Interdependency assets, or on phrases like "assemble edcmbone transcripts for analysis", "write code that...", or any GitHub/research/build work on org projects. +--- + +# the-interdependency — Workflow Protocol for The Interdependency Projects + +`the-interdependency` is a procedural skill that enforces consistent, high-fidelity, structure-preserving practices when working inside The Interdependency ecosystem (org repos, The Interdependent Way artifacts, skill-lib, edcmbone, ucns, pcea, a0, aimmh, etc.). It ensures EDCMBONE analysis follows framework conventions, code and docs always carry usage guidance, GitHub ops respect org standards, and neurodivergence-compatible structure is preserved. + +## Load this when + +- Any task, research, code, or context mentions The-Interdependency, The Interdependent Way, interdependentway.org, Harrison Hovel, or any repository under the The-Interdependency GitHub organization. +- User requests include: "assemble edcmbone transcripts for analysis", "write code that..." (or similar), GitHub maintenance, updates, pushes, repo hygiene, or cross-project work. +- Building, editing, reviewing, or shipping code, specs, documentation, or analysis artifacts destined for or affecting The-Interdependency projects. +- Performing GitHub operations on org repositories (commits, branches, PRs, issues, propagation, drift checks). +- Working with skill-lib itself, canon, msdmd blocks, or propagating skills to target repos. + +## Core Doctrine + +- **Agent/work context gate**: `skill-lib` is standing context for org agents. At every agent instantiation, resolve the available skill-lib entrypoint/index plus the governing repository instructions before that agent may reason about or execute org work. At the start of every unit of work, reevaluate the request against skill descriptions and read every applicable `SKILL.md` before acting. Child/sub-agents inherit already-resolved repository identities, governing contracts, and applicable skill context from the parent, then reevaluate triggers for their own assignment. Previously resolved authoritative instructions stay resolved until their source changes, conflicts, becomes unavailable, or is explicitly superseded. Do not ask the user to restate repository knowledge that authoritative sources already resolve. If required authority cannot be resolved, stop that boundary as `hmmm`; do not guess or reconstruct stable project semantics from conversational repetition. +- **Structure preservation first**: Before any summarization, compression, decision, or output, preserve the complete relational structure, variables, topology, epistemic status (declared / implemented / inferred / hmmm), distinct layers (lived experience vs formal claims vs emotional), and explicitly mark all unresolveds. This follows the org's neurodivergence-preserving interaction principles. +- **Resource-run preflight and completion**: Resource scarcity requires contemplation **before** a compute run begins. Before launch, inspect or estimate whether available time under real external constraints, CPU, memory, disk, battery/power, network, quotas, API/tool usage limits, and session/process durability are sufficient for the run to reach its natural terminal condition. If there is material doubt that it can finish, do not start it: reduce, stage/checkpoint, relocate, acquire resources, or leave it `hmmm`. Once a healthy run begins, let it finish to completion or deterministic computational failure unless the user explicitly cancels it or an unforeseen real resource/safety emergency requires interruption. Do **not** invent or enforce a wall-clock cutoff merely to make work bounded, falsifiable, or convenient. Runtime/resource ceilings are stopping criteria only when the quantity is itself load-bearing to the hypothesis or acceptance criterion, an authorized safety boundary, or a real externally imposed hard limit, and they must be justified before launch. +- **METAPAT consultation gate**: Consult current `The-Interdependency/metapat` before committing a conceptual choice when the task must decide which distinctions, relations, boundaries, transformations, scales, or cross-domain correspondences should organize downstream work. METAPAT consultation is also required when an unresolved conceptual choice would constrain architecture, semantics, measurement, ontology, or later falsifiable claims. Do not consult METAPAT merely to execute an already-fixed implementation, run tests, repair syntax, move data, or apply a relation whose meaning and boundary are already established. METAPAT is the source of truth for its own doctrine; skill-lib routes to it and must not duplicate a frozen theory snapshot. +- **EDCMBONE transcript assembly & analysis**: When the task involves assembling or analyzing transcripts (e.g. for EDCMBONE / Energy Dissonance Circuit Model Bound Operator Numerical Evaluation), apply the established EDCMBONE lens: map energy flows and dissonance circuits, compute/report F-loss metrics (fidelity, deletion, inversion, collapse detection), tag F1–F6 failure modes, segment for cognitive accessibility (especially neurodivergent readers), and preserve transcript topology. Do not improvise assembly; extend or adhere to patterns from the edcmbone repository. +- **Code writing standards**: When writing or modifying code that touches The-Interdependency: + - Use msdmd self-declaration blocks (`# === BLOCK_NAME ===` ... `# === END BLOCK_NAME ===`) wherever the module fits an existing or new metadata skill. + - **Always include prominent usage guidance**: runnable examples, invocation patterns, integration notes, edge cases, limitations, and how the code participates in larger workflows (e.g. a0p/AIMMH orchestration, EDCMBONE analysis pipelines). + - Respect ratios, test contracts, dependency declarations, ownership, and risk boundaries per the relevant skills. + - For new modules, begin with `meta-module-build` patterns. +- **GitHub maintenance & updates**: + - Follow org conventions in `ORG_DISTRIBUTION.md` (install paths `.agents/skills/`, propagation rules). + - Before/after changes, run available drift checkers and update machine-readable indexes (`skills.json`, README tables, AGENTS.md pointers). + - Use clear commit messages that reference affected skills or the change class. + - When propagating skill-lib changes, prefer the canonical `tools/propagate_skills.py` (or equivalent) with `--apply` only after dry-run validation. +- **Usage guidance requirement**: Every code file, SKILL.md update, README change, research summary, or artifact produced under this skill **must contain clear, actionable usage guidance**. This is non-negotiable for accessibility, onboarding, and reducing signal loss. +- **Research & canon alignment**: Ground all claims in source-backed canon (cross-load `canon` skill). Use `char-compress` for context handoff. Leave genuine uncertainty as `hmmm`. + +## Operator workflow contract + +These constraints govern how work is selected and executed; they do not override repository-local authority about what a project means. + +- **Audit before assent**: Test a proposal against current code, canon, evidence, constraints, and failure modes before agreeing with it. Agreement is a conclusion, not a conversational default. +- **Preserve concepts; reject bad placement**: When a proposal is useful but architecturally misplaced, preserve the concept and move or re-scope it to the owning layer rather than either accepting the wrong placement or discarding the idea. +- **Useful, good, true**: Do not generate work merely to create activity. Prefer artifacts and actions that are useful to the stated goal, operationally sound, and truthfully supported by evidence or explicit status. +- **KISS under reality contact**: Prefer the smallest skilled design that survives actual execution. A clever mechanism that is fragile, opaque, untestable, or needlessly expensive is not simpler than a slightly longer mechanism that works. +- **Prior planning before execution**: Resolve authority, placement, dependency order, resource needs, validation, rollback, and terminal condition before expensive or destructive work begins. Planning exists to prevent avoidable failure, not to create an approval ceremony. +- **Complete within granted scope**: When the request, authority, and safety boundary already permit the next action, continue through the coherent workflow instead of repeatedly asking the operator to approve each obvious intermediate step. Ask only when a real unresolved decision cannot be recovered from authoritative sources or safely isolated as `hmmm`. +- **Usage-limit aware orchestration**: Treat model-plan limits, API quotas, tool-call limits, rate limits, context budgets, and session durability as real resources during preflight. Stage or redistribute work before launch so a workflow does not predictably die midway from exhaustion. Do not silently downgrade evidence quality merely to fit a limit. +- **Purposeful functions**: Every function, script, workflow step, and abstraction must have a defensible purpose, coherent inputs/outputs, failure behavior, and a reason to exist at that layer. Remove dead indirection and mechanisms whose only justification is that they already exist. +- **Deprecation is removal plus replacement when capability remains required**: Once a mechanism is declared deprecated, stop routing new work through it and provide or identify its supported replacement when the retired capability remains required. If the capability is intentionally retired as unnecessary, complete removal is the replacement outcome. Do not preserve deprecated behavior by default out of inertia. +- **`hmmm` is mandatory honest incompletion**: `hmmm` is the boundary object for unresolved constraints, missing authority, incomplete evidence, or a living continuation. Never erase an unresolved merely to make an artifact look finished. Where the boundary would otherwise be empty, leave a brief apropos, cogent, or humorous nonsequitur rather than silently dropping it. + +## Operational authority topology + +This section records durable ownership boundaries, not a frozen inventory of the operator's current machines, clients, logins, quotas, or provider sessions. + +- **GitHub repository boundary**: GitHub is the canonical remote source, review, and merge surface for repositories under `The-Interdependency`. GitHub Actions is validation evidence only where a repository's current workflows actually execute the claimed gates; inspect those jobs rather than inferring health from a green badge. +- **VM control-plane authority**: `skill-lib/vm-mcp` owns reusable VM MCP implementation and doctrine. Its authority profiles are deployment choices: bounded defaults remain appropriate for shared or first-contact environments, while the explicit `personal-console` profile is available for a deliberately configured single-owner private VM. The personal-console profile entered canonical skill-lib in merged #81 at `222ba4d4348022d81950c3fad054bae7e528b6a0`. Repository tests do not prove that any particular VM currently satisfies that profile. +- **Stack deployment authority**: `The-Interdependency/stack` owns stack-specific deployment and operational-use guidance. Stack's consumption of the canonical `vm-mcp` personal console entered stack in merged #12 at `22b74340d0c603883193a4ecf53e2ef3f9c3e780`. When stack deployment consumes `vm-mcp`, resolve that exact pinned skill-lib identity and the current `stack/backend/deploy` instructions before acting. No implementation or doctrine authority transfers from skill-lib into stack merely because stack consumes it. +- **Concrete host/client facts are runtime evidence**: A hostname or alias such as `a0`, a client such as Termux, Git transport/authentication method, tunnel state, installed CLI, provider login, exact version, quota, and API availability must be discovered from the current deployment/operator environment before use. This skill must not elevate those transient facts into unconditional organization-wide routing doctrine. +- **Provider execution capacity is not source authority**: OpenAI/Codex, xAI/Grok, DeepSeek/DeepCode, or another provider may be usable execution capacity when currently authenticated and within quota. Their availability must be checked at runtime, and choosing an executor does not transfer repository, semantic, mathematical, measurement, or publication authority. +- **Deprecated/stale routes do not revive themselves**: Historical services, hosts, clients, authentication paths, or provider assumptions are not automatic fallbacks. If a route is deprecated, migrate to its supported replacement and remove obsolete routing when compatibility permits; otherwise preserve the unresolved deployment boundary as `hmmm`. + +### Operational usage guidance + +Before routing work to a machine or provider: + +1. resolve the repository and exact commit that owns the work; +2. read the current deployment instructions owned by the consuming repository; +3. verify the actual host/client/authentication/tunnel/provider state; +4. choose only the authority profile and executor justified by that evidence; and +5. keep human recovery access independent where the deployment contract requires it. + +A statement like "use `a0`" is therefore a runtime/operator decision backed by current deployment evidence, not standing organization canon in this skill. + +## METAPAT consultation test + +Ask one question before conceptual or architectural commitment: + +> Am I deciding **what relation/boundary/transformation should exist or matter**, or merely implementing one already established? + +Consult METAPAT for the first case. Continue locally for the second. + +Strong consultation triggers: + +- choosing or revising an architecture-level distinction; +- deciding whether a boundary deserves independent status; +- comparing similarly shaped transformations across different domains; +- importing a domain term, metaphor, formula, or ontology into another layer; +- deciding what remains invariant across scale or representation change; +- a design choice is being mistaken for an empirical or mathematical claim, or vice versa; +- an unexplained but productive discovery path is at risk of being removed only because its mechanism is not yet known; +- two repos disagree because they encode different conceptions of the same relation rather than because of an implementation bug. + +Non-triggers: + +- routine refactors under fixed contracts; +- dependency/version updates; +- deterministic data ingestion; +- tests whose expected relation is already declared; +- formatting, documentation, packaging, CI, deployment, or syntax repair; +- independent recovery of a result after the discovery result and comparison criterion are already frozen. + +When consultation triggers, inspect the current METAPAT repository state before deciding. At minimum resolve the relevant current axioms, postulates, domain-restraint rules, and any directly applicable theory/implementation boundary. Do not import historical skill-lib `meta` wording as authority over current METAPAT. + +## Workflow + +1. **Agent/work context gate**: On agent birth, resolve skill-lib plus governing repository instructions before org work begins. On every work start, reevaluate skill triggers and load applicable contracts before reasoning or acting. Inherit resolved authority into child/sub-agents; do not make the user restate stable repository knowledge. Missing required authority is `hmmm` and blocks that boundary. +2. **Trigger detection**: Activate on any The-Interdependency context or the example trigger phrases listed in the description. +3. **Resource preflight**: Before starting any compute run, decide whether the available resources can sustain it to its natural terminal condition. If not, do not launch it. Do not substitute an arbitrary timeout for preflight judgment. +4. **METAPAT gate**: Before conceptual or architectural commitment, run the consultation test above. If triggered, inspect current METAPAT before selecting the relation, boundary, transformation, or cross-domain mapping. +5. **Context assembly**: For transcript work, explicitly structure output using EDCMBONE energy-dissonance mapping, F-metrics, failure-mode tags, and accessibility annotations. Preserve full original relations. +6. **Artifact production**: Write code/docs with msdmd blocks (if applicable) + dedicated "Usage Guidance" section or equivalent. Include examples that can be copy-pasted. +7. **GitHub hygiene**: Check drift, update indexes, propagate only after validation. Reference this skill in commit messages where relevant. +8. **Output packaging**: Structure responses with: + - Preserved structure / epistemic layers first. + - EDCMBONE-mapped analysis where transcripts are involved. + - Usage guidance and examples. + - `hmmm` boundaries clearly marked. + - Smallest next patch or action. + +## Anti-patterns + +- Beginning org work or instantiating an org agent without resolving skill-lib, governing repo instructions, and applicable contracts first. +- Asking the user to restate stable repository knowledge instead of resolving it from its authoritative source. +- Flattening, dropping variables, or losing topology/relations before acting or summarizing (directly conflicts with neurodivergence preservation). +- Starting a compute run when available resources have not been considered sufficiently to expect completion. +- Terminating a healthy compute run because of an arbitrary wall-clock limit that was not actually load-bearing to the claim, safety boundary, or external resource limit. +- Producing code, docs, or analysis without explicit usage guidance and examples. +- Assembling or analyzing EDCMBONE transcripts without applying the framework's energy circuit, F-loss, and failure-mode model. +- Performing GitHub or org maintenance without drift checks or index updates. +- Canonizing inferred patterns without source backing (pair with `canon` skill). +- Omitting `hmmm` when uncertainty or missing source exists. +- Treating repo-local copies as canonical source of truth. +- Using METAPAT to decorate a routine implementation decision. +- Making a conceptual architecture choice that crosses the METAPAT gate without consulting current METAPAT. +- Copying METAPAT doctrine into skill-lib and allowing the copy to become a competing authority. +- Treating a concrete host, client, provider login, or quota as standing organization authority without current deployment evidence. + +## Output Rubric (active whenever this skill is loaded) + +- Lead with preserved relational structure and epistemic status. +- Transcript tasks → EDCMBONE-structured output (energy maps, F1–F6 tags, accessibility notes, full topology). +- Code / docs → msdmd blocks where fitting + prominent, copy-pasteable "Usage Guidance" with examples and integration notes. +- GitHub / research → Drift status noted, index updates performed, relevant skills cross-referenced. +- If the METAPAT gate triggered, state what conceptual boundary required consultation and preserve any remaining `hmmm`. +- Always close with actionable next steps and any open `hmmm` items. + +hmmm +- Precise harness integration for automatically fetching current METAPAT after this gate triggers; the skill currently defines the decision rule and source-of-truth boundary, while the consuming agent uses its available GitHub/local-repo access. +- Whether the historical `meta` skill should remain as a compatibility router or be removed after all consumers propagate this gate. +- Whether a companion metadata-block skill (e.g. `# === TIW_WORKFLOW ===` or `# === INTERDEPENDENCY ===`) should be added for self-declaring modules inside The-Interdependency repos. +- Exact canonical reference for the full EDCMBONE transcript assembly protocol — should the detailed steps live in this skill or be expanded inside the edcmbone repo's own skill definitions? +- Actual VM state, current client/private-tunnel state, provider sessions, and quotas remain runtime evidence outside this skill. diff --git a/.agents/skills/thought-lens/SKILL.md b/.agents/skills/thought-lens/SKILL.md new file mode 100644 index 0000000..4da8acb --- /dev/null +++ b/.agents/skills/thought-lens/SKILL.md @@ -0,0 +1,439 @@ +--- +name: thought-lens +description: Translate raw, context-heavy, recursive, fragmentary, coined, or private-language thought into audience-legible language without changing the underlying claim. Load this when a user says people do not understand what they mean; asks to make a thought understandable to strangers, the public, a specific audience, or a platform; supplies dense notes rather than finished prose; needs jargon or coined terms introduced only after their ordinary-language meaning lands; or wants multiple audience/surface renderings from one thought. Do not load merely to polish finished prose or to simplify an already-stable canonical document; use ordinary editing for the former and plain-lens for the latter. +--- + +# thought-lens — translate thought without flattening it + +`thought-lens` sits between a person's internal context and another person's +available context. + +Its job is not to make the thinker sound simpler. Its job is to recover what is +actually being asserted, preserve that structure, and supply the minimum missing +context another person needs to recover the same claim. + +```text +raw thought -> recover structure -> freeze claim kernel -> render for audience + -> back-translate -> compare -> deliver or hmmm +``` + +Never simplify directly from raw thought. Recover the claim first. + +## When to load + +Load when the input is one or more of: + +- notes, fragments, shorthand, recursive sentences, partial equations, coined + terms, compressed references, or private vocabulary; +- understandable to a context-rich collaborator or model but not to a + context-light human reader; +- a thought that must become a conversation answer, public post, thread, + professional explanation, article paragraph, academic framing, or technical + note; +- a request to preserve the thought while reducing how much prerequisite + context the reader must already possess. + +A useful trigger is: **"I know what I mean, but other people do not have the +context."** + +Do not load for spelling, grammar, tone polishing, or a claim that is already +explicit. Do not use it for an established dense canon/spec/document; use +`plain-lens` downstream. Use `domain-claims` when a translated term is proposed +for canonical semantic authority, and `char-compress` when the target is agent +context size rather than human legibility. + +## Source of truth + +The supplied thought is the source of truth for intended meaning. Existing +canon, evidence, or repository sources constrain it when the thinker explicitly +refers to them, but the translator must not silently substitute a better-known +theory for the thought being translated. + +Treat the source as evidence of intended meaning, not automatically as truth +about the world. Preserve the difference among: + +```text +definition | observation | interpretation | hypothesis | causal claim +normative claim | metaphor | analogy | prediction | source-backed claim +``` + +Translation may change vocabulary. It may not silently change claim type, +certainty, polarity, quantifier, causal force, actor, scope, order, exception, +or status. + +## Inputs + +Minimum: + +```yaml +source: +``` + +Optional controls: + +```yaml +audience: stranger | peer | domain expert | named audience | hmmm +surface: conversation | x | thread | linkedin | article | formal | academic | technical | other +budget: 15_seconds | 1_minute | full | +goal: understand | respond | remember | inspect | act | ask_more +voice: preserve | neutral | +``` + +Defaults: + +- `audience`: context-light adult stranger; +- `surface`: compact general explanation; +- `budget`: first layer readable in roughly 15 seconds, with deeper layers + available beneath it; +- `voice`: preserve where it does not increase context debt. + +Do not require the user to pre-structure the thought. Recovering structure is +the work of this skill. + +## Workflow + +### 1. Recover the thought map + +Before writing audience prose, recover this structure internally: + +```yaml +thought_map: + subject: ... + core_claims: [...] + distinctions: [...] + definitions: [...] + relationships: [...] + causal_claims: [...] + evidence_or_examples: [...] + implications: [...] + qualifications: [...] + coined_terms: [...] + prerequisites: [...] + rhetorical_material: [...] + unresolved: [...] +``` + +Rules: + +1. Recover; do not improve. +2. Infer only what is necessary to connect explicit fragments. +3. Keep multiple plausible structures unresolved rather than choosing the + smoothest one. +4. Separate rhetoric, analogy, definition, mechanism, and evidence. +5. Record prerequisite concepts the source assumes the reader already knows. +6. Do not force a fragment into a complete theory. +7. Unknown becomes `hmmm`, not connective invention. + +The thought map is normally internal. Show it when requested, when material +ambiguity exists, or when fidelity cannot be established without exposing the +fork. + +### 2. Freeze the claim kernel + +Create the smallest structure every valid rendering must preserve: + +```yaml +kernel: + claim_type: ... + must_preserve: + - actor / object / relation / condition / consequence + - negation / quantifier / modal force / order / exception / scope + must_not_imply: + - claims not licensed by the source + strength: observed | possible | proposed | likely | asserted | defined | source-backed | hmmm + dependencies: + - prerequisite concept required for full precision + unresolved: + - ... +``` + +The kernel is a translation contract, not a summary. + +A shorter rendering is valid only when omitted material is not part of the +kernel or is explicitly deferred to a deeper layer. If deleting a detail changes +what could make the claim true or false, that detail belongs in the kernel. + +### 3. Calculate the context gap + +For every kernel element ask: + +```text +What must this audience already know to parse it? +What ordinary distinction can carry the same relation? +What example makes the relation visible without becoming fake evidence? +Which coined term is useful only after its ordinary referent is understood? +``` + +Classify prerequisite context as: + +- **required now** — omission causes first-pass misunderstanding; +- **defer safely** — needed for mechanism or precision, not first-pass meaning; +- **domain-only** — useful only to expert readers; +- **unresolved** — the mapping itself is uncertain; emit `hmmm`. + +The optimization target is **reader context required**, not intellectual content +removed. + +### 4. Render from the kernel + +Use this order unless the target surface requires otherwise: + +```text +ordinary distinction +-> concrete consequence/example when useful +-> coined or technical name only if it buys precision +-> mechanism / architecture +-> source or deeper treatment +``` + +A coined term normally enters as: + +```text +. I call this . +``` + +not: + +```text + is involving . +``` + +Surface adapters: + +- **Conversation** — answer the point first, then one layer of why. +- **X / short public post** — one usable distinction per post; no acronym as an + entry requirement. +- **Thread** — concrete hook -> distinction -> claim -> implication -> mechanism + or term -> deeper source -> `hmmm` if material. +- **LinkedIn / professional** — claim -> practical consequence -> mechanism or + example -> source; remove platform-performance filler. +- **Academic** — state claim type and scope first; distinguish proposal from + result; define terms before relying on them; identify evidence and unresolved + bridges. +- **Technical** — preserve exact operators, entities, interfaces, values, + dependencies, conditions, and status. Reduce context debt by adding + definitions, not by deleting structure. + +### 5. Build progressive disclosure + +Default human-facing shape: + +```text +UNDERSTAND THIS FIRST + + +SAY IT LIKE THIS + + +IF THEY ASK WHY + + +IF THEY WANT THE MODEL + + +HMMM + +``` + +Do not print empty sections. `SAY IT LIKE THIS` is the primary reusable output. + +Machine/UI equivalent: + +```yaml +translation: + understand_first: ... + say_it_like_this: ... + if_they_ask_why: ... | null + if_they_want_the_model: ... | null + terms_introduced: [...] + hmmm: [...] +``` + +### 6. Run the fidelity audit + +Compare **source -> kernel -> rendering**: + +```text +[ ] no kernel claim disappeared +[ ] no substantive new claim appeared +[ ] negation and polarity survived +[ ] quantifiers and scope survived +[ ] causal strength did not increase +[ ] certainty/status did not increase +[ ] actor and object did not swap +[ ] operative conditions, exceptions, and order survived +[ ] analogy is not presented as evidence +[ ] coined terms are defined before carrying argumentative weight +[ ] unresolved constraints remain visible as hmmm +``` + +If a rendering fails, regenerate from the kernel. Do not repair a bad +translation with persuasive filler. + +### 7. Back-translate for legibility + +Read the rendering as if the raw source were unavailable: + +```yaml +back_translation: + what_is_being_claimed: ... + claim_type: ... + strength: ... + required_context_still_missing: [...] +``` + +Compare it to the kernel. Pass when a context-naive reading recovers the kernel +without acquiring a material new claim. + +If a genuinely separate model or human is available, use it as the stronger +legibility check. If the same model performs the check, label it a **self-check** +and do not claim it proves human understanding. + +Readability scores are not substitutes for this test. Short words can still +carry the wrong idea. + +## Output contract + +For ordinary use, return the smallest useful rendering first. Do not force the +user to inspect the thought map or kernel unless they asked for them or an +ambiguity must be exposed. + +A good response allows the user to paste raw thought and immediately obtain text +they can use, while still retaining a path back to the full structure. + +Example invocation: + +```text +thought-lens this for a stranger, X, 15 seconds: + +``` + +or simply: + +```text +translate this so someone without my context can understand it: + +``` + +## Example + +Raw thought: + +```text +A network gets called decentralized because there are many nodes, but if every +node must ask the same service who is allowed to speak, the authority is still +centralized. topology isn't authority. +``` + +Kernel: + +```yaml +claim_type: distinction +must_preserve: + - many execution nodes do not by themselves imply distributed authority + - a shared permission authority can remain a central control point + - topology and authority are distinct properties +must_not_imply: + - the network has only one physical node + - all centralized permission systems are necessarily bad +strength: asserted +``` + +Context-light rendering: + +```text +A system can have thousands of independent machines and still have one +gatekeeper. Distribution of machines is not the same thing as distribution of +authority. +``` + +Technical layer: + +```text +Physical or computational topology and authorization topology are separate +properties. A network with many execution nodes remains authority-centralized +when a single service controls admission or permission. +``` + +Different vocabulary; same falsifiable distinction. + +## Anti-patterns + +Reject or repair: + +- **Jargon substitution** — replacing one private term with several unfamiliar + public terms. +- **Flattening** — deleting conditions, exceptions, uncertainty, or interacting + claims until only a slogan remains. +- **Persuasion drift** — changing `may` to `does`, `I suspect` to `is`, or a + proposal into fact because certainty reads more cleanly. +- **Mechanism invention** — supplying a missing causal bridge from general + knowledge without marking it as an addition. +- **Analogy capture** — using an analogy and then reasoning as though the target + literally has the analogy's properties. +- **Audience caricature** — treating a general reader as stupid. Remove missing + context, not intellectual content. +- **Voice erasure** — turning every rendering into generic institutional prose + when the source's cadence can survive without increasing context debt. +- **Acronym-first output** — requiring the reader to join private language + before the public idea exists for them. +- **False legibility claims** — declaring that humans understand because the + generating model understands its own output. +- **Over-recovery** — turning an already explicit sentence into an unnecessary + theory. Example: `The current implementation rejects unsigned requests.` + needs explanation only when explanation is requested. + +## Relation to neighboring skills + +```text +raw thought + | + v +thought-lens recover + freeze + translate + | + +--> public / conversational / academic / technical rendering + | + +--> stabilized document or canon candidate + | + v + plain-lens companion views of established dense source +``` + +`thought-lens` owns translation from pre-document cognition. `plain-lens` owns +companion views of stabilized dense source. `domain-claims` owns semantic +promotion. `char-compress` owns context-size compression. Do not collapse these +boundaries merely because all four transform language. + +## Validation + +Repository acceptance: + +```bash +python -m unittest discover -s tests +python tools/check_skill_lib_drift.py +python tools/check_skill_compliance.py +python tools/build_codex_plugin_skills.py --check +``` + +`thought-lens/fixtures.json` supplies review cases for uncertainty, operator +preservation, coined-term introduction, context-gap reduction, and refusing to +over-recover explicit claims. + +Repository checks prove registration, packaging, and procedural consistency. +They do not prove that a particular human audience understood a rendering. +Stronger field evidence requires an external reader, independent +back-translation, restatement, question, click/action, or another declared +observation. + +## hmmm + +- Human understanding cannot be guaranteed by model self-evaluation; the + back-translation self-check is a guardrail, not proof. +- Audience models are approximations. A named audience can still vary widely in + domain knowledge, literacy, language, culture, attention, and stakes. +- A future executable evaluator can compare claim kernels against independent + human/model back-translations, but no universal legibility metric is claimed + here. +- Sometimes the shortest path between two minds is a definition; sometimes it + is a story. A compiler that cannot tell the difference eventually buys a + trumpet. diff --git a/.agents/skills/thought-lens/fixtures.json b/.agents/skills/thought-lens/fixtures.json new file mode 100644 index 0000000..1d43d25 --- /dev/null +++ b/.agents/skills/thought-lens/fixtures.json @@ -0,0 +1,92 @@ +{ + "version": 1, + "description": "Human/model review fixtures for thought-lens. They test claim recovery, context-gap reduction, status preservation, and refusal to invent bridges. They are not proof that a human audience will understand a rendering.", + "fixtures": [ + { + "id": "topology-vs-authority", + "source": "A network gets called decentralized because there are many nodes, but if every node must ask the same service who is allowed to speak, the authority is still centralized. topology isn't authority.", + "audience": "context-light adult stranger", + "surface": "conversation", + "must_preserve": [ + "many nodes do not by themselves imply distributed authority", + "a shared permission service can remain a central authority", + "topology and authority are distinct properties" + ], + "must_not_imply": [ + "the system has only one physical node", + "all centralized permission systems are bad" + ], + "expected_claim_type": "distinction" + }, + { + "id": "uncertainty-preservation", + "source": "I keep seeing the failures happen after the handoff. I suspect the handoff is where context is being lost, but I don't know whether the cause is compression, permissions, or the receiving agent.", + "audience": "engineer", + "surface": "technical", + "must_preserve": [ + "failures are observed after handoff", + "handoff context loss is a suspicion rather than an established cause", + "three candidate causes remain unresolved" + ], + "must_not_imply": [ + "handoff context loss has been proven", + "compression is the cause" + ], + "expected_claim_type": "observation_plus_hypothesis" + }, + { + "id": "coined-term-after-meaning", + "source": "I call a durable unresolved constraint hmmm. It marks the place where delivered work ends without pretending the unfinished part vanished.", + "audience": "stranger", + "surface": "short_public_post", + "must_preserve": [ + "the unresolved constraint remains visible", + "the marker separates delivered work from unfinished continuation", + "hmmm is the coined label" + ], + "must_not_imply": [ + "hmmm means confusion in general", + "unfinished work is complete" + ], + "expected_claim_type": "definition" + }, + { + "id": "operator-preservation", + "source": "No model may publish the result as measured unless an external evaluator actually ran; a self-check may only be labeled a self-check.", + "audience": "developer", + "surface": "technical", + "must_preserve": [ + "external evaluation is required for the measured label", + "self-checks are allowed only under the self-check label" + ], + "must_not_imply": [ + "self-checks are forbidden", + "a model self-check qualifies as external measurement" + ], + "expected_claim_type": "normative_constraint" + }, + { + "id": "do-not-overrecover", + "source": "The current implementation rejects unsigned requests.", + "audience": "developer", + "surface": "technical", + "should_not_activate_deep_recovery": true, + "must_preserve": [ + "the current implementation rejects unsigned requests" + ], + "must_not_imply": [ + "a theory of identity", + "a general security guarantee", + "a reason why unsigned requests are rejected" + ], + "expected_claim_type": "implementation_observation" + } + ], + "review_questions": [ + "Can a reader state the kernel without seeing the source?", + "Did any modal, negation, quantifier, actor, condition, exception, causal strength, or claim status change?", + "Did the rendering add a substantive claim that the source did not license?", + "Did a coined term carry argumentative weight before its ordinary-language referent was available?", + "Is every unresolved bridge still visible as hmmm rather than silently completed?" + ] +} diff --git a/.agents/skills/typed-meta-frontend/SKILL.md b/.agents/skills/typed-meta-frontend/SKILL.md new file mode 100644 index 0000000..81c6bb6 --- /dev/null +++ b/.agents/skills/typed-meta-frontend/SKILL.md @@ -0,0 +1,167 @@ +--- +name: typed-meta-frontend +description: TypeScript frontend generation from backend-owned module metadata and living specs. Load this when building, reviewing, or refactoring a self-building UI that reads backend metadata, exposes every editable field per module, renders each module's living spec, generates TypeScript types/forms/routes from metadata, or keeps admin/editor frontends synchronized with msdmd-style source declarations. +--- + +# typed-meta-frontend — metadata-built TypeScript editors + +Use this skill to build a frontend that is not hand-authored field-by-field. The backend remains the source of truth; the TypeScript frontend discovers module metadata, renders each module's living spec, and exposes every declared editable field with safe edit/validate/save flows. + +## Core contract + +- Treat backend metadata as authoritative. Do not invent frontend-only fields unless the backend metadata declares them or marks them `hmmm`. +- Generate or derive TypeScript types from the metadata schema before building UI components. +- Display two surfaces for each module: + 1. **Living spec view** — human-readable module identity, purpose, boundaries, dependencies, tests, docs, risk notes, unresolved `hmmm`, and provenance. + 2. **Editable field view** — every backend-declared editable field, including nested fields, arrays, enums, validation rules, permissions, current value, dirty state, and save/error status. +- Preserve non-editable spec facts visibly. A user should be able to see why a field exists and why another field is read-only. +- Surface coverage gaps. A module missing metadata, missing editable declarations, or missing spec provenance must appear as a visible gap, not disappear from the UI. + +## Module-local block convention + +When a repo needs source-local declarations, use a `FRONTEND_META` msdmd block in the backend module that owns the data. Keep large schemas in backend code or generated JSON; the block should identify the module, living spec source, metadata endpoint, and editable field paths. + +```ts +// === FRONTEND_META === +// id: billing_policy_editor +// module_id: billing.policy +// living_spec: docs/billing-policy.md#living-spec +// metadata_endpoint: GET /api/meta/modules/billing.policy +// patch_endpoint: PATCH /api/meta/modules/billing.policy/fields +// editable_fields: settings.retryLimit, settings.gracePeriodDays, notices[].templateMarkdown +// readonly_fields: moduleId, audit.createdAt, audit.updatedBy +// permissions: billing.policy.edit +// hmmm: Whether gracePeriodDays should be tenant-scoped or org-scoped is unresolved. +// === END FRONTEND_META === +``` + +Required fields: `module_id`, `living_spec`, `metadata_endpoint`, `editable_fields`. Optional fields: `patch_endpoint`, `readonly_fields`, `permissions`, `hmmm`. Unknown fields are allowed only when preserved as visible metadata and marked `hmmm` if their meaning is unresolved. + +## Recommended backend metadata shape + +Use existing backend metadata if present. If defining a contract, keep it small and serializable: + +```ts +type ModuleMeta = { + moduleId: string; + title: string; + spec: { + summary: string; + livingSpecMarkdown?: string; + sourcePath?: string; + anchors?: string[]; + status: "declared" | "implemented" | "inferred" | "hmmm"; + hmmm?: string[]; + }; + editableFields: EditableField[]; +}; + +type EditableField = { + path: string; // e.g. "settings.retryLimit" or "owners[0].email" + label: string; + kind: "string" | "number" | "boolean" | "enum" | "markdown" | "json" | "date"; + required: boolean; + readOnly?: boolean; + help?: string; + value?: unknown; + defaultValue?: unknown; + enumOptions?: Array<{ value: string; label: string }>; + validation?: { + min?: number; + max?: number; + pattern?: string; + message?: string; + }; + permission?: string; + provenance?: string; + hmmm?: string; +}; +``` + +If the repo already uses msdmd, prefer adding a dedicated module-local metadata block that points to the backend schema or API rather than duplicating all data in the frontend. + +## Build workflow + +1. **Discover metadata source** + - Find the backend endpoint, generated JSON, OpenAPI route, GraphQL schema, or msdmd-derived collection that lists modules. + - Confirm it contains both living spec data and editable field declarations. + - If metadata is incomplete, preserve the gap as `hmmm` and build a visible incomplete state. + +2. **Generate TypeScript contracts** + - Derive `ModuleMeta`, `EditableField`, API request/response types, and discriminated unions for field `kind`. + - Keep generated files clearly marked if they are regenerated; keep hand-written adapters separate. + - Prefer runtime validation (`zod`, `valibot`, JSON Schema, or repo-standard equivalent) at the API boundary. + +3. **Create a metadata adapter layer** + - Normalize backend metadata into one frontend shape. + - Keep path traversal, defaulting, permission checks, validation message mapping, and save payload construction out of UI components. + - Record unresolved schema mismatches as `hmmm` instead of silently coercing them. + +4. **Render the module index** + - List every module returned by metadata. + - Include metadata health: complete, partial, missing editable fields, missing living spec, save disabled, or `hmmm`. + - Provide search/filter by module id, title, status, owner, capability, risk boundary, and unresolved fields when available. + +5. **Render the living spec** + - Show the backend-provided markdown/spec facts with source path and status. + - Keep generated summaries subordinate to the source spec. + - Highlight `hmmm` as an honest continuation boundary, not as an error to hide. + +6. **Render editable fields** + - Select controls by `EditableField.kind`. + - Show required state, validation, help text, provenance, permission/read-only reason, dirty state, and server errors for every field. + - Support nested paths and arrays without dropping fields. + - Disable save when permission or validation forbids it, but still show the field and reason. + +7. **Save by metadata path** + - Send minimal patches keyed by declared field paths unless the backend requires full objects. + - Re-fetch metadata after save so the living spec and editable values remain backend-synchronized. + - Display optimistic updates only if rollback and server reconciliation are implemented. + +8. **Verify coverage** + - Add tests that fail when a module with editable metadata does not render all editable paths. + - Add tests for living spec presence, read-only reasons, validation errors, nested field paths, array fields, and `hmmm` display. + - Add a drift check when generated TypeScript contracts are committed. + +## Runner / generator contract + +A compliant generator or frontend build step reads backend-owned metadata, +derives TypeScript contracts before UI rendering, keeps generated files marked +as generated, and fails visibly when a declared module, living spec, editable +field, permission, or validation rule cannot be represented. If a consuming repo +does not ship a generator, the same contract applies to its adapter layer and +tests. + +## UI implementation guidance + +- Use schema-driven components: `ModuleList`, `ModuleSpecPanel`, `EditableFieldRenderer`, `FieldControl`, `MetadataHealthBadge`, and `SaveBar`. +- Keep field renderers exhaustive. A new backend `kind` should cause a TypeScript compile error or visible unsupported-field state. +- Use stable field keys from `moduleId + path`; do not key editable fields by label. +- Treat markdown specs as untrusted input unless the backend guarantees sanitization. +- Keep accessibility first: label every control, associate errors with controls, preserve keyboard navigation, and avoid color-only metadata health states. + +## Acceptance checklist + +- Every backend-listed module appears in the frontend. +- Every module displays its living spec or a visible `hmmm` for missing spec. +- Every declared editable field appears exactly once, including nested and array fields. +- Read-only fields still appear with a reason. +- Unknown field kinds produce visible unsupported-field UI and `hmmm`, not blank space. +- Save payloads use backend-declared paths and permissions. +- Tests cover full metadata-to-UI field exposure. +- Usage guidance documents where metadata comes from, how to regenerate types, how to run the frontend, and how to add a new editable field. + +## Anti-patterns + +- Hand-authoring field forms that silently diverge from backend metadata. +- Dropping read-only fields, permission-denied fields, unknown field kinds, or + missing specs from the UI. +- Trusting backend-provided markdown without a sanitization boundary. +- Saving optimistic edits without rollback and server reconciliation. +- Treating frontend convenience fields as canonical when the backend has not + declared them. + +hmmm +- Preferred concrete backend metadata transport is repo-specific: REST, GraphQL, generated JSON, OpenAPI, or msdmd collection can all satisfy the contract. +- The exact persistence strategy for field patches depends on backend authorization and audit requirements. +- A frontend that builds itself from metadata is a mirror with a wrench taped to it; useful, but only if the mirror admits where the wrench cannot reach. diff --git a/.agents/skills/ucns-option-selection/SKILL.md b/.agents/skills/ucns-option-selection/SKILL.md new file mode 100644 index 0000000..9060ad5 --- /dev/null +++ b/.agents/skills/ucns-option-selection/SKILL.md @@ -0,0 +1,253 @@ +--- +name: ucns-option-selection +description: Fail-closed rubric for comparing, retaining, rejecting, deprecating, and selecting UCNS options within an explicit scope. Load this when an agent asks which UCNS candidate should win, whether evidence authorizes selection, how an option moves from registered or implemented to selected, how to compare competing gonol constructors, carriers, geometries, policies, projections, or measurement candidates, or how to issue a scoped UCNS decision receipt. Do not load merely to register options, execute one already-selected option, or choose ordinary UI preferences. Never select universal UCNS canon by score, familiarity, implementation order, or EDCM-local evidence. +--- + +# ucns-option-selection — selection must earn its scope + +Use this procedural rubric for the transition from preserved alternatives to +an explicit, evidence-bearing, scoped decision. Current UCNS source, registries, +manifests, protocols, receipts, and canon remain authoritative. This skill does +not appoint a winner or freeze a current option inventory. + +## Selection principle + +Selection is a gated decision, not an additive score. + +A candidate cannot compensate for a violated invariant, incomplete evidence, +failed replay, hidden information loss, or absent authority by being faster, +simpler, popular, familiar, or already implemented. + +```text +scope and authority + -> eligibility + -> evidence completeness + -> falsification and replay + -> purpose-relative comparison + -> non-transfer and rollback + -> explicit ratification + -> scoped decision receipt +``` + +Failure of a hard gate stops selection while preserving the option and its +evidence under the appropriate standing. + +## Workflow + +### 1. Fix the decision boundary + +Record before comparing outcomes: + +```text +decision_id: +selection_scope: +purpose: +authority: +consumer: +candidate_set: +required_constraints: +comparison_policy: +evidence_boundary: +non_transfer_boundaries: +ratification_rule: +``` + +Examples of valid scope include one EDCM profile, one declared gonol +construction layer, one rendering surface, or one experiment. “UCNS generally” +is not a valid scope without separately ratified universal authority. + +Freeze the candidate set or state the admission rule before inspecting results. +Newly discovered candidates may enter only through a recorded protocol event; +do not silently add or remove competitors after seeing an outcome. + +### 2. Resolve exact candidate identity + +Each candidate must bind: + +- name, version, evaluator kind, and code reference; +- source, corpus, adapter, configuration, and producer identities; +- option values and applicable policies; +- construction and evidence receipt digests; +- declared scope, purpose, and known information loss; +- authorship of candidate, evidence, comparison, and decision. + +Identity mismatch fails closed. Similar names or byte-different receipts are +not interchangeable evidence. + +### 3. Apply hard eligibility gates + +A candidate is eligible only if it: + +- satisfies every decided constraint applicable to the selection scope; +- preserves required distinctions, ordering, multiplicity, provenance, and + typed absence; +- respects construction boundaries, including gonol closure and atomic + promotion when applicable; +- uses declared comparison and structure policies without hidden defaults; +- retains exact evidence beneath every lossy projection; +- violates no active rejection, deprecation, security, consent, custody, or + source-license boundary. + +An ineligible candidate is `REJECTED` for this decision boundary. Rejecting it +here does not erase historical evidence or prove universal invalidity. + +### 4. Require complete evidence + +Selection requires the complete evidence declared by the protocol: + +- full admitted corpus or complete declared mathematical/domain boundary; +- natural terminal execution or a genuine preregistered stop condition; +- exact counts, identities, digests, custody, and failure propagation; +- no sampled prefix represented as completion; +- no fixture success represented as generality; +- no outcome-dependent change to target, metric, control, or criterion. + +Missing required evidence yields `BLOCKED` when a named prerequisite is absent, +or `UNRESOLVED` when the decision boundary itself remains incomplete. + +### 5. Require falsification and independent replay + +The candidate must face its frozen falsifiers and matched-information controls. +Where the protocol requires deterministic identity, independently reconstruct +or replay the full declared scope and compare exact receipts byte-for-byte. + +Classify results without promotion: + +- `SURVIVED` means the candidate survived the declared test; +- `FALSIFIED` means it failed the declared falsifier; +- neither word means selected, canonical, proved, useful outside scope, or + measurement-valid. + +### 6. Compare for the declared purpose + +Compare only after eligibility and evidence gates close. Use the frozen named +policy and report the complete vector rather than collapsing it prematurely: + +```text +constraint fidelity: +purpose effectiveness: +worst-case behavior: +retained distinctions: +declared information loss: +reconstruction/replay: +failure transparency: +resource observations: +integration cost: +rollback and migration: +unresolved dependencies: +``` + +Purpose effectiveness must distinguish “works” from “advantage over a +matched-information alternative.” Resource use may break a tie only when the +decision record explicitly makes it relevant; it cannot rescue semantic or +structural failure. + +Do not use one scalar rank unless the scalar and aggregation rule were frozen, +all hard gates remain independently visible, and the scalar cannot conceal a +disqualifying failure. + +### 7. Apply the selection rule + +A candidate may become `SELECTED_FOR_SCOPE` only when: + +1. its exact identity is closed; +2. every hard eligibility gate passes; +3. required evidence is complete; +4. declared falsifiers and replay requirements are satisfied; +5. it meets the frozen purpose-relative selection criterion; +6. alternatives and negative evidence remain recoverable; +7. non-transfer, rollback, and migration boundaries are explicit; and +8. the named authority performs the required ratification event. + +Repeated use, registration order, implementation completeness, CI success, +agent preference, or an absent objection cannot substitute for ratification. + +### 8. Emit one terminal standing + +- `SELECTED_FOR_SCOPE` — explicitly ratified winner for the declared scope. +- `RETAINED_CANDIDATE` — eligible evidence-bearing alternative not selected. +- `REJECTED` — failed an applicable hard gate or frozen criterion. +- `BLOCKED` — a named prerequisite prevents authorized evaluation or decision. +- `UNRESOLVED` — admissible constructions, interpretation, or decision rule is + not complete enough to close. +- `DEPRECATED` — removed from active forward use by an explicit replacement or + failure-propagation decision; historical evidence remains. + +Do not translate these standings into statuses owned by another scope. + +### 9. Seal the decision receipt + +```text +decision_id: +scope: +purpose: +authority and ratification event: +candidate identities: +eligibility results: +evidence receipts: +falsifier and replay results: +comparison policy and complete vector: +selected candidate or none: +terminal standing of every candidate: +claims authorized: +claims not authorized: +non-transfer boundaries: +rollback trigger and procedure: +migration effect: +remaining hmmm: +``` + +The receipt must identify every candidate considered and preserve negative +results. A selection receipt never rewrites its preregistration or source +evidence. + +## Gonol-constructor application + +Load `gonol-build` with this skill when comparing gonol constructors. + +An unresolved recursive-gonol constructor does not block candidate +construction. Build and label explicit candidates, preserve closed lower-scale +gonols as atomic participants, bind intrinsic relations and option choices, +preregister their falsifiers, and run them to completion. `hmmm` prevents a +candidate from silently becoming the constructor; it does not prevent the +candidate from being built or tested. + +## Anti-patterns + +- Selecting the first implementation because it exists. +- Using a weighted score to cancel a hard-gate failure. +- Treating elegance, speed, compression, or familiarity as semantic evidence. +- Choosing after inspecting hidden outcome labels or changing the candidate set + after results appear. +- Comparing candidates that received different information without declaring + and controlling the difference. +- Treating CI, fixtures, or deterministic replay as purpose effectiveness. +- Promoting EDCM-local survival into universal UCNS, EDCM measurement, + METAPAT, cognition, theorem, or PTCNA authority. +- Treating `UNRESOLVED` as “do no work” instead of authorization to construct + explicit candidates under preserved uncertainty. +- Deleting losing options or their evidence after selection. + +## Validation + +A valid use demonstrates: + +- scope, purpose, authority, candidates, gates, and policies were frozen before + outcome comparison; +- hard gates remained noncompensable; +- complete evidence and independent replay requirements were enforced; +- every candidate received a terminal standing; +- selection, canon, proof, usefulness, and measurement validity remained + distinct; +- ratification was explicit; +- rollback, migration, non-transfer, negative evidence, and `hmmm` survived. + +## hmmm + +- the first UCNS domain to ratify and exercise this complete rubric; +- whether decision receipts should gain a machine-readable schema and runner; +- which authorities may ratify scopes delegated by Erin Spencer; +- the measurement-validity criterion required before any measurement option + can be selected as valid rather than merely structurally preferred; +- when multiple non-dominated candidates should remain a selected set rather + than forcing one winner. diff --git a/.agents/skills/validate-data/SKILL.md b/.agents/skills/validate-data/SKILL.md new file mode 100644 index 0000000..6eaab0a --- /dev/null +++ b/.agents/skills/validate-data/SKILL.md @@ -0,0 +1,409 @@ +--- +name: validate-data +description: QA an analysis before sharing -- methodology, accuracy, and bias checks. Use this when reviewing an analysis before a stakeholder presentation, spot-checking calculations and aggregation logic, verifying a SQL query's results look right, or assessing whether conclusions are actually supported by the data. +argument-hint: "" +--- + +# /validate-data - Validate Analysis Before Sharing + +> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md). + +Review an analysis for accuracy, methodology, and potential biases before sharing with stakeholders. Generates a confidence assessment and improvement suggestions. + +## Usage + +``` +/validate-data +``` + +The analysis can be: +- A document or report in the conversation +- A file (markdown, notebook, spreadsheet) +- SQL queries and their results +- Charts and their underlying data +- A description of methodology and findings + +## Workflow + +### 1. Review Methodology and Assumptions + +Examine: + +- **Question framing**: Is the analysis answering the right question? Could the question be interpreted differently? +- **Data selection**: Are the right tables/datasets being used? Is the time range appropriate? +- **Population definition**: Is the analysis population correctly defined? Are there unintended exclusions? +- **Metric definitions**: Are metrics defined clearly and consistently? Do they match how stakeholders understand them? +- **Baseline and comparison**: Is the comparison fair? Are time periods, cohort sizes, and contexts comparable? + +### 2. Run the Pre-Delivery QA Checklist + +Work through the checklist below — data quality, calculation, reasonableness, and presentation checks. + +### 3. Check for Common Analytical Pitfalls + +Systematically review against the detailed pitfall catalog below (join explosion, survivorship bias, incomplete period comparison, denominator shifting, average of averages, timezone mismatches, selection bias). + +### 4. Verify Calculations and Aggregations + +Where possible, spot-check: + +- Recalculate a few key numbers independently +- Verify that subtotals sum to totals +- Check that percentages sum to 100% (or close to it) where expected +- Confirm that YoY/MoM comparisons use the correct base periods +- Validate that filters are applied consistently across all metrics + +Apply the result sanity-checking techniques below (magnitude checks, cross-validation, red-flag detection). + +### 5. Assess Visualizations + +If the analysis includes charts: + +- Do axes start at appropriate values (zero for bar charts)? +- Are scales consistent across comparison charts? +- Do chart titles accurately describe what's shown? +- Could the visualization mislead a quick reader? +- Are there truncated axes, inconsistent intervals, or 3D effects that distort perception? + +### 6. Evaluate Narrative and Conclusions + +Review whether: + +- Conclusions are supported by the data shown +- Alternative explanations are acknowledged +- Uncertainty is communicated appropriately +- Recommendations follow logically from findings +- The level of confidence matches the strength of evidence + +### 7. Suggest Improvements + +Provide specific, actionable suggestions: + +- Additional analyses that would strengthen the conclusions +- Caveats or limitations that should be noted +- Better visualizations or framings for key points +- Missing context that stakeholders would want + +### 8. Generate Confidence Assessment + +Rate the analysis on a 3-level scale: + +**Ready to share** -- Analysis is methodologically sound, calculations verified, caveats noted. Minor suggestions for improvement but nothing blocking. + +**Share with noted caveats** -- Analysis is largely correct but has specific limitations or assumptions that must be communicated to stakeholders. List the required caveats. + +**Needs revision** -- Found specific errors, methodological issues, or missing analyses that should be addressed before sharing. List the required changes with priority order. + +## Output Format + +``` +## Validation Report + +### Overall Assessment: [Ready to share | Share with caveats | Needs revision] + +### Methodology Review +[Findings about approach, data selection, definitions] + +### Issues Found +1. [Severity: High/Medium/Low] [Issue description and impact] +2. ... + +### Calculation Spot-Checks +- [Metric]: [Verified / Discrepancy found] +- ... + +### Visualization Review +[Any issues with charts or visual presentation] + +### Suggested Improvements +1. [Improvement and why it matters] +2. ... + +### Required Caveats for Stakeholders +- [Caveat that must be communicated] +- ... +``` + +--- + +## Pre-Delivery QA Checklist + +Run through this checklist before sharing any analysis with stakeholders. + +### Data Quality Checks + +- [ ] **Source verification**: Confirmed which tables/data sources were used. Are they the right ones for this question? +- [ ] **Freshness**: Data is current enough for the analysis. Noted the "as of" date. +- [ ] **Completeness**: No unexpected gaps in time series or missing segments. +- [ ] **Null handling**: Checked null rates in key columns. Nulls are handled appropriately (excluded, imputed, or flagged). +- [ ] **Deduplication**: Confirmed no double-counting from bad joins or duplicate source records. +- [ ] **Filter verification**: All WHERE clauses and filters are correct. No unintended exclusions. + +### Calculation Checks + +- [ ] **Aggregation logic**: GROUP BY includes all non-aggregated columns. Aggregation level matches the analysis grain. +- [ ] **Denominator correctness**: Rate and percentage calculations use the right denominator. Denominators are non-zero. +- [ ] **Date alignment**: Comparisons use the same time period length. Partial periods are excluded or noted. +- [ ] **Join correctness**: JOIN types are appropriate (INNER vs LEFT). Many-to-many joins haven't inflated counts. +- [ ] **Metric definitions**: Metrics match how stakeholders define them. Any deviations are noted. +- [ ] **Subtotals sum**: Parts add up to the whole where expected. If they don't, explain why (e.g., overlap). + +### Reasonableness Checks + +- [ ] **Magnitude**: Numbers are in a plausible range. Revenue isn't negative. Percentages are between 0-100%. +- [ ] **Trend continuity**: No unexplained jumps or drops in time series. +- [ ] **Cross-reference**: Key numbers match other known sources (dashboards, previous reports, finance data). +- [ ] **Order of magnitude**: Total revenue is in the right ballpark. User counts match known figures. +- [ ] **Edge cases**: What happens at the boundaries? Empty segments, zero-activity periods, new entities. + +### Presentation Checks + +- [ ] **Chart accuracy**: Bar charts start at zero. Axes are labeled. Scales are consistent across panels. +- [ ] **Number formatting**: Appropriate precision. Consistent currency/percentage formatting. Thousands separators where needed. +- [ ] **Title clarity**: Titles state the insight, not just the metric. Date ranges are specified. +- [ ] **Caveat transparency**: Known limitations and assumptions are stated explicitly. +- [ ] **Reproducibility**: Someone else could recreate this analysis from the documentation provided. + +## Common Data Analysis Pitfalls + +### Join Explosion + +**The problem**: A many-to-many join silently multiplies rows, inflating counts and sums. + +**How to detect**: +```sql +-- Check row count before and after join +SELECT COUNT(*) FROM table_a; -- 1,000 +SELECT COUNT(*) FROM table_a a JOIN table_b b ON a.id = b.a_id; -- 3,500 (uh oh) +``` + +**How to prevent**: +- Always check row counts after joins +- If counts increase, investigate the join relationship (is it really 1:1 or 1:many?) +- Use `COUNT(DISTINCT a.id)` instead of `COUNT(*)` when counting entities through joins + +### Survivorship Bias + +**The problem**: Analyzing only entities that exist today, ignoring those that were deleted, churned, or failed. + +**Examples**: +- Analyzing user behavior of "current users" misses churned users +- Looking at "companies using our product" ignores those who evaluated and left +- Studying properties of "successful" outcomes without "unsuccessful" ones + +**How to prevent**: Ask "who is NOT in this dataset?" before drawing conclusions. + +### Incomplete Period Comparison + +**The problem**: Comparing a partial period to a full period. + +**Examples**: +- "January revenue is $500K vs. December's $800K" -- but January isn't over yet +- "This week's signups are down" -- checked on Wednesday, comparing to a full prior week + +**How to prevent**: Always filter to complete periods, or compare same-day-of-month / same-number-of-days. + +### Denominator Shifting + +**The problem**: The denominator changes between periods, making rates incomparable. + +**Examples**: +- Conversion rate improves because you changed how you count "eligible" users +- Churn rate changes because the definition of "active" was updated + +**How to prevent**: Use consistent definitions across all compared periods. Note any definition changes. + +### Average of Averages + +**The problem**: Averaging pre-computed averages gives wrong results when group sizes differ. + +**Example**: +- Group A: 100 users, average revenue $50 +- Group B: 10 users, average revenue $200 +- Wrong: Average of averages = ($50 + $200) / 2 = $125 +- Right: Weighted average = (100*$50 + 10*$200) / 110 = $63.64 + +**How to prevent**: Always aggregate from raw data. Never average pre-aggregated averages. + +### Timezone Mismatches + +**The problem**: Different data sources use different timezones, causing misalignment. + +**Examples**: +- Event timestamps in UTC vs. user-facing dates in local time +- Daily rollups that use different cutoff times + +**How to prevent**: Standardize all timestamps to a single timezone (UTC recommended) before analysis. Document the timezone used. + +### Selection Bias in Segmentation + +**The problem**: Segments are defined by the outcome you're measuring, creating circular logic. + +**Examples**: +- "Users who completed onboarding have higher retention" -- obviously, they self-selected +- "Power users generate more revenue" -- they became power users BY generating revenue + +**How to prevent**: Define segments based on pre-treatment characteristics, not outcomes. + +### Other Statistical Traps + +- **Simpson's paradox**: Trend reverses when data is aggregated vs. segmented +- **Correlation presented as causation** without supporting evidence +- **Small sample sizes** leading to unreliable conclusions +- **Outliers disproportionately affecting averages** (should medians be used instead?) +- **Multiple testing / cherry-picking** significant results +- **Look-ahead bias**: Using future information to explain past events +- **Cherry-picked time ranges** that favor a particular narrative + +## Result Sanity Checking + +### Magnitude Checks + +For any key number in your analysis, verify it passes the "smell test": + +| Metric Type | Sanity Check | +|---|---| +| User counts | Does this match known MAU/DAU figures? | +| Revenue | Is this in the right order of magnitude vs. known ARR? | +| Conversion rates | Is this between 0% and 100%? Does it match dashboard figures? | +| Growth rates | Is 50%+ MoM growth realistic, or is there a data issue? | +| Averages | Is the average reasonable given what you know about the distribution? | +| Percentages | Do segment percentages sum to ~100%? | + +### Cross-Validation Techniques + +1. **Calculate the same metric two different ways** and verify they match +2. **Spot-check individual records** -- pick a few specific entities and trace their data manually +3. **Compare to known benchmarks** -- match against published dashboards, finance reports, or prior analyses +4. **Reverse engineer** -- if total revenue is X, does per-user revenue times user count approximately equal X? +5. **Boundary checks** -- what happens when you filter to a single day, a single user, or a single category? Are those micro-results sensible? + +### Red Flags That Warrant Investigation + +- Any metric that changed by more than 50% period-over-period without an obvious cause +- Counts or sums that are exact round numbers (suggests a filter or default value issue) +- Rates exactly at 0% or 100% (may indicate incomplete data) +- Results that perfectly confirm the hypothesis (reality is usually messier) +- Identical values across time periods or segments (suggests the query is ignoring a dimension) + +## Documentation Standards for Reproducibility + +### Analysis Documentation Template + +Every non-trivial analysis should include: + +```markdown +## Analysis: [Title] + +### Question +[The specific question being answered] + +### Data Sources +- Table: [schema.table_name] (as of [date]) +- Table: [schema.other_table] (as of [date]) +- File: [filename] (source: [where it came from]) + +### Definitions +- [Metric A]: [Exactly how it's calculated] +- [Segment X]: [Exactly how membership is determined] +- [Time period]: [Start date] to [end date], [timezone] + +### Methodology +1. [Step 1 of the analysis approach] +2. [Step 2] +3. [Step 3] + +### Assumptions and Limitations +- [Assumption 1 and why it's reasonable] +- [Limitation 1 and its potential impact on conclusions] + +### Key Findings +1. [Finding 1 with supporting evidence] +2. [Finding 2 with supporting evidence] + +### SQL Queries +[All queries used, with comments] + +### Caveats +- [Things the reader should know before acting on this] +``` + +### Code Documentation + +For any code (SQL, Python) that may be reused: + +```python +""" +Analysis: Monthly Cohort Retention +Author: [Name] +Date: [Date] +Data Source: events table, users table +Last Validated: [Date] -- results matched dashboard within 2% + +Purpose: + Calculate monthly user retention cohorts based on first activity date. + +Assumptions: + - "Active" means at least one event in the month + - Excludes test/internal accounts (user_type != 'internal') + - Uses UTC dates throughout + +Output: + Cohort retention matrix with cohort_month rows and months_since_signup columns. + Values are retention rates (0-100%). +""" +``` + +### Version Control for Analyses + +- Save queries and code in version control (git) or a shared docs system +- Note the date of the data snapshot used +- If an analysis is re-run with updated data, document what changed and why +- Link to prior versions of recurring analyses for trend comparison + +## Examples + +``` +/validate-data Review this quarterly revenue analysis before I send it to the exec team: [analysis] +``` + +``` +/validate-data Check my churn analysis -- I'm comparing Q4 churn rates to Q3 but Q4 has a shorter measurement window +``` + +``` +/validate-data Here's a SQL query and its results for our conversion funnel. Does the logic look right? [query + results] +``` + +## Tips + +- Run /validate-data before any high-stakes presentation or decision +- Even quick analyses benefit from a sanity check -- it takes a minute and can save your credibility +- If the validation finds issues, fix them and re-validate +- Share the validation output alongside your analysis to build stakeholder confidence + +## Workflow + +1. Restate what the analysis claims and what decision rides on it. +2. Check methodology: aggregation logic, filters, joins, time windows, denominators. +3. Reproduce at least one key number independently before trusting any of them. +4. Attack the conclusion: seek the disconfirming cut, segment, or period. +5. Deliver a verdict distinguishing verified, plausible, and unsupported claims. + +## Anti-patterns + +- Validating only the happy path while the stakeholder decision hinges on edge segments. +- Confirmation-shaped review — checking that the analysis is right instead of trying to break it. +- Signing off without independently reproducing a single headline number. + +## Provenance + +Imported from `anthropics/knowledge-work-plugins` @ `94e1a08` (`data/skills/validate-data/`), Apache-2.0. +Local modifications: trigger phrasing normalized to skill-lib convention; this +Workflow/Anti-patterns/Provenance/hmmm bookend appended. Upstream body above is +otherwise unmodified. See `ATTRIBUTION.md` at repo root. + +hmmm +- Severity taxonomy for findings (blocker vs caveat) is not yet standardized. +- Relation to test-build CONTRACTS blocks for recurring analyses is unexplored. +- Upstream re-sync cadence with `anthropics/knowledge-work-plugins` is undecided; drift against upstream is currently invisible. diff --git a/.agents/skills/visitor-intro/SKILL.md b/.agents/skills/visitor-intro/SKILL.md new file mode 100644 index 0000000..0494dad --- /dev/null +++ b/.agents/skills/visitor-intro/SKILL.md @@ -0,0 +1,155 @@ +--- +name: visitor-intro +description: Onboarding tour for visitors arriving at any The-Interdependency repo. Load this when an unfamiliar user asks "what is this?", "what is The Interdependency?", "how do these repos fit together?", "where do I start?", or otherwise signals they are new to the org. Gives the agent a consistent, repo-aware way to orient a newcomer without inventing facts. +--- + +# visitor-intro — Orienting newcomers to The Interdependency + +This skill exists because visitors land at one repo at a time. A +contributor who clones `a0` sees an agent platform; one who clones +`ucns` sees number theory; one who clones `PCEA` sees an encryption +algorithm. Without a shared frame, each landing looks like a separate +project. It isn't — they're parts of one organization-level system. + +The goal of this skill is to let any agent give a coherent, brief, +honest tour from wherever the visitor happens to be, then point them at +the canonical entry points so they can read the org's own words rather +than the agent's paraphrase. + +## When to load + +Load this skill when any of the following are true: + +- The visitor explicitly asks "what is this", "what is The + Interdependency", "how do these repos relate", "where do I start". +- The visitor has cloned or opened a repo and asks for an overview + before doing any task. +- The visitor mentions they are new, evaluating, or auditing the org. +- A task touches more than one repo and the visitor seems unaware that + the other repo exists or what it does. + +Do not load this skill for in-task work by an existing contributor. +This is an onboarding skill, not a doctrine reference. + +## Doctrine + +Three rules govern any tour given under this skill: + +1. **Repo-aware.** Start from the repo the visitor is in. Name it + explicitly, say what it does, then widen the frame to the org. +2. **Map, don't recite.** Give the visitor a labelled map and a small + number of entry points. Do not paraphrase the founding documents at + length — they exist and can be read directly. +3. **Mark unknowns.** If the visitor asks about a part of the org you + cannot describe from this skill or the repo's own files, say so. + Write `hmmm` rather than inventing alignment. + +## Org thesis (one paragraph) + +The Interdependency is a research organization building an integrated +agent platform whose components are released as independent repos. +`a0` is the user-facing agent platform; `pcna` is the inference engine +it embeds; `ucns`, `PCEA`, and `ZFAE` are mathematical substrates the +engine and platform rest on; `edcmbone` is a measurement layer for +agent behaviour; `interdependent-lib` and `skill-lib` are shared +libraries; the rest are extensions, archives, or variants. The +founding document is `a0/interdependent_way.md`. + +## Repo map + +Give the visitor this map. Use the one-liner for the repo they are in +plus a short ring of related repos; do not dump the whole table unless +they ask. + +| Repo | One-liner | +|---|---| +| `a0` | The agent platform (`a0p`). Three-process app (Express + Vite + Python/FastAPI) with a metadata-driven console. This is what most visitors should run first. | +| `pcna` | Prime Circular Neural Architecture — the six-ring inference engine (Phi / Psi / Omega / Guardian / Memory-L / Memory-S) embedded in `a0`. | +| `PTCA` | Prime Tensor Circular Architecture — the ring-tensor substrate that `pcna` builds on. | +| `PCEA` | Prime Circular Encryption Algorithm — companion encryption layer for ring states. | +| `ucns` | Unit Circle Number System — the recursive factorization theory underlying the ring math. | +| `a0ucns` | `a0` packaged with `ucns` for combined research deployment. | +| `eml_ucns` | EML-flavoured `ucns` variant. | +| `edcmbone` | Structural fidelity measurement for AI interactions. Backbone of the EDCM behavioural directive layer. | +| `aimmh` | Emergent Multi-Model AI Hub — multi-provider routing surface. | +| `interdependent-lib` | Shared cross-repo library code. | +| `skill-lib` | Canonical org-wide agent skill library (this repo). | +| `ai-tiw` | Archive / content repo (model-response artefacts; predates current doctrine). | +| `ZFAE` | Zero-Field Algebraic Encoding — substrate work. | + +If a visitor is in a repo not in this table, say so honestly and point +them at the repo's own `README.md`. + +## Core concepts visitors will encounter + +Name these only as needed. Do not lecture. + +- **PCNA / the six rings** — Phi, Psi, Omega, Guardian, Memory-Long, + Memory-Short. Inference is a six-step pipeline over these rings. + Defined in `a0`'s `python/engine/pcna.py` and documented in `a0/spec.md`. +- **EDCM** — Behavioural directive scoring (CM, DA, DRIFT, DVG, INT, TBF) + that guides LLM selection and fires corrective actions. Lives in + `a0/python/services/edcm.py`; doctrine in `edcmbone`. +- **SigmaCore** — Encodes the workspace filesystem as a prime-ring + tensor; the Psi ring's filesystem companion. +- **msdmd** — Module Self-Declared Metadata Markdown. The org's + convention for keeping module contracts in the same file as the code. + Defined in `../msdmd/SKILL.md`. +- **The Interdependent Way** — The founding sociopolitical document. + `a0/interdependent_way.md`. Long, poetic, and load-bearing for the + org's framing. Quote sparingly; link instead. + +## Recommended reading order + +Offer this as a numbered list when the visitor asks "where do I +start". Pick the first item to match the repo they are in. + +1. The repo's own `README.md` (always). +2. `a0/replit.md` — platform overview and current state. +3. `a0/CLAUDE.md` — concrete architecture: process topology, + frontend metadata-driven console, route registration, key services. +4. `a0/spec.md` — full agent platform spec (PCNA, EDCM, sentinel + channels). Long; skim. +5. `a0/interdependent_way.md` — founding philosophy. Optional but + recommended before any contribution. +6. `../README.md` — the doctrinal toolkit agents use here. + +## Workflow: tailoring by landing repo + +Use the table below to pick the opening sentence. Then offer the +reading list above, reordered so the visitor's current repo's +documents appear first. + +| Visitor is in | Opening frame | +|---|---| +| `a0` | "You're in the agent platform. Most of the org's runtime lives here; the other repos are substrates and extensions." | +| `pcna` / `PTCA` / `PCEA` / `ucns` / `ZFAE` | "You're in a mathematical substrate of the platform. The user-facing app that consumes this is `a0`." | +| `edcmbone` | "You're in the behavioural-measurement layer. The runtime that fires on these measurements is `a0/python/services/edcm.py`." | +| `skill-lib` | "You're in the canonical org-wide agent skill library. Every other repo carries a `.agents/skills/` copy of these skills." | +| `interdependent-lib` | "You're in a shared cross-repo library. It is consumed by the runtime repos rather than run on its own." | +| `aimmh` | "You're in the multi-model hub used by the platform for provider routing." | +| `ai-tiw` | "You're in an archive / content repo. Historical artefacts; predates current module doctrine." | +| anything else | "I don't have a one-line frame for this repo committed to memory. Let me read its `README.md` and tell you what I see." | + +## Anti-patterns / things to refuse + +- Do not invent relationships between repos that are not in this skill + or in the repo's own files. +- Do not claim a repo is "deprecated", "the main one", "the new + version", or similar status the org has not stated. +- Do not paraphrase `interdependent_way.md` as if it were a TL;DR. + Quote a single short line and link the file. + +## Output shape + +A good tour is short. Aim for: + +- One sentence naming the repo the visitor is in and what it does. +- One sentence widening to the org thesis. +- A 3–5 entry map of the most relevant sibling repos for the visitor's + apparent interest. +- A numbered reading list of 3–5 files, current-repo first. +- One closing line offering to go deeper on any item. + +If the visitor asks a specific question, answer it first and only then +offer the tour. diff --git a/.agents/skills/vm-mcp/SKILL.md b/.agents/skills/vm-mcp/SKILL.md new file mode 100644 index 0000000..9cfe192 --- /dev/null +++ b/.agents/skills/vm-mcp/SKILL.md @@ -0,0 +1,400 @@ +--- +name: vm-mcp +description: Private VM control-plane skill for giving an AI/MCP client SSH-like access to a Linux or Google Compute Engine VM without handing the client an SSH private key. Load this when a user asks to connect ChatGPT, an OpenAI client, Codex, Claude, or another MCP host to a VM; expose bounded shell/file tools on a private VM; replace repeated human SSH with an auditable MCP control plane; install or audit the shipped vm-mcp runtime; or add narrowly scoped administrative actions above the workspace shell. Do not load for ordinary human-only SSH setup with no MCP/agent access. +--- + +# vm-mcp — private VM control plane + +`vm-mcp` gives an MCP-capable agent operational contact with a private VM while +keeping SSH keys and cloud login credentials outside model context. + +The transport invariant remains: + +```text +human/bootstrap path: SSH / Google OS Login / IAP +model path: MCP -> authenticated private tunnel -> loopback vm-mcp +credential boundary: SSH/cloud credentials stay outside model context +``` + +The runtime now supports three explicit authority profiles rather than treating +minimal capability as the only safe shape. + +## Profiles + +```text +read-only + vm_info + list/read/stat + +workspace + read-only + + write/mkdir/move/remove under VM_MCP_ROOT + + shell_exec as confined non-root vmmcp + +personal-console + workspace + + user_exec as any explicit local non-root account + + admin_exec as root +``` + +`read-only` is the default and remains appropriate for first contact, shared +systems, or consumers that do not need mutation. `personal-console` is intended +for a **single-owner private VM** where broad administration is desired and the +owner prefers capability breadth over a narrow application API. + +The personal-console rule is: + +> Broad capability is allowed, but privilege transitions must remain obvious. + +`user_exec` and `admin_exec` are therefore separate tools. Named convenience +tools may be added later for ergonomics, but they are not permission cages and +do not replace the general execution primitives. + +## Trigger / non-trigger + +Load this skill when the requested result includes one or more of: + +- connect ChatGPT/OpenAI or another MCP client to a private Linux/GCE VM; +- inspect, write, or administer VM files through MCP; +- use a VM shell without placing an SSH private key in the model path; +- expose a broad personal VM console to one owner; +- install, update, audit, or troubleshoot canonical `vm-mcp`; +- distinguish ordinary non-root work from explicit root administration; +- replace repeated SSH pastes with persistent, auditable MCP contact. + +Do not load it for ordinary human-only SSH setup or a generic application +deployment where no MCP/agent VM contact is wanted. + +## Source of truth + +Priority: + +1. actual target VM facts: OS, accounts, filesystem, services, network and human recovery path; +2. this canonical `skill-lib/vm-mcp` runtime and tests; +3. current official MCP SDK/protocol documentation; +4. current target MCP client's connection, approval, and private-transport documentation; +5. `hmmm` for unresolved transport/client/host facts. + +Do not copy the control plane into an application repository and let that copy +become authority. Application repos may document how they consume the service. + +## Security / authority model + +The non-root MCP HTTP service retains its original containment: + +```text +vm-mcp.service + user: vmmcp + bind: 127.0.0.1 only + NoNewPrivileges=true + ProtectSystem=strict + empty Linux capabilities + Docker socket inaccessible + cloud metadata address denied + direct writes confined to VM_MCP_ROOT +``` + +Personal-console root access does **not** weaken that service. It adds a second +component: + +```text +vm-mcp.service (vmmcp) + | + | AF_UNIX /run/vm-mcp/admin.sock + v +vm-mcp-admin.service (root) + | + +-- user_exec(user, command, cwd) + | drops uid/gid/groups before exec + | + `-- admin_exec(command, cwd) + remains uid 0 explicitly +``` + +The broker: + +- accepts only Unix-domain connections; +- verifies the connecting process with `SO_PEERCRED` against the `vmmcp` uid; +- creates its socket root-owned and group-accessible only to `vmmcp`; +- requires itself to be uid 0 before executing requests; +- rejects `root` through `user_exec`; root must use the visibly privileged `admin_exec` surface; +- runs commands in dedicated process groups with bounded timeout/output and descendant cleanup; +- sanitizes the command environment rather than inheriting arbitrary service secrets; +- records request id, mode, run-as user, cwd, command SHA-256, exit status, and timeout in journald; +- returns the command/output to the MCP caller but does not print arbitrary environment secrets automatically. + +Personal-console is intentionally high authority. Anyone able to invoke +`admin_exec` effectively controls the host. Therefore it is appropriate only +when that authority matches the owner's intent and the MCP transport/account is +private and controlled by that owner. + +## Tools + +| Tool | read-only | workspace | personal-console | Boundary | +|---|---:|---:|---:|---| +| `vm_info` | yes | yes | yes | service/profile/limits | +| `list_directory` | yes | yes | yes | under `VM_MCP_ROOT` | +| `read_text` | yes | yes | yes | under `VM_MCP_ROOT`, bounded | +| `stat_path` | yes | yes | yes | under `VM_MCP_ROOT` | +| `write_text` | no | yes | yes | atomic UTF-8 write under root | +| `make_directory` | no | yes | yes | under root | +| `move_path` | no | yes | yes | source/destination under root | +| `remove_path` | no | yes | yes | under root; root itself refused | +| `shell_exec` | no | yes | yes | vmmcp + systemd confinement | +| `user_exec` | no | no | yes | arbitrary explicit non-root local user via broker | +| `admin_exec` | no | no | yes | root via explicit broker | + +Use `shell_exec` for work that should stay inside the MCP workspace. Use +`user_exec` when repository/application ownership belongs to another local +service or human account. Use `admin_exec` for host-level operations such as +systemd, packages, mounts, ownership, PostgreSQL provisioning, or recovery. + +## Installer behavior + +Canonical install: + +```bash +sudo VM_MCP_ROOT=/srv/vm-mcp/workspace \ + VM_MCP_PROFILE=read-only \ + bash vm-mcp/install.sh +``` + +Single-owner personal console: + +```bash +sudo VM_MCP_ROOT=/srv/vm-mcp/workspace \ + VM_MCP_PROFILE=personal-console \ + bash vm-mcp/install.sh +``` + +Important ownership rule: if `VM_MCP_ROOT` already exists, the installer leaves +that directory's ownership unchanged. It creates/chowns the root only when the +path does not yet exist. This prevents an existing application checkout such as +`/srv/stack` from accidentally being transferred to `vmmcp` merely because it +was selected as an MCP root. + +The install places immutable runtime code under `/opt/vm-mcp`, configuration in +`/etc/vm-mcp.env`, and starts `vm-mcp-admin.service` only for +`personal-console`. + +## Workflow + +### 1. Resolve the VM before mutation + +Observe rather than infer: + +```text +OS/distribution + Python +systemd availability +human SSH/OS Login recovery path +MCP private transport +intended VM_MCP_ROOT +ownership of application/repository paths +whether this is truly single-owner +current client write/destructive-operation support +``` + +Missing facts remain `hmmm`. + +### 2. Pin canonical skill-lib + +Install from an exact reviewed `The-Interdependency/skill-lib` commit. Record the +commit via `/opt/vm-mcp/SOURCE_COMMIT`. + +### 3. Run canonical tests before installation + +```bash +PYTHONPATH=vm-mcp python -m unittest discover -s vm-mcp/tests -p 'test_*.py' +``` + +When the full repository checkout is available, also run: + +```bash +python tools/check_skill_compliance.py +python tools/check_skill_lib_drift.py +python ratios/ratios_check.py --root . +python tools/build_codex_plugin_skills.py --check +python -m llms.build --root . --out llms.txt --check +``` + +### 4. Choose the profile deliberately + +Use `read-only` for first contact or shared deployments. Use `workspace` when a +confined work area and shell are enough. Use `personal-console` when the owner +explicitly wants a broad personal VM console, including root administration. + +Do not silently upgrade an existing deployment from a lower authority profile. + +### 5. Verify local containment + +```bash +sudo systemctl --no-pager --full status vm-mcp.service +ss -ltnp | grep ':8765' +``` + +Expected MCP listener: `127.0.0.1:8765`. + +For personal-console also verify: + +```bash +sudo systemctl --no-pager --full status vm-mcp-admin.service +sudo stat -c '%U %G %a %n' /run/vm-mcp /run/vm-mcp/admin.sock +``` + +Expected broker boundary: + +```text +broker process: root +socket transport: AF_UNIX only +/run/vm-mcp: root:vmmcp 750 +admin.sock: root:vmmcp 660 +``` + +### 6. Establish private MCP transport + +Never publish raw port `8765` to the public internet. Use the current client's +authenticated private-tunnel/private-network mechanism. Client capabilities are +time-sensitive; verify current official product documentation at connection +time rather than treating this skill's historical product snapshot as current. + +### 7. Exercise progressive authority + +For a personal console, verify in this order: + +```text +vm_info +read_text / list_directory +write_text inside disposable VM_MCP_ROOT fixture +shell_exec("id -u") +user_exec(, "id -u") +admin_exec("id -u") +``` + +Expected final result for `admin_exec("id -u")` is `0`. Confirm the corresponding +journald broker receipt before using root for real administration. + +### 8. Prefer ordinary authority when sufficient + +Even in personal-console mode, use the least surprising authority that can do +the job: + +```text +workspace file operation -> file tools +workspace diagnostic -> shell_exec +repo/service-account work -> user_exec +host administration -> admin_exec +``` + +This is an observability rule, not a capability prohibition. + +### 9. Keep human recovery independent + +SSH/OS Login/IAP remains bootstrap, rescue, and break-glass access. Do not remove +it merely because MCP works. + +## Personal-console examples + +Repository work as its owning service account: + +```text +user_exec( + user="stackorchestrator", + cwd="/srv/stack", + command="python -m frontend.cli.stackctl fresh status" +) +``` + +Host service inspection: + +```text +admin_exec( + cwd="/", + command="systemctl --no-pager --full status stack-orchestrator-worker.service" +) +``` + +Package or host maintenance is also possible through `admin_exec`; the tool is +not artificially limited to a predefined command vocabulary. That breadth is +the point of the single-owner profile. + +## Validation + +The shipped suite must cover at least: + +- parent/symlink path escape rejection; +- bounded file/directory/process output; +- read-only default and backward compatibility for the historical shell flag; +- workspace write gating and path confinement; +- shell cwd confinement, timeout, output bound, and environment sanitization; +- personal-console gate before broker use; +- root rejection through `user_exec`; +- root selection through `admin_exec`; +- broker root-process requirement; +- `SO_PEERCRED` caller verification; +- root:vmmcp socket permissions; +- non-root MCP systemd hardening remaining intact; +- loopback-only MCP listener and metadata-address denial; +- installer preserving ownership of an existing `VM_MCP_ROOT`. + +Actual VM acceptance must additionally exercise a real non-root `user_exec`, a +real root `admin_exec`, journald evidence, service restart, and rollback on the +target host. + +## Rollback + +Return to read-only without deleting human recovery access: + +```bash +sudo sed -i 's/^VM_MCP_PROFILE=.*/VM_MCP_PROFILE=read-only/' /etc/vm-mcp.env +sudo systemctl disable --now vm-mcp-admin.service +sudo systemctl restart vm-mcp.service +``` + +Full stop: + +```bash +sudo systemctl disable --now vm-mcp.service vm-mcp-admin.service +``` + +Disconnect the MCP client/tunnel as a separate control-plane action. + +## Anti-patterns + +- Putting SSH private keys, service-account keys, OAuth refresh tokens, or sudo passwords into MCP arguments or prompts. +- Opening port `8765` publicly because private transport is inconvenient. +- Running the MCP HTTP service itself as root merely to obtain admin capability. +- Hiding root execution behind a tool that looks non-privileged. +- Letting `user_exec(user="root", ...)` become an alias for root; use `admin_exec` visibly. +- Using an existing application checkout as `VM_MCP_ROOT` and changing its ownership as an installer side effect. +- Assuming `read-only` is always preferable when the owner intentionally wants a broad personal console. +- Assuming `personal-console` is appropriate for multi-user/shared systems merely because it is available. +- Treating named convenience tools as the only permissible operations in a single-owner console. +- Removing the independent SSH/OS Login recovery route after MCP contact succeeds. + +## Output shape + +When deploying or auditing, report: + +```text +source: exact skill-lib commit +vm: observed OS/layout +profile: read-only | workspace | personal-console +workspace: exact VM_MCP_ROOT + ownership +mcp service: user/bind/hardening/status +root broker: disabled | socket/status/permissions +private transport: observed status +shell_exec: enabled/disabled +user_exec: enabled/disabled +admin_exec: enabled/disabled +validation: commands actually executed + outcomes +human recovery: observed path +hmmm: unresolved constraints +``` + +Never describe a command as executed when it was only derived for another +environment. + +## hmmm + +- Private tunnel provisioning remains client/product infrastructure and must be resolved from the current official client surface at deployment time. +- The shipped broker intentionally grants broad root administration in `personal-console`; future shared/multi-owner deployments may need a separate policy/profile rather than weakening this profile into ambiguous partial authority. +- Named convenience tools for git, systemd, PostgreSQL, fresh-making, backups, and logs are useful ergonomics but are not required for capability because `user_exec` and `admin_exec` already expose the underlying operations. diff --git a/.agents/skills/vm-mcp/admin_broker.py b/.agents/skills/vm-mcp/admin_broker.py new file mode 100644 index 0000000..b9effc6 --- /dev/null +++ b/.agents/skills/vm-mcp/admin_broker.py @@ -0,0 +1,309 @@ +# ratios: loc_comments=222:57 imports_exports=16:3 calls_definitions=95:10 +"""Root-side Unix-socket execution broker for vm-mcp personal-console mode. + +Usage guidance: +- Run only as the root-owned ``vm-mcp-admin.service``. +- Accept requests only from the configured ``vmmcp`` Unix account. +- ``user`` mode drops to the requested non-root account before execution. +- ``admin`` mode remains root and is intentionally equivalent to broad host + administration; keep the MCP transport private and single-owner. +""" +from __future__ import annotations + +import grp +import hashlib +import json +import os +import pwd +import selectors +import signal +import socket +import subprocess +import struct +import sys +import time +import uuid +from pathlib import Path +from typing import Any + +# === MODULE_BUILD === +# id: vm_mcp_personal_console_broker +# module_name: vm_mcp_personal_console_broker +# module_kind: service +# summary: root-side Unix-socket broker for explicit non-root user_exec and root admin_exec in single-owner personal-console deployments +# owner: skill-lib vm-mcp maintainers +# public_surface: execute_request, serve +# internal_surface: peer credential verification, privilege drop, bounded process execution +# auth_boundary: admin +# storage_boundary: write +# network_boundary: local +# user_data_boundary: read_write +# admin_only: true +# tests: vm-mcp/tests/test_admin_broker.py +# rollout: vm-mcp-admin.service only when VM_MCP_PROFILE=personal-console +# rollback: stop and disable vm-mcp-admin.service; return vm-mcp to read-only or workspace profile +# === END MODULE_BUILD === + +# === BOUNDARIES === +# id: vm_mcp_admin_broker_root_boundary +# summary: root-owned local broker receives only verified vmmcp Unix-socket requests and may execute arbitrary host commands in personal-console mode +# auth_boundary: admin +# storage_boundary: write +# network_boundary: local +# user_data_boundary: read_write +# admin_only: true +# side_effects: process, filesystem, service, database, network +# owner: skill-lib vm-mcp maintainers +# === END BOUNDARIES === + +# === CONTRACTS === +# id: vm_mcp_admin_broker_peer_verified +# given: a process connects to the root broker Unix socket +# then: SO_PEERCRED must identify the configured vmmcp service user or the request is rejected +# class: security +# +# id: vm_mcp_user_exec_non_root +# given: personal-console user_exec requests a local user +# then: root is rejected and the child process drops uid/gid/groups to the requested non-root account before exec +# class: security +# +# id: vm_mcp_admin_exec_explicit_root +# given: personal-console admin_exec requests a command +# then: the broker executes it as uid 0 and reports root mode explicitly in the result +# class: authority +# +# id: vm_mcp_broker_execution_bounded +# given: a brokered command times out, emits excessive output, or leaves descendants running +# then: the process group is killed, output is capped, and terminal evidence is returned +# class: safety +# === END CONTRACTS === + +DEFAULT_SOCKET = Path("/run/vm-mcp/admin.sock") +DEFAULT_CALLER = "vmmcp" +MAX_REQUEST_BYTES = 1024 * 1024 +MAX_TIMEOUT_SECONDS = 3600.0 +MAX_OUTPUT_BYTES = 1024 * 1024 +_READ_CHUNK = 64 * 1024 + + +def _safe_env(account: pwd.struct_passwd) -> dict[str, str]: + return { + "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOME": account.pw_dir, + "USER": account.pw_name, + "LOGNAME": account.pw_name, + "SHELL": account.pw_shell or "/bin/bash", + "LANG": os.environ.get("LANG", "C.UTF-8"), + "GIT_TERMINAL_PROMPT": "0", + "PYTHONDONTWRITEBYTECODE": "1", + } + + +def _drop_privileges(account: pwd.struct_passwd) -> None: + os.initgroups(account.pw_name, account.pw_gid) + os.setgid(account.pw_gid) + os.setuid(account.pw_uid) + + +def _kill_group(process: subprocess.Popen[bytes]) -> None: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + + +def _communicate_bounded( + process: subprocess.Popen[bytes], *, timeout: float, limit: int +) -> tuple[bytes, bytes, bool, bool, bool]: + if process.stdout is None or process.stderr is None: + raise RuntimeError("stdout/stderr pipes are required") + selector = selectors.DefaultSelector() + for stream, name in ((process.stdout, "stdout"), (process.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, data=name) + buffers = {"stdout": bytearray(), "stderr": bytearray()} + truncated = {"stdout": False, "stderr": False} + deadline = time.monotonic() + timeout + timed_out = False + descendants_cleaned = False + try: + while selector.get_map(): + now = time.monotonic() + if not timed_out and now >= deadline: + timed_out = True + _kill_group(process) + descendants_cleaned = True + if process.poll() is not None and not descendants_cleaned: + _kill_group(process) + descendants_cleaned = True + events = selector.select(0.05 if timed_out else max(0.0, min(0.1, deadline - now))) + for key, _ in events: + stream = key.fileobj + name = key.data + try: + chunk = os.read(stream.fileno(), _READ_CHUNK) + except BlockingIOError: + continue + if not chunk: + selector.unregister(stream) + stream.close() + continue + remaining = max(0, limit - len(buffers[name])) + if remaining: + buffers[name].extend(chunk[:remaining]) + if len(chunk) > remaining: + truncated[name] = True + if timed_out and time.monotonic() > deadline + 2.0: + for key in list(selector.get_map().values()): + selector.unregister(key.fileobj) + key.fileobj.close() + finally: + selector.close() + if process.poll() is None: + _kill_group(process) + process.wait(timeout=2.0) + return ( + bytes(buffers["stdout"]), bytes(buffers["stderr"]), timed_out, + truncated["stdout"], truncated["stderr"], + ) + + +def _account_for_request(mode: str, user: str | None) -> pwd.struct_passwd: + if mode == "admin": + return pwd.getpwnam("root") + if mode != "user": + raise ValueError("mode must be 'user' or 'admin'") + if not user or user == "root": + raise ValueError("user mode requires an explicit non-root account") + account = pwd.getpwnam(user) + if account.pw_uid == 0: + raise ValueError("user mode cannot target uid 0") + return account + + +def execute_request(request: dict[str, Any]) -> dict[str, Any]: + if os.geteuid() != 0: + raise PermissionError("broker execution requires root") + mode = str(request.get("mode", "")) + user = request.get("user") + command = str(request.get("command", "")) + cwd = str(request.get("cwd", "/")) + if not command.strip(): + raise ValueError("command must not be empty") + directory = Path(cwd).expanduser().resolve() + if not directory.is_dir(): + raise NotADirectoryError(directory) + timeout = max(0.1, min(float(request.get("timeout_seconds", 60.0)), MAX_TIMEOUT_SECONDS)) + limit = max(1, min(int(request.get("max_output_bytes", 256 * 1024)), MAX_OUTPUT_BYTES)) + account = _account_for_request(mode, str(user) if user is not None else None) + preexec = None if account.pw_uid == 0 else lambda: _drop_privileges(account) + request_id = f"exec_{uuid.uuid4().hex}" + command_sha256 = hashlib.sha256(command.encode("utf-8")).hexdigest() + started = time.monotonic() + process = subprocess.Popen( + ["/bin/bash", "-lc", command], cwd=directory, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=_safe_env(account), preexec_fn=preexec, start_new_session=True, + ) + stdout, stderr, timed_out, stdout_truncated, stderr_truncated = _communicate_bounded( + process, timeout=timeout, limit=limit + ) + result = { + "ok": True, + "request_id": request_id, + "mode": mode, + "run_as": account.pw_name, + "uid": account.pw_uid, + "cwd": str(directory), + "command": command, + "command_sha256": command_sha256, + "exit_code": None if timed_out else process.returncode, + "timed_out": timed_out, + "duration_seconds": round(time.monotonic() - started, 6), + "stdout": stdout.decode("utf-8", errors="replace"), + "stderr": stderr.decode("utf-8", errors="replace"), + "stdout_truncated": stdout_truncated, + "stderr_truncated": stderr_truncated, + "output_limit_bytes_per_stream": limit, + } + print( + json.dumps({ + "event": "vm_mcp_exec", + "request_id": request_id, + "mode": mode, + "run_as": account.pw_name, + "cwd": str(directory), + "command_sha256": command_sha256, + "exit_code": result["exit_code"], + "timed_out": timed_out, + }, sort_keys=True), + file=sys.stderr, + flush=True, + ) + return result + + +def _peer_uid(connection: socket.socket) -> int: + if not hasattr(socket, "SO_PEERCRED"): + raise RuntimeError("SO_PEERCRED is required") + raw = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")) + _pid, uid, _gid = struct.unpack("3i", raw) + return uid + + +def _recv_request(connection: socket.socket) -> dict[str, Any]: + chunks: list[bytes] = [] + size = 0 + while True: + chunk = connection.recv(64 * 1024) + if not chunk: + break + size += len(chunk) + if size > MAX_REQUEST_BYTES: + raise ValueError("request exceeds broker limit") + chunks.append(chunk) + payload = b"".join(chunks).decode("utf-8").strip() + value = json.loads(payload) + if not isinstance(value, dict): + raise ValueError("request must be a JSON object") + return value + + +def serve() -> None: + if os.geteuid() != 0: + raise PermissionError("vm-mcp admin broker must run as root") + socket_path = Path(os.environ.get("VM_MCP_ADMIN_SOCKET", str(DEFAULT_SOCKET))) + caller_name = os.environ.get("VM_MCP_CALLER_USER", DEFAULT_CALLER) + caller = pwd.getpwnam(caller_name) + caller_group = grp.getgrnam(caller_name) + socket_path.parent.mkdir(parents=True, exist_ok=True) + os.chown(socket_path.parent, 0, caller_group.gr_gid) + os.chmod(socket_path.parent, 0o750) + socket_path.unlink(missing_ok=True) + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server: + server.bind(str(socket_path)) + os.chown(socket_path, 0, caller_group.gr_gid) + os.chmod(socket_path, 0o660) + server.listen(16) + while True: + connection, _ = server.accept() + with connection: + try: + peer_uid = _peer_uid(connection) + if peer_uid != caller.pw_uid: + raise PermissionError( + f"broker peer uid {peer_uid} is not configured caller uid {caller.pw_uid}" + ) + response = execute_request(_recv_request(connection)) + except Exception as exc: + response = {"ok": False, "error_type": type(exc).__name__, "error": str(exc)} + connection.sendall(json.dumps(response, separators=(",", ":")).encode("utf-8")) + + +def main() -> None: + serve() + + +if __name__ == "__main__": + main() +# ratios: loc_comments=222:57 imports_exports=16:3 calls_definitions=95:10 diff --git a/.agents/skills/vm-mcp/admin_client.py b/.agents/skills/vm-mcp/admin_client.py new file mode 100644 index 0000000..902714e --- /dev/null +++ b/.agents/skills/vm-mcp/admin_client.py @@ -0,0 +1,88 @@ +# ratios: loc_comments=46:33 imports_exports=5:1 calls_definitions=15:1 +"""Unix-socket client for the vm-mcp personal-console broker. + +Usage guidance: call :func:`request_exec` only from the non-root MCP service. +The broker socket is local-only and must be owned by root with group access for +``vmmcp``. +""" +from __future__ import annotations + +import json +import socket +from pathlib import Path +from typing import Any + +# === MODULE_BUILD === +# id: vm_mcp_admin_client +# module_name: vm_mcp_admin_client +# module_kind: adapter +# summary: sends bounded personal-console execution requests from the non-root MCP service to the local root broker +# owner: skill-lib vm-mcp maintainers +# public_surface: request_exec +# internal_surface: AF_UNIX JSON request/response transport +# auth_boundary: admin +# storage_boundary: none +# network_boundary: local +# user_data_boundary: read_write +# admin_only: true +# tests: vm-mcp/tests/test_assets.py +# rollout: imported by policy.py only in personal-console profile +# rollback: return deployment to workspace/read-only profile +# === END MODULE_BUILD === + +# === BOUNDARIES === +# id: vm_mcp_admin_client_socket_boundary +# summary: connects only to the configured local Unix-domain broker socket and sends no SSH/cloud credentials +# auth_boundary: admin +# storage_boundary: none +# network_boundary: local +# user_data_boundary: read_write +# admin_only: true +# side_effects: process +# owner: skill-lib vm-mcp maintainers +# === END BOUNDARIES === + +MAX_RESPONSE_BYTES = 2 * 1024 * 1024 + + +def request_exec( + *, + socket_path: Path, + mode: str, + user: str | None, + command: str, + cwd: str, + timeout_seconds: float, + max_output_bytes: int, +) -> dict[str, Any]: + request = { + "mode": mode, + "user": user, + "command": command, + "cwd": cwd, + "timeout_seconds": timeout_seconds, + "max_output_bytes": max_output_bytes, + } + encoded = (json.dumps(request, separators=(",", ":")) + "\n").encode("utf-8") + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: + client.connect(str(socket_path)) + client.sendall(encoded) + client.shutdown(socket.SHUT_WR) + chunks: list[bytes] = [] + size = 0 + while True: + chunk = client.recv(64 * 1024) + if not chunk: + break + size += len(chunk) + if size > MAX_RESPONSE_BYTES: + raise RuntimeError("admin broker response exceeded client limit") + chunks.append(chunk) + payload = b"".join(chunks).decode("utf-8") + response = json.loads(payload) + if not isinstance(response, dict): + raise RuntimeError("admin broker returned a non-object response") + if not response.get("ok"): + raise RuntimeError(str(response.get("error", "admin broker request failed"))) + return response +# ratios: loc_comments=46:33 imports_exports=5:1 calls_definitions=15:1 diff --git a/.agents/skills/vm-mcp/install.sh b/.agents/skills/vm-mcp/install.sh new file mode 100644 index 0000000..545f68c --- /dev/null +++ b/.agents/skills/vm-mcp/install.sh @@ -0,0 +1,151 @@ +# ratios: loc_comments=115:18 imports_exports=0:0 calls_definitions=2:0 +#!/usr/bin/env bash +set -euo pipefail + +# Install canonical vm-mcp on a Linux VM. +# +# Usage: +# sudo VM_MCP_ROOT=/srv/vm-mcp/workspace bash vm-mcp/install.sh +# sudo VM_MCP_PROFILE=personal-console \ +# VM_MCP_ROOT=/srv/vm-mcp/workspace bash vm-mcp/install.sh +# +# Profiles: +# read-only read tools only (default) +# workspace workspace write tools + confined shell_exec +# personal-console workspace tools plus brokered user_exec/admin_exec +# +# The MCP HTTP service always binds to 127.0.0.1. personal-console starts a +# separate root Unix-socket broker; it does not make the MCP service root. + +if [[ ${EUID} -ne 0 ]]; then + echo "ERROR: run as root (sudo ... bash vm-mcp/install.sh)" >&2 + exit 2 +fi + +SOURCE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_ROOT="${VM_MCP_INSTALL_ROOT:-/opt/vm-mcp}" +WORK_ROOT="${VM_MCP_ROOT:-/srv/vm-mcp/workspace}" +PROFILE="${VM_MCP_PROFILE:-read-only}" +PORT="${VM_MCP_PORT:-8765}" +SERVICE_USER="${VM_MCP_SERVICE_USER:-vmmcp}" +SERVICE_GROUP="$SERVICE_USER" +ADMIN_SOCKET="${VM_MCP_ADMIN_SOCKET:-/run/vm-mcp/admin.sock}" +NOLOGIN="$(command -v nologin || true)" +[[ -n "$NOLOGIN" ]] || NOLOGIN=/bin/false + +case "$PROFILE" in + read-only|workspace|personal-console) ;; + *) echo "ERROR: VM_MCP_PROFILE must be read-only, workspace, or personal-console" >&2; exit 3 ;; +esac +case "$WORK_ROOT" in + /*) ;; + *) echo "ERROR: VM_MCP_ROOT must be an absolute path" >&2; exit 3 ;; +esac +case "$WORK_ROOT" in + *$'\n'*|*$'\r'*) echo "ERROR: VM_MCP_ROOT must not contain newlines" >&2; exit 3 ;; +esac + +install_python() { + if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq python3 python3-venv ca-certificates >/dev/null + elif command -v dnf >/dev/null 2>&1; then + dnf install -y python3 ca-certificates >/dev/null + elif command -v yum >/dev/null 2>&1; then + yum install -y python3 ca-certificates >/dev/null + fi + command -v python3 >/dev/null 2>&1 || { + echo "ERROR: python3 is required and no supported package manager installed it" >&2 + exit 4 + } +} + +systemd_quote() { + local value=$1 + value=${value//\\/\\\\} + value=${value//\"/\\\"} + printf '"%s"' "$value" +} + +install_python + +if ! id "$SERVICE_USER" >/dev/null 2>&1; then + useradd --system --home "$WORK_ROOT/.vm-mcp-home" --shell "$NOLOGIN" "$SERVICE_USER" +else + usermod --home "$WORK_ROOT/.vm-mcp-home" "$SERVICE_USER" +fi + +# Existing application directories retain their existing ownership. This fixes +# the dangerous first-release behavior where selecting an existing VM_MCP_ROOT +# could transfer ownership of that directory to vmmcp. +if [[ ! -e "$WORK_ROOT" ]]; then + install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$WORK_ROOT" +elif [[ ! -d "$WORK_ROOT" ]]; then + echo "ERROR: VM_MCP_ROOT exists but is not a directory: $WORK_ROOT" >&2 + exit 5 +fi +install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$WORK_ROOT/.vm-mcp-home" +install -d -o root -g root -m 0755 "$INSTALL_ROOT" "$INSTALL_ROOT/systemd" + +for file in server.py policy.py admin_client.py admin_broker.py requirements.txt; do + install -m 0644 "$SOURCE_ROOT/$file" "$INSTALL_ROOT/$file" +done +install -m 0644 "$SOURCE_ROOT/systemd/vm-mcp.service" "$INSTALL_ROOT/systemd/vm-mcp.service" +install -m 0644 "$SOURCE_ROOT/systemd/vm-mcp-admin.service" "$INSTALL_ROOT/systemd/vm-mcp-admin.service" + +SOURCE_COMMIT=hmmm +if command -v git >/dev/null 2>&1 && git -C "$SOURCE_ROOT" rev-parse HEAD >/dev/null 2>&1; then + SOURCE_COMMIT="$(git -C "$SOURCE_ROOT" rev-parse HEAD)" +fi +printf '%s\n' "$SOURCE_COMMIT" > "$INSTALL_ROOT/SOURCE_COMMIT" +chmod 0644 "$INSTALL_ROOT/SOURCE_COMMIT" + +if [[ ! -x "$INSTALL_ROOT/.venv/bin/python" ]]; then + python3 -m venv "$INSTALL_ROOT/.venv" +fi +"$INSTALL_ROOT/.venv/bin/python" -m pip install --upgrade pip >/dev/null +"$INSTALL_ROOT/.venv/bin/python" -m pip install -r "$INSTALL_ROOT/requirements.txt" + +install -m 0644 "$SOURCE_ROOT/systemd/vm-mcp.service" /etc/systemd/system/vm-mcp.service +install -m 0644 "$SOURCE_ROOT/systemd/vm-mcp-admin.service" /etc/systemd/system/vm-mcp-admin.service +cat > /etc/vm-mcp.env < /etc/systemd/system/vm-mcp.service.d/workspace.conf </dev/null 2>&1 || true +fi +systemctl enable --now vm-mcp.service +systemctl restart vm-mcp.service + +printf '\nvm-mcp installed\n' +printf ' source commit: %s\n' "$SOURCE_COMMIT" +printf ' endpoint: http://127.0.0.1:%s/mcp\n' "$PORT" +printf ' workspace: %s\n' "$WORK_ROOT" +printf ' profile: %s\n' "$PROFILE" +if [[ "$PROFILE" == personal-console ]]; then + printf ' root broker: %s\n' "$ADMIN_SOCKET" + printf ' user_exec: enabled\n' + printf ' admin_exec: enabled (root)\n' +else + printf ' root broker: disabled\n' +fi +systemctl --no-pager --full status vm-mcp.service | sed -n '1,14p' +# ratios: loc_comments=115:18 imports_exports=0:0 calls_definitions=2:0 diff --git a/.agents/skills/vm-mcp/policy.py b/.agents/skills/vm-mcp/policy.py new file mode 100644 index 0000000..9968491 --- /dev/null +++ b/.agents/skills/vm-mcp/policy.py @@ -0,0 +1,481 @@ +# ratios: loc_comments=374:57 imports_exports=13:12 calls_definitions=134:24 +"""Policy primitives for the vm-mcp runtime. + +Usage guidance: +- Construct :class:`VmMcpConfig` from the service environment. +- Keep the default ``read-only`` profile for shared or first-contact installs. +- Use ``workspace`` for root-confined writes and shell execution. +- Use ``personal-console`` only for a single-owner private VM where broad + ``user_exec`` and explicit ``admin_exec`` are intended. +- Systemd confinement remains the host-write boundary for the non-root service; + personal-console host/root execution crosses a separate Unix-socket broker. +""" +from __future__ import annotations + +import os +import pwd +import selectors +import shutil +import signal +import socket +import subprocess +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +# === CONTRACTS === +# id: vm_mcp_read_paths_confined +# given: a file or directory tool receives a relative path, absolute path, parent traversal, or symlink target +# then: the resolved target must remain under VM_MCP_ROOT or the tool refuses access +# class: security +# +# id: vm_mcp_listing_symlinks_not_followed +# given: a directory listing encounters a symlink whose target is outside VM_MCP_ROOT +# then: the listing reports the symlink itself and does not follow the target for file metadata +# class: security +# +# id: vm_mcp_output_bounded +# given: a file, directory, or process result exceeds its configured response limit +# then: the response is capped and reports truncation visibly +# class: safety +# +# id: vm_mcp_profile_default_read_only +# given: the service starts without an explicit profile +# then: writes, shell execution, user execution, and admin execution are refused +# class: security +# +# id: vm_mcp_workspace_writes_confined +# given: a workspace write, move, directory creation, or removal is requested +# then: the resolved path remains under VM_MCP_ROOT and the profile must permit workspace mutation +# class: security +# +# id: vm_mcp_shell_cwd_confined +# given: shell execution receives a working directory outside VM_MCP_ROOT or through an escaping symlink +# then: execution is refused before a process is spawned +# class: security +# +# id: vm_mcp_shell_execution_bounded +# given: shell execution emits excessive output, exceeds its timeout, or tries to leave background descendants running +# then: output is capped, timed-out process groups are killed, and surviving descendants are killed before return +# class: safety +# +# id: vm_mcp_credentials_not_inherited +# given: the MCP service process has unrelated environment variables or host credentials +# then: shell execution receives a sanitized environment rather than the service process environment +# class: security +# +# id: vm_mcp_personal_console_explicit +# given: user_exec or admin_exec is requested +# then: the deployment must explicitly select the personal-console profile +# class: security +# === END CONTRACTS === + +DEFAULT_ROOT = Path("/srv/vm-mcp/workspace") +DEFAULT_ADMIN_SOCKET = Path("/run/vm-mcp/admin.sock") +DEFAULT_MAX_READ_BYTES = 256 * 1024 +DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024 +DEFAULT_MAX_TIMEOUT_SECONDS = 300.0 +DEFAULT_MAX_DIRECTORY_ENTRIES = 500 +PROFILES = ("read-only", "workspace", "personal-console") +_READ_CHUNK = 64 * 1024 + + +@dataclass(frozen=True) +class VmMcpConfig: + root: Path + profile: str + max_read_bytes: int + max_output_bytes: int + max_timeout_seconds: float + max_directory_entries: int + admin_socket: Path + + @property + def workspace_write_enabled(self) -> bool: + return self.profile in {"workspace", "personal-console"} + + @property + def shell_enabled(self) -> bool: + return self.profile in {"workspace", "personal-console"} + + @property + def personal_console_enabled(self) -> bool: + return self.profile == "personal-console" + + @classmethod + def from_env(cls) -> "VmMcpConfig": + profile = os.environ.get("VM_MCP_PROFILE", "").strip().lower() + if not profile: + # Backward compatibility with the first vm-mcp release. + profile = "workspace" if _env_flag("VM_MCP_SHELL_ENABLED", default=False) else "read-only" + if profile not in PROFILES: + raise ValueError(f"VM_MCP_PROFILE must be one of {PROFILES}: {profile!r}") + return cls( + root=Path(os.environ.get("VM_MCP_ROOT", str(DEFAULT_ROOT))).expanduser(), + profile=profile, + max_read_bytes=_positive_int( + os.environ.get("VM_MCP_MAX_READ_BYTES"), DEFAULT_MAX_READ_BYTES + ), + max_output_bytes=_positive_int( + os.environ.get("VM_MCP_MAX_OUTPUT_BYTES"), DEFAULT_MAX_OUTPUT_BYTES + ), + max_timeout_seconds=_positive_float( + os.environ.get("VM_MCP_MAX_TIMEOUT_SECONDS"), DEFAULT_MAX_TIMEOUT_SECONDS + ), + max_directory_entries=_positive_int( + os.environ.get("VM_MCP_MAX_DIRECTORY_ENTRIES"), DEFAULT_MAX_DIRECTORY_ENTRIES + ), + admin_socket=Path( + os.environ.get("VM_MCP_ADMIN_SOCKET", str(DEFAULT_ADMIN_SOCKET)) + ), + ) + + +def _env_flag(name: str, *, default: bool) -> bool: + raw = os.environ.get(name) + if raw is None: + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +def _positive_int(raw: str | None, default: int) -> int: + if raw is None: + return default + value = int(raw) + if value <= 0: + raise ValueError(f"configured integer limit must be positive: {raw!r}") + return value + + +def _positive_float(raw: str | None, default: float) -> float: + if raw is None: + return default + value = float(raw) + if value <= 0: + raise ValueError(f"configured timeout limit must be positive: {raw!r}") + return value + + +def resolve_under_root(root: Path, requested: str, *, must_exist: bool = True) -> Path: + root_resolved = root.expanduser().resolve(strict=False) + requested_path = Path(requested).expanduser() + candidate = ( + requested_path if requested_path.is_absolute() else root_resolved / requested_path + ).resolve(strict=False) + try: + candidate.relative_to(root_resolved) + except ValueError as exc: + raise PermissionError(f"path escapes VM_MCP_ROOT: {requested}") from exc + if must_exist and not candidate.exists(): + raise FileNotFoundError(candidate) + return candidate + + +def _require_workspace_write(config: VmMcpConfig) -> None: + if not config.workspace_write_enabled: + raise PermissionError( + "workspace mutation is disabled; set VM_MCP_PROFILE=workspace or personal-console" + ) + + +def _require_personal_console(config: VmMcpConfig) -> None: + if not config.personal_console_enabled: + raise PermissionError( + "host execution is disabled; set VM_MCP_PROFILE=personal-console" + ) + + +def vm_info(config: VmMcpConfig) -> dict[str, Any]: + root = config.root.expanduser().resolve(strict=False) + return { + "hostname": socket.gethostname(), + "user": pwd.getpwuid(os.getuid()).pw_name, + "root": str(root), + "root_exists": root.exists(), + "profile": config.profile, + "workspace_write_enabled": config.workspace_write_enabled, + "shell_enabled": config.shell_enabled, + "personal_console_enabled": config.personal_console_enabled, + "admin_socket": str(config.admin_socket), + "limits": { + "max_read_bytes": config.max_read_bytes, + "max_output_bytes": config.max_output_bytes, + "max_timeout_seconds": config.max_timeout_seconds, + "max_directory_entries": config.max_directory_entries, + }, + } + + +def list_directory( + config: VmMcpConfig, + requested: str = ".", + *, + max_entries: int = 200, +) -> dict[str, Any]: + directory = resolve_under_root(config.root, requested) + if not directory.is_dir(): + raise NotADirectoryError(directory) + limit = max(1, min(int(max_entries), config.max_directory_entries)) + entries: list[dict[str, Any]] = [] + truncated = False + for index, child in enumerate(sorted(directory.iterdir(), key=lambda p: p.name)): + if index >= limit: + truncated = True + break + stat = child.lstat() + kind = "symlink" if child.is_symlink() else "directory" if child.is_dir() else "file" + entries.append( + {"name": child.name, "kind": kind, "size": stat.st_size, "mtime_ns": stat.st_mtime_ns} + ) + return {"path": str(directory), "entries": entries, "truncated": truncated, "limit": limit} + + +def read_text( + config: VmMcpConfig, + requested: str, + *, + max_bytes: int = 64 * 1024, +) -> dict[str, Any]: + path = resolve_under_root(config.root, requested) + if not path.is_file(): + raise IsADirectoryError(path) + limit = max(1, min(int(max_bytes), config.max_read_bytes)) + with path.open("rb") as handle: + raw = handle.read(limit + 1) + truncated = len(raw) > limit + payload = raw[:limit] + return { + "path": str(path), + "text": payload.decode("utf-8", errors="replace"), + "bytes_read": len(payload), + "truncated": truncated, + "limit": limit, + } + + +def stat_path(config: VmMcpConfig, requested: str) -> dict[str, Any]: + path = resolve_under_root(config.root, requested) + stat = path.lstat() + return { + "path": str(path), + "kind": "symlink" if path.is_symlink() else "directory" if path.is_dir() else "file", + "size": stat.st_size, + "mode": oct(stat.st_mode & 0o7777), + "uid": stat.st_uid, + "gid": stat.st_gid, + "mtime_ns": stat.st_mtime_ns, + } + + +def write_text( + config: VmMcpConfig, + requested: str, + text: str, + *, + create_parents: bool = False, +) -> dict[str, Any]: + _require_workspace_write(config) + path = resolve_under_root(config.root, requested, must_exist=False) + if create_parents: + parent = resolve_under_root(config.root, str(path.parent), must_exist=False) + parent.mkdir(parents=True, exist_ok=True) + elif not path.parent.is_dir(): + raise FileNotFoundError(path.parent) + encoded = text.encode("utf-8") + if len(encoded) > config.max_read_bytes: + raise ValueError("write payload exceeds VM_MCP_MAX_READ_BYTES") + tmp = path.with_name(f".{path.name}.vm-mcp.tmp") + tmp.write_bytes(encoded) + os.replace(tmp, path) + return {"path": str(path), "bytes_written": len(encoded)} + + +def make_directory( + config: VmMcpConfig, + requested: str, + *, + parents: bool = False, +) -> dict[str, Any]: + _require_workspace_write(config) + path = resolve_under_root(config.root, requested, must_exist=False) + path.mkdir(parents=parents, exist_ok=True) + return {"path": str(path), "created": True} + + +def move_path(config: VmMcpConfig, source: str, destination: str) -> dict[str, Any]: + _require_workspace_write(config) + src = resolve_under_root(config.root, source) + dst = resolve_under_root(config.root, destination, must_exist=False) + if not dst.parent.is_dir(): + raise FileNotFoundError(dst.parent) + os.replace(src, dst) + return {"source": str(src), "destination": str(dst)} + + +def remove_path(config: VmMcpConfig, requested: str, *, recursive: bool = False) -> dict[str, Any]: + _require_workspace_write(config) + path = resolve_under_root(config.root, requested) + if path == config.root.expanduser().resolve(strict=False): + raise PermissionError("refusing to remove VM_MCP_ROOT") + if path.is_dir() and not path.is_symlink(): + if recursive: + shutil.rmtree(path) + else: + path.rmdir() + else: + path.unlink() + return {"path": str(path), "removed": True, "recursive": recursive} + + +def _sanitized_subprocess_env() -> dict[str, str]: + user = pwd.getpwuid(os.getuid()) + env = { + "PATH": os.environ.get( + "VM_MCP_EXEC_PATH", + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + ), + "HOME": user.pw_dir, + "USER": user.pw_name, + "LOGNAME": user.pw_name, + "LANG": os.environ.get("LANG", "C.UTF-8"), + "GIT_TERMINAL_PROMPT": "0", + "PYTHONDONTWRITEBYTECODE": "1", + } + if os.environ.get("LC_ALL"): + env["LC_ALL"] = os.environ["LC_ALL"] + return env + + +def _kill_group(process: subprocess.Popen[bytes]) -> None: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + + +def _communicate_bounded( + process: subprocess.Popen[bytes], *, timeout: float, limit: int +) -> tuple[bytes, bytes, bool, bool, bool]: + if process.stdout is None or process.stderr is None: + raise RuntimeError("stdout/stderr pipes are required") + selector = selectors.DefaultSelector() + streams = ((process.stdout, "stdout"), (process.stderr, "stderr")) + for stream, name in streams: + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, data=name) + buffers = {"stdout": bytearray(), "stderr": bytearray()} + truncated = {"stdout": False, "stderr": False} + deadline = time.monotonic() + timeout + timed_out = False + descendants_cleaned = False + try: + while selector.get_map(): + now = time.monotonic() + if not timed_out and now >= deadline: + timed_out = True + _kill_group(process) + descendants_cleaned = True + if process.poll() is not None and not descendants_cleaned: + _kill_group(process) + descendants_cleaned = True + wait = 0.05 if timed_out else max(0.0, min(0.1, deadline - now)) + events = selector.select(wait) + for key, _ in events: + stream = key.fileobj + name = key.data + try: + chunk = os.read(stream.fileno(), _READ_CHUNK) + except BlockingIOError: + continue + if not chunk: + selector.unregister(stream) + stream.close() + continue + remaining = max(0, limit - len(buffers[name])) + if remaining: + buffers[name].extend(chunk[:remaining]) + if len(chunk) > remaining: + truncated[name] = True + if timed_out and time.monotonic() > deadline + 2.0: + for key in list(selector.get_map().values()): + selector.unregister(key.fileobj) + key.fileobj.close() + finally: + selector.close() + if process.poll() is None: + _kill_group(process) + process.wait(timeout=2.0) + return ( + bytes(buffers["stdout"]), bytes(buffers["stderr"]), timed_out, + truncated["stdout"], truncated["stderr"], + ) + + +def run_shell( + config: VmMcpConfig, + command: str, + *, + cwd: str = ".", + timeout_seconds: float = 60.0, +) -> dict[str, Any]: + if not config.shell_enabled: + raise PermissionError( + "shell_exec is disabled; set VM_MCP_PROFILE=workspace or personal-console" + ) + if not command.strip(): + raise ValueError("command must not be empty") + directory = resolve_under_root(config.root, cwd) + if not directory.is_dir(): + raise NotADirectoryError(directory) + timeout = max(0.1, min(float(timeout_seconds), config.max_timeout_seconds)) + process = subprocess.Popen( + ["/bin/bash", "-lc", command], cwd=directory, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=_sanitized_subprocess_env(), start_new_session=True, + ) + stdout, stderr, timed_out, stdout_truncated, stderr_truncated = _communicate_bounded( + process, timeout=timeout, limit=config.max_output_bytes + ) + return { + "command": command, + "cwd": str(directory), + "exit_code": None if timed_out else process.returncode, + "timed_out": timed_out, + "timeout_seconds": timeout, + "stdout": stdout.decode("utf-8", errors="replace"), + "stderr": stderr.decode("utf-8", errors="replace"), + "stdout_truncated": stdout_truncated, + "stderr_truncated": stderr_truncated, + "output_limit_bytes_per_stream": config.max_output_bytes, + } + + +def broker_exec( + config: VmMcpConfig, + *, + mode: str, + command: str, + cwd: str, + user: str | None = None, + timeout_seconds: float = 60.0, +) -> dict[str, Any]: + _require_personal_console(config) + if mode not in {"user", "admin"}: + raise ValueError(f"unsupported broker mode: {mode}") + if mode == "user" and (not user or user == "root"): + raise ValueError("user_exec requires an explicit non-root local user") + if not command.strip(): + raise ValueError("command must not be empty") + timeout = max(0.1, min(float(timeout_seconds), config.max_timeout_seconds)) + from admin_client import request_exec + return request_exec( + socket_path=config.admin_socket, + mode=mode, + user=user, + command=command, + cwd=cwd, + timeout_seconds=timeout, + max_output_bytes=config.max_output_bytes, + ) +# ratios: loc_comments=374:57 imports_exports=13:12 calls_definitions=134:24 diff --git a/.agents/skills/vm-mcp/requirements.txt b/.agents/skills/vm-mcp/requirements.txt new file mode 100644 index 0000000..e2938c6 --- /dev/null +++ b/.agents/skills/vm-mcp/requirements.txt @@ -0,0 +1,3 @@ +# Current stable MCP Python SDK major line as verified 2026-08-07. +# Keep the upper bound so a future major cannot silently replace the runtime API. +mcp>=2,<3 diff --git a/.agents/skills/vm-mcp/server.py b/.agents/skills/vm-mcp/server.py new file mode 100644 index 0000000..f31147c --- /dev/null +++ b/.agents/skills/vm-mcp/server.py @@ -0,0 +1,190 @@ +# ratios: loc_comments=89:60 imports_exports=6:12 calls_definitions=37:14 +"""MCP server for bounded or personal-console access to a private Linux VM. + +Usage guidance: + VM_MCP_PROFILE=read-only python server.py + VM_MCP_PROFILE=workspace python server.py + VM_MCP_PROFILE=personal-console python server.py + +The service always binds to loopback. ``personal-console`` additionally exposes +explicit ``user_exec`` and ``admin_exec`` through the separate root broker; use +that profile only for a single-owner private VM behind an authenticated tunnel. +""" +from __future__ import annotations + +import os +from typing import Any, Callable + +from mcp.server import MCPServer +from mcp.types import ToolAnnotations + +from policy import ( + VmMcpConfig, + broker_exec, + list_directory as policy_list_directory, + make_directory as policy_make_directory, + move_path as policy_move_path, + read_text as policy_read_text, + remove_path as policy_remove_path, + run_shell as policy_run_shell, + stat_path as policy_stat_path, + vm_info as policy_vm_info, + write_text as policy_write_text, +) + +# === MODULE_BUILD === +# id: vm_mcp_control_plane +# module_name: vm_mcp_control_plane +# module_kind: service +# summary: exposes loopback-only VM inspection, workspace mutation, confined shell, and explicit personal-console user/root execution surfaces +# owner: skill-lib vm-mcp maintainers +# public_surface: vm_info, list_directory, read_text, stat_path, write_text, make_directory, move_path, remove_path, shell_exec, user_exec, admin_exec +# internal_surface: policy.py, admin_client.py, admin_broker.py +# auth_boundary: admin +# storage_boundary: write +# network_boundary: external +# user_data_boundary: read_write +# admin_only: true +# tests: vm-mcp/tests/test_policy.py, vm-mcp/tests/test_assets.py, vm-mcp/tests/test_admin_broker.py +# rollout: root_installer_plus_systemd_plus_private_mcp_tunnel +# rollback: disable vm-mcp services and remove private client registration +# feature_flag: VM_MCP_PROFILE +# unresolved: client_specific_private_tunnel_registration, application_layer_auth_when_not_using_a_private_tunnel +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: vm_mcp_loopback_only +# given: the MCP server starts with its shipped runtime configuration +# then: Streamable HTTP binds to 127.0.0.1 on /mcp rather than a public interface +# class: security +# +# id: vm_mcp_host_write_confined +# given: the non-root MCP service starts with its shipped systemd configuration +# then: NoNewPrivileges remains enabled, Linux capabilities are empty, and direct service writes stay under VM_MCP_ROOT +# class: security +# +# id: vm_mcp_metadata_credentials_blocked +# given: the non-root MCP service attempts to reach the standard cloud metadata-service address +# then: the systemd network policy denies 169.254.169.254 +# class: security +# +# id: vm_mcp_personal_console_root_separate +# given: personal-console admin_exec is enabled +# then: root execution occurs only in the separate Unix-socket broker and not by making the MCP service root +# class: authority +# === END CONTRACTS === + +mcp = MCPServer("vm-mcp") + +READ_ONLY = ToolAnnotations(readOnlyHint=True, openWorldHint=False) +WRITE_FS = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=False) +WRITE_SHELL = ToolAnnotations(readOnlyHint=False, destructiveHint=True, idempotentHint=False, openWorldHint=True) +ADMIN_SHELL = ToolAnnotations(readOnlyHint=False, destructiveHint=True, idempotentHint=False, openWorldHint=True) + + +def _config() -> VmMcpConfig: + return VmMcpConfig.from_env() + + +def _result(call: Callable[[], dict[str, Any]]) -> dict[str, Any]: + try: + return {"ok": True, **call()} + except (OSError, ValueError, PermissionError, RuntimeError) as exc: + return {"ok": False, "error_type": type(exc).__name__, "error": str(exc)} + + +@mcp.tool(annotations=READ_ONLY) +def vm_info() -> dict[str, Any]: + """Return service identity, profile, workspace root, broker socket, and limits.""" + return _result(lambda: policy_vm_info(_config())) + + +@mcp.tool(annotations=READ_ONLY) +def list_directory(path: str = ".", max_entries: int = 200) -> dict[str, Any]: + """List one directory under VM_MCP_ROOT without following outside symlinks.""" + return _result(lambda: policy_list_directory(_config(), path, max_entries=max_entries)) + + +@mcp.tool(annotations=READ_ONLY) +def read_text(path: str, max_bytes: int = 65536) -> dict[str, Any]: + """Read bounded UTF-8-compatible text from a file under VM_MCP_ROOT.""" + return _result(lambda: policy_read_text(_config(), path, max_bytes=max_bytes)) + + +@mcp.tool(annotations=READ_ONLY) +def stat_path(path: str) -> dict[str, Any]: + """Return bounded metadata for one path under VM_MCP_ROOT.""" + return _result(lambda: policy_stat_path(_config(), path)) + + +@mcp.tool(annotations=WRITE_FS) +def write_text(path: str, text: str, create_parents: bool = False) -> dict[str, Any]: + """Atomically write one UTF-8 text file under VM_MCP_ROOT.""" + return _result(lambda: policy_write_text(_config(), path, text, create_parents=create_parents)) + + +@mcp.tool(annotations=WRITE_FS) +def make_directory(path: str, parents: bool = False) -> dict[str, Any]: + """Create a directory under VM_MCP_ROOT.""" + return _result(lambda: policy_make_directory(_config(), path, parents=parents)) + + +@mcp.tool(annotations=WRITE_FS) +def move_path(source: str, destination: str) -> dict[str, Any]: + """Atomically move one path to another location under VM_MCP_ROOT.""" + return _result(lambda: policy_move_path(_config(), source, destination)) + + +@mcp.tool(annotations=WRITE_SHELL) +def remove_path(path: str, recursive: bool = False) -> dict[str, Any]: + """Remove a file/symlink or, when requested, a directory under VM_MCP_ROOT.""" + return _result(lambda: policy_remove_path(_config(), path, recursive=recursive)) + + +@mcp.tool(annotations=WRITE_SHELL) +def shell_exec(command: str, cwd: str = ".", timeout_seconds: float = 60.0) -> dict[str, Any]: + """Run one bounded shell command as the confined non-root vm-mcp service user.""" + return _result(lambda: policy_run_shell(_config(), command, cwd=cwd, timeout_seconds=timeout_seconds)) + + +@mcp.tool(annotations=WRITE_SHELL) +def user_exec( + user: str, + command: str, + cwd: str = "/", + timeout_seconds: float = 60.0, +) -> dict[str, Any]: + """Personal-console only: execute as an explicit local non-root user.""" + return _result( + lambda: broker_exec( + _config(), mode="user", user=user, command=command, cwd=cwd, + timeout_seconds=timeout_seconds, + ) + ) + + +@mcp.tool(annotations=ADMIN_SHELL) +def admin_exec(command: str, cwd: str = "/", timeout_seconds: float = 60.0) -> dict[str, Any]: + """Personal-console only: execute an explicitly privileged command as root.""" + return _result( + lambda: broker_exec( + _config(), mode="admin", user=None, command=command, cwd=cwd, + timeout_seconds=timeout_seconds, + ) + ) + + +def main() -> None: + mcp.run( + transport="streamable-http", + host="127.0.0.1", + port=int(os.environ.get("VM_MCP_PORT", "8765")), + streamable_http_path="/mcp", + stateless_http=True, + json_response=True, + ) + + +if __name__ == "__main__": + main() +# ratios: loc_comments=89:60 imports_exports=6:12 calls_definitions=37:14 diff --git a/.agents/skills/vm-mcp/systemd/vm-mcp-admin.service b/.agents/skills/vm-mcp/systemd/vm-mcp-admin.service new file mode 100644 index 0000000..7c0d224 --- /dev/null +++ b/.agents/skills/vm-mcp/systemd/vm-mcp-admin.service @@ -0,0 +1,25 @@ +[Unit] +Description=skill-lib VM MCP personal-console root broker +After=local-fs.target +Before=vm-mcp.service + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=/opt/vm-mcp +Environment=VM_MCP_ADMIN_SOCKET=/run/vm-mcp/admin.sock +Environment=VM_MCP_CALLER_USER=vmmcp +Environment=PYTHONDONTWRITEBYTECODE=1 +ExecStart=/opt/vm-mcp/.venv/bin/python /opt/vm-mcp/admin_broker.py +Restart=on-failure +RestartSec=2 +UMask=0077 +RuntimeDirectory=vm-mcp +RuntimeDirectoryMode=0750 +RestrictAddressFamilies=AF_UNIX +TasksMax=256 +LimitNOFILE=4096 + +[Install] +WantedBy=multi-user.target diff --git a/.agents/skills/vm-mcp/systemd/vm-mcp.service b/.agents/skills/vm-mcp/systemd/vm-mcp.service new file mode 100644 index 0000000..623cb52 --- /dev/null +++ b/.agents/skills/vm-mcp/systemd/vm-mcp.service @@ -0,0 +1,47 @@ +[Unit] +Description=skill-lib VM MCP control plane +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=vmmcp +Group=vmmcp +WorkingDirectory=/opt/vm-mcp +EnvironmentFile=-/etc/vm-mcp.env +Environment=VM_MCP_ROOT=/srv/vm-mcp/workspace +Environment=VM_MCP_PROFILE=read-only +Environment=VM_MCP_PORT=8765 +Environment=VM_MCP_ADMIN_SOCKET=/run/vm-mcp/admin.sock +Environment=PYTHONDONTWRITEBYTECODE=1 +ExecStart=/opt/vm-mcp/.venv/bin/python /opt/vm-mcp/server.py +Restart=on-failure +RestartSec=2 +UMask=0077 +NoNewPrivileges=true +PrivateTmp=true +PrivateDevices=true +ProtectSystem=strict +ProtectHome=true +ProtectControlGroups=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true +ProtectClock=true +ProtectHostname=true +ProtectProc=invisible +ProcSubset=pid +RestrictSUIDSGID=true +RestrictRealtime=true +LockPersonality=true +CapabilityBoundingSet= +AmbientCapabilities= +ReadWritePaths=/srv/vm-mcp/workspace +InaccessiblePaths=-/run/docker.sock -/var/run/docker.sock +IPAddressDeny=169.254.169.254 +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +TasksMax=256 +LimitNOFILE=4096 + +[Install] +WantedBy=multi-user.target diff --git a/.agents/skills/vm-mcp/tests/test_admin_broker.py b/.agents/skills/vm-mcp/tests/test_admin_broker.py new file mode 100644 index 0000000..6f83795 --- /dev/null +++ b/.agents/skills/vm-mcp/tests/test_admin_broker.py @@ -0,0 +1,88 @@ +"""Contract tests for personal-console user/root execution separation.""" +from __future__ import annotations + +# === CHECKS === +# id: check_vm_mcp_user_exec_non_root +# proves: vm_mcp_user_exec_non_root +# call: self::test_user_mode_rejects_root +# mutates: none +# cleanup: none +# +# id: check_vm_mcp_admin_exec_explicit_root +# proves: vm_mcp_admin_exec_explicit_root +# call: self::test_admin_mode_selects_root +# mutates: none +# cleanup: none +# +# id: check_vm_mcp_broker_execution_bounded +# proves: vm_mcp_broker_execution_bounded +# call: self::test_admin_execution_timeout_is_bounded_when_root +# mutates: process +# cleanup: process_group_killed +# +# id: check_vm_mcp_admin_broker_peer_verified +# proves: vm_mcp_admin_broker_peer_verified +# call: self::test_serve_source_uses_peer_credentials +# mutates: none +# cleanup: none +# === END CHECKS === + +import pwd +import unittest +from pathlib import Path +from unittest.mock import patch + +import admin_broker + + +class VmMcpAdminBrokerTests(unittest.TestCase): + def test_user_mode_rejects_root(self) -> None: + with self.assertRaises(ValueError): + admin_broker._account_for_request("user", "root") + + def test_admin_mode_selects_root(self) -> None: + account = admin_broker._account_for_request("admin", None) + self.assertEqual(account.pw_uid, 0) + self.assertEqual(account.pw_name, "root") + + def test_user_mode_selects_non_root_account(self) -> None: + current = pwd.getpwuid(__import__("os").getuid()) + if current.pw_uid == 0: + self.skipTest("test runner is root; no portable non-root fixture") + account = admin_broker._account_for_request("user", current.pw_name) + self.assertEqual(account.pw_uid, current.pw_uid) + + def test_serve_source_uses_peer_credentials(self) -> None: + text = Path(admin_broker.__file__).read_text(encoding="utf-8") + self.assertIn("SO_PEERCRED", text) + self.assertIn("peer_uid != caller.pw_uid", text) + self.assertIn("os.chmod(socket_path, 0o660)", text) + + def test_admin_execution_timeout_is_bounded_when_root(self) -> None: + import os + if os.geteuid() != 0: + self.skipTest("root broker integration requires root test process") + result = admin_broker.execute_request({ + "mode": "admin", "command": "sleep 2", "cwd": "/", + "timeout_seconds": 0.1, "max_output_bytes": 1024, + }) + self.assertTrue(result["timed_out"]) + self.assertIsNone(result["exit_code"]) + + def test_request_rejects_empty_command_before_spawn(self) -> None: + with patch("admin_broker.os.geteuid", return_value=0), \ + patch("admin_broker.subprocess.Popen") as popen: + with self.assertRaises(ValueError): + admin_broker.execute_request({"mode": "admin", "command": "", "cwd": "/"}) + popen.assert_not_called() + + def test_execution_requires_root_broker_process(self) -> None: + with patch("admin_broker.os.geteuid", return_value=1000), \ + patch("admin_broker.subprocess.Popen") as popen: + with self.assertRaises(PermissionError): + admin_broker.execute_request({"mode": "admin", "command": "true", "cwd": "/"}) + popen.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/vm-mcp/tests/test_assets.py b/.agents/skills/vm-mcp/tests/test_assets.py new file mode 100644 index 0000000..e9d1904 --- /dev/null +++ b/.agents/skills/vm-mcp/tests/test_assets.py @@ -0,0 +1,88 @@ +"""Static contract checks for vm-mcp deployment assets.""" +from __future__ import annotations + +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +# === CHECKS === +# id: check_vm_mcp_loopback_config +# proves: vm_mcp_loopback_only +# call: self::test_server_binds_loopback_only +# mutates: none +# cleanup: none +# +# id: check_vm_mcp_systemd_write_boundary +# proves: vm_mcp_host_write_confined +# call: self::test_non_root_service_keeps_hardening +# mutates: none +# cleanup: none +# +# id: check_vm_mcp_metadata_denial +# proves: vm_mcp_metadata_credentials_blocked +# call: self::test_systemd_blocks_cloud_metadata_address +# mutates: none +# cleanup: none +# +# id: check_vm_mcp_personal_console_root_separate +# proves: vm_mcp_personal_console_root_separate +# call: self::test_personal_console_uses_separate_root_broker +# mutates: none +# cleanup: none +# === END CHECKS === + + +class VmMcpAssetTests(unittest.TestCase): + def test_server_binds_loopback_only(self) -> None: + text = (ROOT / "server.py").read_text(encoding="utf-8") + self.assertIn('host="127.0.0.1"', text) + self.assertIn('streamable_http_path="/mcp"', text) + self.assertNotIn('host="0.0.0.0"', text) + + def test_non_root_service_keeps_hardening(self) -> None: + text = (ROOT / "systemd" / "vm-mcp.service").read_text(encoding="utf-8") + for expected in ( + "User=vmmcp", + "NoNewPrivileges=true", + "ProtectSystem=strict", + "CapabilityBoundingSet=\n", + "AmbientCapabilities=\n", + "ReadWritePaths=/srv/vm-mcp/workspace", + "InaccessiblePaths=-/run/docker.sock -/var/run/docker.sock", + ): + self.assertIn(expected, text) + + def test_systemd_blocks_cloud_metadata_address(self) -> None: + text = (ROOT / "systemd" / "vm-mcp.service").read_text(encoding="utf-8") + self.assertIn("IPAddressDeny=169.254.169.254", text) + + def test_personal_console_uses_separate_root_broker(self) -> None: + service = (ROOT / "systemd" / "vm-mcp-admin.service").read_text(encoding="utf-8") + server = (ROOT / "server.py").read_text(encoding="utf-8") + installer = (ROOT / "install.sh").read_text(encoding="utf-8") + self.assertIn("User=root", service) + self.assertIn("RestrictAddressFamilies=AF_UNIX", service) + broker = (ROOT / "admin_broker.py").read_text(encoding="utf-8") + self.assertIn("os.chown(socket_path.parent, 0, caller_group.gr_gid)", broker) + self.assertIn("os.chmod(socket_path, 0o660)", broker) + self.assertIn("def admin_exec", server) + self.assertIn("def user_exec", server) + self.assertIn("VM_MCP_PROFILE=personal-console", installer) + self.assertNotIn("User=root\nGroup=root\nWorkingDirectory=/opt/vm-mcp\nEnvironmentFile", (ROOT / "systemd" / "vm-mcp.service").read_text(encoding="utf-8")) + + def test_installer_preserves_existing_workspace_ownership(self) -> None: + text = (ROOT / "install.sh").read_text(encoding="utf-8") + self.assertIn('if [[ ! -e "$WORK_ROOT" ]]', text) + self.assertNotIn('install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$WORK_ROOT"\ninstall -d', text) + + def test_runtime_uses_current_v2_sdk(self) -> None: + requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8") + server = (ROOT / "server.py").read_text(encoding="utf-8") + self.assertIn("mcp>=2,<3", requirements) + self.assertIn("from mcp.server import MCPServer", server) + self.assertNotIn("from mcp.server.fastmcp", server) + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/vm-mcp/tests/test_policy.py b/.agents/skills/vm-mcp/tests/test_policy.py new file mode 100644 index 0000000..01b8856 --- /dev/null +++ b/.agents/skills/vm-mcp/tests/test_policy.py @@ -0,0 +1,234 @@ +"""Contract tests for vm-mcp profiles, workspace policy, and confined shell. + +Run: + PYTHONPATH=vm-mcp python -m unittest discover -s vm-mcp/tests -p 'test_*.py' +""" +from __future__ import annotations + +import os +import tempfile +import time +import unittest +from pathlib import Path +from unittest.mock import patch + +# === CHECKS === +# id: check_vm_mcp_parent_escape_rejected +# proves: vm_mcp_read_paths_confined +# call: self::test_parent_escape_rejected +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_symlink_escape_rejected +# proves: vm_mcp_read_paths_confined +# call: self::test_symlink_escape_rejected +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_listing_symlink_not_followed +# proves: vm_mcp_listing_symlinks_not_followed +# call: self::test_listing_does_not_follow_symlink_metadata +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_output_bounded +# proves: vm_mcp_output_bounded +# call: self::test_read_text_is_bounded +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_profile_default_read_only +# proves: vm_mcp_profile_default_read_only +# call: self::test_default_profile_is_read_only +# mutates: process_environment +# cleanup: patch_dict_rollback +# +# id: check_vm_mcp_workspace_write_gate +# proves: vm_mcp_workspace_writes_confined +# call: self::test_write_requires_mutating_profile +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_workspace_write_confined +# proves: vm_mcp_workspace_writes_confined +# call: self::test_write_and_move_remain_under_root +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_personal_console_gate +# proves: vm_mcp_personal_console_explicit +# call: self::test_broker_exec_requires_personal_console +# mutates: none +# cleanup: none +# +# id: check_vm_mcp_shell_cwd_escape_rejected +# proves: vm_mcp_shell_cwd_confined +# call: self::test_shell_cwd_escape_rejected +# mutates: filesystem +# cleanup: tempdir_teardown +# +# id: check_vm_mcp_shell_output_bounded +# proves: vm_mcp_shell_execution_bounded +# call: self::test_shell_output_is_bounded_while_draining +# mutates: process +# cleanup: process_group_killed +# +# id: check_vm_mcp_shell_timeout +# proves: vm_mcp_shell_execution_bounded +# call: self::test_shell_timeout_is_enforced +# mutates: process +# cleanup: process_group_killed +# +# id: check_vm_mcp_environment_sanitized +# proves: vm_mcp_credentials_not_inherited +# call: self::test_shell_does_not_inherit_unrelated_environment +# mutates: process_environment +# cleanup: patch_dict_rollback +# === END CHECKS === + +from policy import ( + VmMcpConfig, + broker_exec, + list_directory, + move_path, + read_text, + resolve_under_root, + run_shell, + write_text, +) + + +class VmMcpPolicyTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + + def tearDown(self) -> None: + self.temp.cleanup() + + def config(self, *, profile: str = "read-only", output: int = 64) -> VmMcpConfig: + return VmMcpConfig( + root=self.root, + profile=profile, + max_read_bytes=64, + max_output_bytes=output, + max_timeout_seconds=3.0, + max_directory_entries=10, + admin_socket=self.root / "admin.sock", + ) + + def test_default_profile_is_read_only(self) -> None: + with patch.dict(os.environ, {}, clear=True): + config = VmMcpConfig.from_env() + self.assertEqual(config.profile, "read-only") + self.assertFalse(config.shell_enabled) + self.assertFalse(config.workspace_write_enabled) + self.assertFalse(config.personal_console_enabled) + + def test_legacy_shell_flag_maps_to_workspace(self) -> None: + with patch.dict(os.environ, {"VM_MCP_SHELL_ENABLED": "1"}, clear=True): + config = VmMcpConfig.from_env() + self.assertEqual(config.profile, "workspace") + + def test_parent_escape_rejected(self) -> None: + with self.assertRaises(PermissionError): + resolve_under_root(self.root, "../outside", must_exist=False) + + def test_symlink_escape_rejected(self) -> None: + outside = self.root.parent / f"{self.root.name}-outside" + outside.mkdir() + try: + (outside / "secret.txt").write_text("secret", encoding="utf-8") + (self.root / "escape").symlink_to(outside, target_is_directory=True) + with self.assertRaises(PermissionError): + read_text(self.config(), "escape/secret.txt") + finally: + (outside / "secret.txt").unlink(missing_ok=True) + outside.rmdir() + + def test_listing_does_not_follow_symlink_metadata(self) -> None: + outside = self.root.parent / f"{self.root.name}-outside" + outside.mkdir() + try: + target = outside / "huge.txt" + target.write_text("x" * 1000, encoding="utf-8") + link = self.root / "link" + link.symlink_to(target) + result = list_directory(self.config(), ".") + entry = result["entries"][0] + self.assertEqual(entry["kind"], "symlink") + self.assertEqual(entry["size"], link.lstat().st_size) + self.assertNotEqual(entry["size"], target.stat().st_size) + finally: + target.unlink(missing_ok=True) + outside.rmdir() + + def test_read_text_is_bounded(self) -> None: + (self.root / "large.txt").write_text("abcdefghij", encoding="utf-8") + result = read_text(self.config(), "large.txt", max_bytes=5) + self.assertEqual(result["text"], "abcde") + self.assertTrue(result["truncated"]) + + def test_directory_listing_is_bounded(self) -> None: + for index in range(4): + (self.root / f"{index}.txt").write_text("x", encoding="utf-8") + result = list_directory(self.config(), ".", max_entries=2) + self.assertEqual(len(result["entries"]), 2) + self.assertTrue(result["truncated"]) + + def test_write_requires_mutating_profile(self) -> None: + with self.assertRaises(PermissionError): + write_text(self.config(), "x.txt", "x") + + def test_write_and_move_remain_under_root(self) -> None: + config = self.config(profile="workspace") + write_text(config, "a/x.txt", "hello", create_parents=True) + result = move_path(config, "a/x.txt", "moved.txt") + self.assertEqual(Path(result["destination"]).read_text(encoding="utf-8"), "hello") + with self.assertRaises(PermissionError): + write_text(config, "../outside.txt", "no") + + def test_shell_is_disabled_in_read_only(self) -> None: + with self.assertRaises(PermissionError): + run_shell(self.config(), "true") + + def test_shell_cwd_escape_rejected(self) -> None: + with self.assertRaises(PermissionError): + run_shell(self.config(profile="workspace"), "true", cwd="..") + + def test_shell_output_is_bounded_while_draining(self) -> None: + result = run_shell( + self.config(profile="workspace", output=5), + "python3 -c 'print(\"x\" * 1000000, end=\"\")'", + ) + self.assertEqual(result["exit_code"], 0) + self.assertEqual(result["stdout"], "xxxxx") + self.assertTrue(result["stdout_truncated"]) + + def test_shell_timeout_is_enforced(self) -> None: + started = time.monotonic() + result = run_shell( + self.config(profile="workspace"), "sleep 2", timeout_seconds=0.1 + ) + self.assertTrue(result["timed_out"]) + self.assertIsNone(result["exit_code"]) + self.assertLess(time.monotonic() - started, 1.0) + + def test_shell_does_not_inherit_unrelated_environment(self) -> None: + with patch.dict(os.environ, {"VM_MCP_TEST_SECRET_SENTINEL": "must-not-leak"}): + result = run_shell( + self.config(profile="workspace"), + "printf '%s' \"${VM_MCP_TEST_SECRET_SENTINEL-unset}\"", + ) + self.assertEqual(result["stdout"], "unset") + + def test_broker_exec_requires_personal_console(self) -> None: + with self.assertRaises(PermissionError): + broker_exec( + self.config(profile="workspace"), mode="admin", user=None, + command="true", cwd="/", timeout_seconds=1, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4705456 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: EPAC CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + + - name: Resolve pinned UCNS + env: + UCNS_SHA: 828c0b8bbcfc267efb5701da714191c1f73a81ff + run: | + set -euo pipefail + git clone --quiet https://github.com/The-Interdependency/ucns.git _deps/ucns + git -C _deps/ucns checkout --quiet "$UCNS_SHA" + test "$(git -C _deps/ucns rev-parse HEAD)" = "$UCNS_SHA" + + - name: Repository regression suite + run: | + set -euo pipefail + PYTHONPATH=".:_deps/ucns/src" python -m unittest discover -s tests -q + + - name: Subatomic executable witnesses + run: | + set -euo pipefail + PYTHONPATH=".:subatomic:_deps/ucns/src" python - <<'PY' + from pathlib import Path + import importlib.util + import inspect + + total = 0 + for path in sorted(Path("subatomic").glob("test_*.py")): + spec = importlib.util.spec_from_file_location(f"epac_subatomic_{path.stem}", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + for name, fn in sorted(vars(module).items()): + if not name.startswith("test_") or not callable(fn): + continue + if inspect.signature(fn).parameters: + raise RuntimeError(f"unsupported fixture-bearing witness: {path}:{name}") + fn() + total += 1 + if total == 0: + raise RuntimeError("no subatomic witnesses executed") + print(f"{total} subatomic witnesses passed") + PY + + - name: Preserve molecular falsification standing + run: | + set -euo pipefail + PYTHONPATH=".:_deps/ucns/src" python - <<'PY' + from epac_comparison import compare_after_construction + standings = compare_after_construction()["standings"] + assert standings + assert set(standings.values()) == {"FALSIFIED"}, standings + print(standings) + PY + + - name: Verify work-graph identity + run: | + python - <<'PY' + import hashlib + import json + from pathlib import Path + + doc = json.loads(Path("docs/work-graph.json").read_text(encoding="utf-8")) + payload = {"repositories": doc["repositories"], "boundaries": doc["boundaries"]} + actual = hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + ).hexdigest() + assert actual == doc["work_graph_sha256"], (actual, doc["work_graph_sha256"]) + print(actual) + PY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e08eee8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +_deps/ +dist/ +build/ +*.egg-info/ +.skill-lib/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0254b66 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# EPAC agent instructions + +EPAC is an extracted research project originating at `The-Interdependency/stack:research/epac`. + +Before work: + +1. read `.agents/skills/README.md` and every applicable `.agents/skills/*/SKILL.md`; +2. read `docs/PROVENANCE.md`, `docs/work-graph.json`, and the relevant preregistration/result document; +3. preserve epistemic status: implementation success does not transfer physics, chemistry, theorem, proof, measurement, or empirical validity; +4. preserve falsified results as evidence rather than deleting or rebranding them; +5. use UCNS only through its own declared public surfaces; do not invent Public Gonol position operations; +6. treat METAPAT affixiation/harmonic language as semantic application vocabulary, not imported physics; +7. include runnable usage guidance in code and research artifacts; +8. unknown authority, mapping, or evidence remains `hmmm`. + +Current graduation boundary: this repository is being physically extracted and independently verified. Stable release/reconsumption, clean packaging, and license/distribution gates remain separate evidence requirements. diff --git a/LICENSE_STATUS.md b/LICENSE_STATUS.md new file mode 100644 index 0000000..5afd813 --- /dev/null +++ b/LICENSE_STATUS.md @@ -0,0 +1,9 @@ +# License status + +`hmmm` — no repository license has yet been selected for the independent EPAC repository. + +This is a blocking graduation/release gate. Public visibility does not grant downstream reuse rights. Do not publish a stable package or claim redistribution permission until the owner selects and records the license. + +## Usage guidance + +Until the license is resolved, treat this repository as publicly inspectable source with redistribution rights unresolved. Keep packaging/publication gates closed. diff --git a/README.md b/README.md index c0cbd63..cdf8d99 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # EPAC -EPAC is The Interdependency's independent implementation and public-contract home for the elementary/particle-scale energy-and-arity coupling research program that originated in `The-Interdependency/stack`. +EPAC is the independent repository for The Interdependency's elementary/particle-scale energy-and-arity coupling research program that originated in `The-Interdependency/stack`. `EPAC` is the stable project handle. No fixed lexical expansion is required for the handle; historical expansions are provenance, not identity. ## Standing -- Repository authority: EPAC implementation and public contract are being extracted here from the stack incubator. +- Physical repository state: extracted from the stack incubator and independently verified. +- Authority transition: incomplete until the release/reconsumption graduation gates are satisfied. - Research status: provisional / cross-domain hypothesis unless a narrower artifact says otherwise. - Empirical status: no transfer. Repository independence does not make a physics or chemistry claim true. -- Molecular-shape prediction: **FALSIFIED** for the preregistered comparison currently carried from the incubator; that negative result is preserved as evidence. +- Molecular-shape prediction: **FALSIFIED** for the preregistered comparison carried from the incubator; that negative result is preserved as evidence. - UCNS Public Gonol position operations beyond carrier identity: `hmmm`. - Standing-wave / field descriptions: live modeling direction, not established external physics merely by appearing here. @@ -23,18 +24,29 @@ Extraction source: The extraction preserves the stack research artifacts and their epistemic status. Stack-local statements such as “no independent EPAC repository exists” are migration scaffolding and do not become EPAC doctrine. -## Intended structure +## Structure -- `epac_*.py` — current executable constructors and comparison surfaces migrated from the incubator. -- `subatomic/` — subatomic construction candidates, receipts, and tests. +- `epac_*.py` — executable constructors and comparison surfaces migrated from the incubator. +- `subatomic/` — subatomic construction candidates, receipts, and executable witnesses. - `tests/` — repository-level regression and falsification tests. - `data/` — bounded input/comparison data used by the current experiments. -- `docs/` — scope, arity, preregistration, provenance, and graduation records. +- `docs/` — scope, arity, preregistration, provenance, work graph, and graduation records. - `.agents/skills/` — repo-local copy of canonical organization skills, sourced from `The-Interdependency/skill-lib`. +## Verification + +The extraction gate executes: + +- 41 repository regression tests; +- 26 subatomic executable witnesses; +- the preregistered molecular comparison, requiring all four current standings to remain `FALSIFIED`; +- deterministic work-graph digest verification. + +CI independently resolves the pinned UCNS source before running the same gates. + ## Usage guidance -Until packaging is independently qualified, run the migrated research suite from a checkout with the required UCNS source available on `PYTHONPATH`. The repository CI defines the authoritative current invocation once populated. +Until packaging is independently qualified, run the research suite from a checkout with the pinned UCNS source available on `PYTHONPATH`; `.github/workflows/ci.yml` is the executable reference invocation. Do not treat successful execution as empirical validation. Constructors establish reproducible declared structures; comparison tests determine the standing of the claims they actually test. @@ -42,5 +54,6 @@ Do not treat successful execution as empirical validation. Constructors establis - distribution surface and first immutable release artifact - license/distribution-rights selection for this independent repository -- exact replacement for the incubator's local UCNS path before clean-install qualification +- clean package/install dependency contract for UCNS +- downstream forge reconsumption and authority-transition receipt - whether standing-wave language earns a stronger domain claim after explicit external-physics comparison diff --git a/data/periodic_table_z1_18.json b/data/periodic_table_z1_18.json new file mode 100644 index 0000000..dc87260 --- /dev/null +++ b/data/periodic_table_z1_18.json @@ -0,0 +1,31 @@ +{ + "schema": "epac.periodic-table-atomic-structure", + "version": "v1", + "scope": "Z=1-18 ground-state atomic structure for element-gonol construction", + "source": "established main-group ground-state electron configurations and typical hydride valences; not a molecular-shape table", + "excludes": [ + "bond-angle", + "shape-class", + "hybridization-as-shape" + ], + "elements": [ + {"Z": 1, "symbol": "H", "period": 1, "group": 1, "electron_configuration": "1s1", "valence_electrons": 1, "typical_valence": 1}, + {"Z": 2, "symbol": "He", "period": 1, "group": 18, "electron_configuration": "1s2", "valence_electrons": 2, "typical_valence": 0}, + {"Z": 3, "symbol": "Li", "period": 2, "group": 1, "electron_configuration": "1s2.2s1", "valence_electrons": 1, "typical_valence": 1}, + {"Z": 4, "symbol": "Be", "period": 2, "group": 2, "electron_configuration": "1s2.2s2", "valence_electrons": 2, "typical_valence": 2}, + {"Z": 5, "symbol": "B", "period": 2, "group": 13, "electron_configuration": "1s2.2s2.2p1", "valence_electrons": 3, "typical_valence": 3}, + {"Z": 6, "symbol": "C", "period": 2, "group": 14, "electron_configuration": "1s2.2s2.2p2", "valence_electrons": 4, "typical_valence": 4}, + {"Z": 7, "symbol": "N", "period": 2, "group": 15, "electron_configuration": "1s2.2s2.2p3", "valence_electrons": 5, "typical_valence": 3}, + {"Z": 8, "symbol": "O", "period": 2, "group": 16, "electron_configuration": "1s2.2s2.2p4", "valence_electrons": 6, "typical_valence": 2}, + {"Z": 9, "symbol": "F", "period": 2, "group": 17, "electron_configuration": "1s2.2s2.2p5", "valence_electrons": 7, "typical_valence": 1}, + {"Z": 10, "symbol": "Ne", "period": 2, "group": 18, "electron_configuration": "1s2.2s2.2p6", "valence_electrons": 8, "typical_valence": 0}, + {"Z": 11, "symbol": "Na", "period": 3, "group": 1, "electron_configuration": "[Ne].3s1", "valence_electrons": 1, "typical_valence": 1}, + {"Z": 12, "symbol": "Mg", "period": 3, "group": 2, "electron_configuration": "[Ne].3s2", "valence_electrons": 2, "typical_valence": 2}, + {"Z": 13, "symbol": "Al", "period": 3, "group": 13, "electron_configuration": "[Ne].3s2.3p1", "valence_electrons": 3, "typical_valence": 3}, + {"Z": 14, "symbol": "Si", "period": 3, "group": 14, "electron_configuration": "[Ne].3s2.3p2", "valence_electrons": 4, "typical_valence": 4}, + {"Z": 15, "symbol": "P", "period": 3, "group": 15, "electron_configuration": "[Ne].3s2.3p3", "valence_electrons": 5, "typical_valence": 3}, + {"Z": 16, "symbol": "S", "period": 3, "group": 16, "electron_configuration": "[Ne].3s2.3p4", "valence_electrons": 6, "typical_valence": 2}, + {"Z": 17, "symbol": "Cl", "period": 3, "group": 17, "electron_configuration": "[Ne].3s2.3p5", "valence_electrons": 7, "typical_valence": 1}, + {"Z": 18, "symbol": "Ar", "period": 3, "group": 18, "electron_configuration": "[Ne].3s2.3p6", "valence_electrons": 8, "typical_valence": 0} + ] +} diff --git a/data/sealed_known_molecular_geometry.json b/data/sealed_known_molecular_geometry.json new file mode 100644 index 0000000..f325d17 --- /dev/null +++ b/data/sealed_known_molecular_geometry.json @@ -0,0 +1,12 @@ +{ + "schema": "epac.sealed-known-molecular-geometry", + "version": "v1", + "opened_only_after_construction": true, + "molecules": { + "H2": {"atom_count": 2, "known_shape": "linear"}, + "H2O": {"atom_count": 3, "known_shape": "bent"}, + "NH3": {"atom_count": 4, "known_shape": "trigonal-pyramidal"}, + "CH4": {"atom_count": 5, "known_shape": "tetrahedral"}, + "CO2": {"atom_count": 3, "known_shape": "linear"} + } +} diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md new file mode 100644 index 0000000..f1f1229 --- /dev/null +++ b/docs/PROVENANCE.md @@ -0,0 +1,21 @@ +# EPAC extraction provenance + +EPAC originated as stack-local research and was extracted into this repository on 2026-09-05. + +## Exact source + +- forge: `The-Interdependency/stack` +- source commit: `ef51f2e8f32ccfd5394525dad72475a61a505bc1` +- source path: `research/epac/` +- source EPAC tree: `4d1fa3cfc1c115bfc94a9a7f90a567b6d2ebbbc1` +- UCNS dependency pinned by that stack manifest: `The-Interdependency/ucns@828c0b8bbcfc267efb5701da714191c1f73a81ff` +- skill doctrine installed at extraction: `The-Interdependency/skill-lib@8dfb974ea0cee72e4412f9d2c8b597a8930a4d57` +- METAPAT semantic consultation: `The-Interdependency/metapat@d6699e21b11c8f8394998efc34a468e2d6efc8b0` + +Extraction moves code and public-contract work into an independent repository. It does not transfer semantic, mathematical, proof, measurement, chemistry, physics, or empirical authority from upstream projects or external domains. + +Historical receipts and preregistrations are retained. Stack-specific placeholder statements are migration history, not current EPAC status. + +## Usage guidance + +Reproduce the extraction baseline by checking out the exact stack commit above and comparing `research/epac/` with the migrated source files, allowing only documented migration repairs, repository records, skill installation, and CI wiring. diff --git a/docs/arity.md b/docs/arity.md new file mode 100644 index 0000000..4f96a5a --- /dev/null +++ b/docs/arity.md @@ -0,0 +1,65 @@ +# Dimensional arity + +Status: **CROSS-DOMAIN-HYPOTHESIS / provisional**. Not org canon. + +Dimension tells where. Arity tells what intersects at once. Degree tells how a +dimension is incident on declared couplings. + +```text +(z, x) ≠ (x, z) +(x, z) and (y, z) ↛ (x, y, z) without an explicit proof + +every physical instance of x has its own (z, x_i) +every physical instance of y has its own (z, y_j) +``` + +A second atom occurrence is a second instance. `(z, x_0)` does not cover `x_1`. +`(x_i, z)` does not satisfy `(z, x_i)`. Letters and chemical-symbol +abbreviations are nomenclature, not physics, and are not these instances. + +Precursors: each proton and each neutron is a closed gonol. The nucleus is +their affixiation. Neutrons couple to protons as `(proton_j, neutron_i)` with +slot charges `(+1, 0)`. Proton-proton and neutron-neutron are not inferred. +Hydrogen-1 is one proton and no neutrons. + +At atomic scale the hub is that closed nucleus and every electron instance has +its own `(nucleus, electron_i)` with slot charges `(Z, -1)`. Molecular scale +does not reopen nucleons or electrons: water remains `(O#2, H#0)` and +`(O#2, H#1)`. + +Degree is required. For ambient `{x,y,z}` with couplings `(z,x)` and `(z,y)`: + +```text +deg(z) = 2 at slot 0 +deg(x) = 1 at slot 1 +deg(y) = 1 at slot 1 +``` + +That incidence structure is the geometry of two binary couplings sharing `z`. +It is not a ternary coupling and not `(x,y)`. + +Charge state rides on each coupling from the math already present: per-slot +dimension charges (nuclear `Z` when the axis is an atom) and Möbius `ε` at +`t=0`. `(z,x)` with charges `(q_z, q_x, ε)` is not `(x,z)` with +`(q_x, q_z, ε)`. The three-dimensional structure **is** that combination — +oriented couplings, each arity's charge state, and degree. Two charged +arity-2 couplings on a degree-2 hub already occupy three participating axes. +It still does not declare `(x,y,z)`. + +Representing that 3 takes 4 dimensions: a quaternion +`(ε, q_z, q_x, q_y)`. The extra coordinate is the scalar, Möbius `ε`, already +in the math. It is not a fourth ambient axis, not Minkowski time, and not a +Hamilton-product proof of `(x,y,z)`. `ij = k` does not install a coupling. +Helium's nucleus plus two electrons is one local 3 in 4-representation; +the letters `H` and `e` are not those axes. A single binary (H₂, hydrogen +atom) is not a 3 and has no quaternion. + +Construction is `epac.public_gonol` on the UCNS Public Gonol carrier, not +`edcm.gonol`. + +Overlap of members is not a proof. Forbidden inference rules include +`overlap-closure`, `permutation-identity`, and `ambient-power-set`. + +See `epac_dimensional_arity.py`. After construction, `epac_comparison.py` reads +that 3-structure against sealed known chemistry. Sealed shape names stay out +of construction. diff --git a/docs/domain-claims.md b/docs/domain-claims.md new file mode 100644 index 0000000..4d08584 --- /dev/null +++ b/docs/domain-claims.md @@ -0,0 +1,30 @@ +# EPAC domain claim + +## Project handle + +- surface form: `EPAC` +- term id: `the-interdependency.epac` +- claiming domain: The Interdependency / EPAC project +- claimed sense: stable project identifier for this repository and its bounded research program +- scope: repository identity, implementation/public-contract work, research records +- claim type: native +- status: ratified as the repository handle +- handle identity: despecified +- canonical expansion: none + +Historical or audience-specific expansions may be recorded as provenance but do not redefine the handle. + +## Research boundary + +EPAC may construct and test relational representations involving atomic, subatomic, molecular, energy, arity, recurrence, and coupling surfaces. Those constructions do not acquire external physics or chemistry truth by repository placement. + +“Standing wave,” “field,” “particle,” “harmonic,” and related terms remain domain-qualified. In particular, METAPAT harmonic language does not itself mean physical vibration or frequency. + +## Usage guidance + +Use `EPAC` as the stable project name. When a longer explanation is needed, describe the actual research scope rather than inventing a fixed acronym expansion. Stronger ontology claims require an explicit domain claim and evidence. + +## hmmm + +- whether standing-wave language will earn a narrower ratified EPAC ontology claim after explicit external-physics comparison +- whether any fixed lexical expansion of EPAC is useful enough to ratify; none is currently required diff --git a/docs/graduation.json b/docs/graduation.json new file mode 100644 index 0000000..bdb9034 --- /dev/null +++ b/docs/graduation.json @@ -0,0 +1,66 @@ +{ + "schema": "the-interdependency.project-graduation-record", + "version": "1.0.0", + "candidate": "EPAC", + "lifecycle_state": "stabilizing", + "physical_repository_state": "extracted", + "mode": "execution", + "authorization": { + "external_mutation": "authorized", + "authority_transfer": "not-yet-complete" + }, + "forge": { + "repository": "The-Interdependency/stack", + "source_commit": "ef51f2e8f32ccfd5394525dad72475a61a505bc1", + "source_path": "research/epac" + }, + "future_authority": { + "repository": "The-Interdependency/epac" + }, + "distribution": { + "kind": "hmmm", + "artifact": "hmmm", + "version": "hmmm", + "candidate_immutable_identity": "hmmm", + "published_immutable_identity": "hmmm", + "published_matches_verified_candidate": "hmmm" + }, + "upstream": [ + { + "repository": "The-Interdependency/ucns", + "commit": "828c0b8bbcfc267efb5701da714191c1f73a81ff", + "relation": "mathematical representation and Public Gonol dependency", + "authority_transfer": false + }, + { + "repository": "The-Interdependency/metapat", + "commit": "d6699e21b11c8f8394998efc34a468e2d6efc8b0", + "relation": "semantic authority for affixiation/harmonic application language", + "authority_transfer": false + }, + { + "repository": "The-Interdependency/skill-lib", + "commit": "8dfb974ea0cee72e4412f9d2c8b597a8930a4d57", + "relation": "organization build and evidence doctrine", + "authority_transfer": false + } + ], + "gates": { + "public_api": "hmmm", + "independent_tests": "pass", + "clean_build_install": "fail", + "license_distribution_rights": "fail", + "release_ownership_authority": "pass", + "provenance_preserved": "pass", + "exact_candidate_forge_verification": "hmmm", + "stable_release": "hmmm", + "downstream_reconsumption": "hmmm" + }, + "transition_receipt": "hmmm", + "hmmm": [ + "distribution surface not selected", + "repository license not selected", + "clean package/install contract not yet established", + "stack has not reconsumed a released immutable EPAC artifact" + ] +} diff --git a/docs/preregistration-molecular-geometry-from-element-gonols.md b/docs/preregistration-molecular-geometry-from-element-gonols.md new file mode 100644 index 0000000..ea8ccf6 --- /dev/null +++ b/docs/preregistration-molecular-geometry-from-element-gonols.md @@ -0,0 +1,111 @@ +> Migration note: this preregistration was frozen in the stack incubator before EPAC extraction. Its original authority/context statements are retained as historical evidence. + +# Preregistration: molecular geometry from element gonols + +- Status: **CROSS-DOMAIN-HYPOTHESIS / provisional research candidate** +- Owner of record: `The-Interdependency/stack` → `research/epac/` +- Constructor: `epac.public_gonol` on the pinned UCNS Public Gonol carrier. + Not `edcm.gonol`. +- Comparison policy is frozen **before** construction. Known molecular-shape + labels are sealed and may be opened only by the comparison step. + +## Domain claims + +| Surface form | Term id | Claiming domain | Claimed sense | Excluded | +|---|---|---|---|---| +| element gonol | `epac.periodic.element_gonol` | epac candidate | closed gonol of one periodic-table element carrying Z, ground-state electron configuration, and typical main-group valence | molecular shape, bond angle, hybridization | +| valence arity | `epac.periodic.typical_valence` | epac candidate | main-group hydride valence from the periodic table (group-derived) | VSEPR domain count as a shape rule | +| affixiation | `metapat.affixiation_harmonics.affixiation` | METAPAT | identity-preserving higher-order relation | UCNS topology selection | +| UCNS coupling | `ucns.native-mobius-root-loop` | UCNS | established 360° frame flip / 720° restore | invented 3-space arrangement | +| molecular gonol | `epac.molecular.affixiated_whole` | epac candidate | closed recursive gonol of element-gonol participants | known chemistry shape names | +| predicted geometry | `epac.molecular.construction_invariants` | epac candidate | atom count, center valence, slot occupancy, Möbius frame sequence | sealed comparison labels | + +Collision check: physics/chemistry own empirical molecular shapes. This candidate +does not claim those senses during construction. Resolution: **clear** (separate +scopes) until comparison. + +## Frozen pipeline + +```text +proton gonols (charge +1) and neutron gonols (charge 0) + -> nucleus = affixiation of those nucleons; (proton_j, neutron_i) + -> every electron instance: (nucleus, electron_i) with charges (Z, -1) + -> close that atomic 3-structure inside the element gonol + -> unpaired-valence attachment sites + -> declared oriented (center, ligand_i) couplings with per-slot Z and Möbius ε + -> molecular three-dimensional structure = those atom-instance couplings + charge states + degree + -> each local 3 represented in 4 quaternion components (scalar ε plus the three axis charges) + -> molecular Public Gonol (closed atoms remain atomic participants) + -> construction invariants + -> (only then) compare to sealed known chemistry +``` + +## Inputs allowed in construction + +- atomic number Z, default isotope A +- each proton instance and each neutron instance of that isotope (counts must match Z and A−Z) +- every electron: n, l, m_l, m_s, shell, subshell +- hydrogenic angular identity Y_l^m, radial node count n-l-1 +- Slater atomic Z_eff and hydrogenic Rydberg energy -Z_eff²/n² +- unpaired valence electrons from Hund filling +- atomic s→p promotion in the same n when more unpaired sites are required +- caller-supplied stoichiometric formula (element counts only) + +## Inputs forbidden in construction + +- bond angles +- VSEPR shape names +- hybridization labels used as shape +- any sealed comparison filename contents + +## UCNS coupling candidate + +Only the implemented Möbius root loop is applied: + +```text +(t, ε) ~ (t + n, (-1)^n ε) +t = 0, 1, 2 +``` + +Public Gonol positions, when supplied, are identity coordinates. Position +operations remain `hmmm`. No spherical equal-spacing rule is added. + +## Molecules in this run + +`H2`, `H2O`, `NH3`, `CH4`, `CO2` + +## Comparison policy (frozen) + +Opened only after molecular gonols exist: + +1. Construction source and receipts must not contain the sealed shape labels. +2. Record construction invariants per formula. +3. Open `data/sealed_known_molecular_geometry.json`. +4. Ask whether the constructed three-dimensional structure distinguishes + formulas that chemistry distinguishes by shape. +5. Compare four signatures: charged oriented couplings plus degree (the + 3-structure already in the math); arity/degree topology without charge; + UCNS Möbius coupling; atomic unpaired (l, m_l) plus ligand shell content; + against a matched-information control of formula symbols only. + Do not import sealed shape names into construction. + +## Terminal standings for the hmmm question + +The question: does gonol geometry predict molecular shape, or merely reproduce +information already present in the inputs? + +- `SURVIVED` as prediction — only if the charged 3-structure is invariant + inside each sealed shape class, distinguishes different sealed classes, and + is not the matched-information control. +- `FALSIFIED` as prediction — if the 3-structure splits a sealed class, or + collapses classes chemistry splits, or if distinguishing power is already + present in valence+stoichiometry. +- `UNRESOLVED` — if the readout is incomplete. +- None of these standings select canon. + +## hmmm + +- Public Gonol function operations beyond carrier identity +- whether a later UCNS 3-space coupling exists that is not VSEPR imported +- expansion of the element table beyond Z=1–18 +- epac still has no canonical source repository diff --git a/docs/research-status.md b/docs/research-status.md new file mode 100644 index 0000000..c02586e --- /dev/null +++ b/docs/research-status.md @@ -0,0 +1,27 @@ +# Research status + +## Preserved negative result + +The preregistered molecular-shape prediction is **FALSIFIED** for all four tested readouts: + +- charged oriented 3-structure; +- topology-only 3-structure; +- UCNS Möbius signature; +- atomic-shell signature. + +This means the current construction is not evidence that EPAC predicts empirical molecular shape. The falsification remains part of the repository's evidence base. + +## Implemented candidate surfaces + +The extraction carries atomic, periodic, molecular, dimensional-arity, Public Gonol, subatomic, comparison, receipt, and regression-test surfaces from the pinned stack source. + +## Usage guidance + +Treat constructor success as reproducibility of a declared structure. Treat a comparison standing only as evidence for the exact preregistered claim it scores. Do not infer physics or chemistry validation from either. + +## hmmm + +- exact UCNS Public Gonol position operations beyond carrier identity +- a non-imported mapping from EPAC relational structure to empirical molecular angles +- standing-wave/field ontology beyond provisional modeling language +- release packaging and downstream artifact consumption diff --git a/docs/work-graph.json b/docs/work-graph.json new file mode 100644 index 0000000..db79097 --- /dev/null +++ b/docs/work-graph.json @@ -0,0 +1,42 @@ +{ + "schema": "the-interdependency.stack-manifest", + "version": "1.0.0", + "work_graph_sha256": "19f468b9773f71fc78ad58e15ebf3b36ed7fc001ade57846ba0bba9dc84e1631", + "repositories": [ + { + "repository": "The-Interdependency/stack", + "commit": "ef51f2e8f32ccfd5394525dad72475a61a505bc1", + "authority": "forge/provenance authority for the extracted candidate state", + "relation": "extraction source" + }, + { + "repository": "The-Interdependency/skill-lib", + "commit": "8dfb974ea0cee72e4412f9d2c8b597a8930a4d57", + "authority": "organization-wide build and evidence doctrine", + "relation": "repo-local skill snapshot source" + }, + { + "repository": "The-Interdependency/metapat", + "commit": "d6699e21b11c8f8394998efc34a468e2d6efc8b0", + "authority": "semantic authority for METAPAT application terms", + "relation": "semantic source consumed by EPAC" + }, + { + "repository": "The-Interdependency/ucns", + "commit": "828c0b8bbcfc267efb5701da714191c1f73a81ff", + "authority": "geometry and mathematical representation", + "relation": "Public Gonol and Möbius dependency consumed by EPAC" + } + ], + "boundaries": { + "authority_transfer": false, + "proof_status_transfer": false, + "measurement_status_transfer": false, + "semantic_mapping": "external-provenance", + "agent_scope": "cross-repository-work-graph", + "hmmm": [ + "EPAC stable release and stack reconsumption are not complete", + "license/distribution rights gate remains unresolved" + ] + } +} diff --git a/epac_atomic.py b/epac_atomic.py new file mode 100644 index 0000000..8bcbd06 --- /dev/null +++ b/epac_atomic.py @@ -0,0 +1,285 @@ +"""Atomic and subatomic structure used by element gonols. + +Nothing here is molecular. Electrons are filled by Aufbau, Pauli, and Hund. +Angular identities are hydrogenic spherical harmonics labeled by (n, l, m_l). +Screening is Slater's atomic Z_eff. Energies are hydrogenic Rydberg units +with that Z_eff. Nucleus instances are default isotopes, identity only. + +Do not import the sealed molecular comparison file from this module. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterator + + +SUBSHELL_ORDER: tuple[tuple[int, int], ...] = ( + (1, 0), + (2, 0), + (2, 1), + (3, 0), + (3, 1), +) + +ISOTOPE_DEFAULTS: dict[int, int] = { + 1: 1, + 2: 4, + 3: 7, + 4: 9, + 5: 11, + 6: 12, + 7: 14, + 8: 16, + 9: 19, + 10: 20, + 11: 23, + 12: 24, + 13: 27, + 14: 28, + 15: 31, + 16: 32, + 17: 35, + 18: 40, +} + +SYMBOLS: tuple[str, ...] = ( + "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", + "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", +) + + +@dataclass(frozen=True, slots=True) +class ElectronState: + """One electron in an atom: quantum numbers plus atomic wave labels.""" + + index: int + n: int + l: int + m_l: int + m_s: int + shell: str + subshell: str + angular_id: str + radial_nodes: int + z_eff: str + e_rydberg: str + valence: bool + paired: bool + + +@dataclass(frozen=True, slots=True) +class AtomicRecord: + Z: int + symbol: str + period: int + group: int + A: int + proton_count: int + neutron_count: int + electrons: tuple[ElectronState, ...] + configuration: str + valence_n: int + valence_electrons: int + unpaired_valence: tuple[ElectronState, ...] + promoted_unpaired_valence: tuple[ElectronState, ...] + + +def _period_group(Z: int) -> tuple[int, int]: + if Z == 1: + return 1, 1 + if Z == 2: + return 1, 18 + if Z <= 4: + return 2, Z - 2 + if Z <= 10: + return 2, Z + 8 + if Z <= 12: + return 3, Z - 10 + return 3, Z + + +def _ml_down(l: int) -> tuple[int, ...]: + return tuple(range(l, -l - 1, -1)) + + +def _subshell_name(n: int, l: int) -> str: + return f"{n}{'spdf'[l]}" + + +def _angular_id(l: int, m_l: int) -> str: + return f"Y_l{l}_m{m_l}" + + +def _slater_zeff(Z: int, n: int, l: int, occupied: tuple[tuple[int, int], ...]) -> float: + """Slater screening for one electron in subshell (n, l).""" + + others = list(occupied) + others.remove((n, l)) + sigma = 0.0 + same_group = 0 + for on, ol in others: + if n == 1 and l == 0: + if on == 1 and ol == 0: + sigma += 0.30 + continue + if on == n and ((l in {0, 1} and ol in {0, 1}) or ol == l): + same_group += 1 + elif on == n - 1: + sigma += 0.85 + elif on <= n - 2: + sigma += 1.00 + sigma += 0.35 * same_group + return round(Z - sigma, 3) + + +def _fill_electrons(Z: int) -> tuple[ElectronState, ...]: + remaining = Z + occupied_pairs: list[tuple[int, int]] = [] + raw: list[tuple[int, int, int, int]] = [] + for n, l in SUBSHELL_ORDER: + capacity = 2 * (2 * l + 1) + take = min(remaining, capacity) + slots = [(m_l, 1) for m_l in _ml_down(l)] + [(m_l, -1) for m_l in _ml_down(l)] + for m_l, m_s in slots[:take]: + raw.append((n, l, m_l, m_s)) + occupied_pairs.append((n, l)) + remaining -= take + if remaining == 0: + break + valence_n = max(n for n, _l, _ml, _ms in raw) + occupied = tuple(occupied_pairs) + electrons: list[ElectronState] = [] + occupancy: dict[tuple[int, int, int], int] = {} + for n, l, m_l, m_s in raw: + occupancy[(n, l, m_l)] = occupancy.get((n, l, m_l), 0) + 1 + seen: dict[tuple[int, int, int], int] = {} + for index, (n, l, m_l, m_s) in enumerate(raw): + seen[(n, l, m_l)] = seen.get((n, l, m_l), 0) + 1 + z_eff = _slater_zeff(Z, n, l, occupied) + energy = round(-(z_eff ** 2) / (n ** 2), 6) + electrons.append( + ElectronState( + index=index, + n=n, + l=l, + m_l=m_l, + m_s=m_s, + shell=f"n{n}", + subshell=_subshell_name(n, l), + angular_id=_angular_id(l, m_l), + radial_nodes=n - l - 1, + z_eff=str(z_eff), + e_rydberg=str(energy), + valence=(n == valence_n), + paired=occupancy[(n, l, m_l)] == 2, + ) + ) + return tuple(electrons) + + +def _configuration(electrons: tuple[ElectronState, ...]) -> str: + counts: dict[str, int] = {} + order: list[str] = [] + for electron in electrons: + name = electron.subshell + if name not in counts: + order.append(name) + counts[name] = 0 + counts[name] += 1 + return ".".join(f"{name}{counts[name]}" for name in order) + + +def _unpaired_valence(electrons: tuple[ElectronState, ...]) -> tuple[ElectronState, ...]: + return tuple(e for e in electrons if e.valence and not e.paired and e.m_s == 1) + + +def _promoted_unpaired(electrons: tuple[ElectronState, ...]) -> tuple[ElectronState, ...]: + """Atomic valence promotion: move valence s pair into empty valence p to unpair. + + This is an atomic excited configuration (same n). It is not a molecular hybrid. + """ + + unpaired = list(_unpaired_valence(electrons)) + valence = [e for e in electrons if e.valence] + valence_n = valence[0].n if valence else 1 + if valence_n < 2: + return tuple(unpaired) + p_occupied_m = {e.m_l for e in valence if e.l == 1} + empty_p_m = [m for m in _ml_down(1) if m not in p_occupied_m] + s_pairs_by_orbital: dict[tuple[int, int, int], list[ElectronState]] = {} + for electron in valence: + if electron.l == 0 and electron.paired: + s_pairs_by_orbital.setdefault((electron.n, electron.l, electron.m_l), []).append(electron) + s_pair = next((pair for pair in s_pairs_by_orbital.values() if len(pair) == 2), None) + if s_pair is None or not empty_p_m: + return tuple(unpaired) + # Promote the spin-down valence s electron into the first empty valence p + # and flip it to spin-up. The spin-up s electron stays behind, so every + # promoted unpaired electron carries m_s = +1, matching the ground-state + # unpaired convention used by _unpaired_valence. + promoted_from_s = next((item for item in s_pair if item.m_s == -1), s_pair[0]) + remaining_s = next(item for item in s_pair if item.index != promoted_from_s.index) + new_p = ElectronState( + index=promoted_from_s.index, + n=valence_n, + l=1, + m_l=empty_p_m[0], + m_s=1, + shell=f"n{valence_n}", + subshell=_subshell_name(valence_n, 1), + angular_id=_angular_id(1, empty_p_m[0]), + radial_nodes=valence_n - 2, + z_eff=promoted_from_s.z_eff, + e_rydberg=promoted_from_s.e_rydberg, + valence=True, + paired=False, + ) + unpaired_s = ElectronState( + index=remaining_s.index, + n=remaining_s.n, + l=0, + m_l=remaining_s.m_l, + m_s=remaining_s.m_s, + shell=remaining_s.shell, + subshell=remaining_s.subshell, + angular_id=remaining_s.angular_id, + radial_nodes=remaining_s.radial_nodes, + z_eff=remaining_s.z_eff, + e_rydberg=remaining_s.e_rydberg, + valence=True, + paired=False, + ) + promoted = [unpaired_s, new_p, *[e for e in unpaired if e.l != 0]] + # Canonical subshell ordering: s before p, p orbitals by ascending m_l. + promoted.sort(key=lambda electron: (electron.l, electron.m_l)) + return tuple(promoted) + + +def atomic_record(Z: int) -> AtomicRecord: + if not 1 <= Z <= 18: + raise ValueError("this candidate table is Z=1-18") + electrons = _fill_electrons(Z) + valence_n = max(e.n for e in electrons) + period, group = _period_group(Z) + A = ISOTOPE_DEFAULTS[Z] + return AtomicRecord( + Z=Z, + symbol=SYMBOLS[Z - 1], + period=period, + group=group, + A=A, + proton_count=Z, + neutron_count=A - Z, + electrons=electrons, + configuration=_configuration(electrons), + valence_n=valence_n, + valence_electrons=sum(1 for e in electrons if e.valence), + unpaired_valence=_unpaired_valence(electrons), + promoted_unpaired_valence=_promoted_unpaired(electrons), + ) + + +def iter_table() -> Iterator[AtomicRecord]: + for Z in range(1, 19): + yield atomic_record(Z) diff --git a/epac_comparison.py b/epac_comparison.py new file mode 100644 index 0000000..bbefe5b --- /dev/null +++ b/epac_comparison.py @@ -0,0 +1,158 @@ +"""Sealed-shape comparison after EPAC Public Gonol construction. + +The three-dimensional structure is the charged oriented couplings plus degree. +This module opens known chemistry only after those structures exist. It does +not import VSEPR names into construction. + +Usage guidance +-------------- + from epac_comparison import compare_after_construction + + record = compare_after_construction() + print(record["standings"]) +""" + +from __future__ import annotations + +import json +from collections import defaultdict +from pathlib import Path +from typing import Any, Mapping + +from epac_dimensional_arity import charged_structure_readout, topology_structure_readout +from epac_molecular import construct_declared_molecules, matched_information_control + + +EPAC_ROOT = Path(__file__).resolve().parent +SEALED_PATH = EPAC_ROOT / "data" / "sealed_known_molecular_geometry.json" +SEALED_SHAPE_LABELS = ("linear", "bent", "trigonal-pyramidal", "tetrahedral", "vsepr") +CONSTRUCTION_FILES = ( + "epac_atomic.py", + "epac_dimensional_arity.py", + "epac_molecular.py", + "epac_periodic.py", + "epac_public_gonol.py", +) + + +def construction_sources_omit_sealed_labels(root: Path = EPAC_ROOT) -> tuple[str, ...]: + hits: list[str] = [] + for name in CONSTRUCTION_FILES: + text = (root / name).read_text(encoding="utf-8").lower() + for label in SEALED_SHAPE_LABELS: + if label in text: + hits.append(f"{name}:{label}") + return tuple(hits) + + +def _partitions(values: Mapping[str, Any]) -> dict[Any, tuple[str, ...]]: + groups: dict[Any, list[str]] = defaultdict(list) + for formula, value in values.items(): + groups[value].append(formula) + return {key: tuple(sorted(formulas)) for key, formulas in groups.items()} + + +def _formula_sets(partitions: Mapping[Any, tuple[str, ...]]) -> frozenset[frozenset[str]]: + return frozenset(frozenset(group) for group in partitions.values()) + + +def _standing( + readout: Mapping[str, Any], + known_shapes: Mapping[str, str], + control: Mapping[str, Any], +) -> str: + """Preregistered shape-class prediction standing. + + SURVIVED only if the readout is invariant inside each sealed shape class, + distinguishes different sealed classes, and is not the matched-information + control. + """ + + by_shape: dict[str, set[Any]] = defaultdict(set) + for formula, shape in known_shapes.items(): + by_shape[shape].add(readout[formula]) + splits_a_class = any(len(values) > 1 for values in by_shape.values()) + collapsed_classes = False + shapes = list(by_shape) + for i, left in enumerate(shapes): + for right in shapes[i + 1 :]: + if by_shape[left] & by_shape[right]: + collapsed_classes = True + if splits_a_class or collapsed_classes: + return "FALSIFIED" + if _formula_sets(_partitions(readout)) == _formula_sets(_partitions(control)): + return "FALSIFIED" + if _formula_sets(_partitions(readout)) == _formula_sets(_partitions(known_shapes)): + return "SURVIVED" + return "UNRESOLVED" + + +def compare_after_construction(root: Path = EPAC_ROOT) -> dict[str, Any]: + """Construct first, then open the sealed shapes, then score standings.""" + + label_hits = construction_sources_omit_sealed_labels(root) + constructions = construct_declared_molecules() + charged = {} + topology = {} + mobius = {} + atomic = {} + control = {} + for formula, construction in constructions.items(): + structure = construction.receipt.structure + if structure is None: + raise ValueError(f"{formula} closed without a three-dimensional structure") + charged[formula] = charged_structure_readout(structure) + topology[formula] = topology_structure_readout(structure) + mobius[formula] = construction.invariants["ucns_coupling_signature"] + atomic[formula] = construction.invariants["atomic_coupling_signature"] + control[formula] = matched_information_control(construction.invariants) + + sealed = json.loads((root / "data" / "sealed_known_molecular_geometry.json").read_text(encoding="utf-8")) + known_shapes = {formula: sealed["molecules"][formula]["known_shape"] for formula in constructions} + + return { + "opened_after_construction": True, + "construction_omits_sealed_labels": not label_hits, + "sealed_label_hits": label_hits, + "known_shapes": known_shapes, + "readouts": { + "charged_3_structure": {formula: list(value) for formula, value in charged.items()}, + "topology_3_structure": {formula: list(value) for formula, value in topology.items()}, + }, + "partitions": { + "known_shapes": {shape: formulas for shape, formulas in _partitions(known_shapes).items()}, + "charged_3_structure": { + str(index): formulas for index, formulas in enumerate(_partitions(charged).values()) + }, + "topology_3_structure": { + str(index): formulas for index, formulas in enumerate(_partitions(topology).values()) + }, + }, + "topology_collapses_h2o_with_co2": topology["H2O"] == topology["CO2"], + "charged_distinguishes_h2o_from_co2": charged["H2O"] != charged["CO2"], + "linear_class_split_by_charged_structure": charged["H2"] != charged["CO2"], + "standings": { + "charged_3_structure_as_sealed_shape_prediction": _standing(charged, known_shapes, control), + "topology_3_structure_as_sealed_shape_prediction": _standing(topology, known_shapes, control), + "ucns_mobius_as_sealed_shape_prediction": _standing(mobius, known_shapes, control), + "atomic_shells_as_sealed_shape_prediction": _standing(atomic, known_shapes, control), + }, + "nonclaims": ( + "not selected canon", + "not an imported VSEPR construction rule", + "not a cartesian embedding", + ), + "hmmm": ( + "whether a later mapping from charged 3-structure to empirical angles exists without importing VSEPR", + "exact UCNS geometric operation of each Public Gonol function position", + ), + } + + +__all__ = [ + "CONSTRUCTION_FILES", + "SEALED_PATH", + "SEALED_SHAPE_LABELS", + "compare_after_construction", + "construction_sources_omit_sealed_labels", +] diff --git a/epac_dimensional_arity.py b/epac_dimensional_arity.py new file mode 100644 index 0000000..feaf55b --- /dev/null +++ b/epac_dimensional_arity.py @@ -0,0 +1,645 @@ +"""Declared dimensional arity, orientation, and degree. + +Dimension tells where. Arity tells what intersects at once. Degree tells how +a dimension is incident on declared couplings. + +``(z, x)`` is not ``(x, z)``. Shared members of ``(x, z)`` and ``(y, z)`` do +not yield ``(x, y, z)`` without an explicit proof. Overlap is not a proof. + +Every physical instance of ``x`` has its own declared ``(z, x_i)``. Every +physical instance of ``y`` has its own declared ``(z, y_j)``. A second +occurrence is a second instance, not a reuse of the first coupling. +``(x_i, z)`` does not satisfy ``(z, x_i)``. Letters and abbreviations are +not this domain. At atomic scale the instances are electrons and the hub is +the nucleus. At molecular scale the instances are closed atom gonols. + +The three-dimensional structure is the combination of declared oriented +couplings, their arity charge states, and degree. That span can involve three +axes through two charged binaries. It is not a ternary coupling. + +Representing that 3 takes 4 dimensions: a quaternion. The extra coordinate is +the scalar (Möbius ε already in the math). It is not a fourth ambient axis, +not Minkowski time, and not a Hamilton-product proof of ``(x, y, z)``. + +Domain claims (provisional): + +- dimension: independent coordinate axis +- arity: number of dimensions in one declared coupling +- degree: incidence of one dimension on declared couplings, including slot +- coupling: ordered declaration of participating dimensions +- charge state: per-slot charges on a coupling, with Möbius ε at t=0 +- instance: occurrence-addressed physical axis or atom; each x_i / y_j is distinct +- quaternion: 4-component representation of one local 3-structure + +Collision: edcm.gonol arity_policy counts gonol participants, not dimensional +intersections. Letters/abbreviations are nomenclature, not physics instances. +Quaternion basis names are representation labels, not letters-as-physics. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass + + +# Established UCNS Möbius frame sign at t=0: ε in (t, ε) ~ (t+n, (-1)^n ε). +MOBIUS_EPSILON_T0 = 1 +REPRESENTED_STRUCTURE_DIMENSION = 3 +QUATERNION_REPRESENTATION_DIMENSION = 4 +QUATERNION_SCALAR_AXIS = "epac.representation.quaternion.scalar" + +FORBIDDEN_INFERENCE_RULES = frozenset( + { + "ambient-power-set", + "overlap-closure", + "permutation-identity", + "shared-dimension-join", + "hamilton-product-closure", + } +) + + +class DimensionalArityError(ValueError): + """Fail-closed dimensional arity error.""" + + +@dataclass(frozen=True, slots=True) +class Dimension: + """One independent coordinate axis, with optional established charge.""" + + id: str + charge: int | None = None + + def __post_init__(self) -> None: + if not isinstance(self.id, str) or not self.id or self.id.isspace(): + raise DimensionalArityError("dimension id must be exact non-empty text") + if self.charge is not None and (isinstance(self.charge, bool) or not isinstance(self.charge, int)): + raise DimensionalArityError("dimension charge must be an int or None") + + +@dataclass(frozen=True, slots=True) +class Coupling: + """One explicitly declared ordered intersection of dimensions.""" + + dimensions: tuple[Dimension, ...] + + def __post_init__(self) -> None: + if not self.dimensions: + raise DimensionalArityError("a coupling must declare at least one dimension") + ids = [dimension.id for dimension in self.dimensions] + if len(ids) != len(set(ids)): + raise DimensionalArityError("a coupling cannot repeat a dimension") + + @property + def arity(self) -> int: + return len(self.dimensions) + + @property + def declared_ids(self) -> tuple[str, ...]: + return tuple(dimension.id for dimension in self.dimensions) + + @property + def slot_charges(self) -> tuple[int | None, ...]: + return tuple(dimension.charge for dimension in self.dimensions) + + @property + def charge_state(self) -> tuple[tuple[int | None, ...], int]: + """Per-slot charges plus Möbius ε at t=0. Ordered: (z,x) ≠ (x,z).""" + + return (self.slot_charges, MOBIUS_EPSILON_T0) + + +@dataclass(frozen=True, slots=True) +class DegreeRelation: + """How one dimension sits in declared couplings. + + degree is the number of incidences. slot_degrees counts incidences at each + ordered position. (z,x) puts z in slot 0; (x,z) puts z in slot 1. + """ + + dimension: Dimension + incidences: tuple[tuple[tuple[str, ...], int], ...] + + @property + def degree(self) -> int: + return len(self.incidences) + + @property + def slot_degrees(self) -> tuple[tuple[int, int], ...]: + counts: dict[int, int] = {} + for _declared, slot in self.incidences: + counts[slot] = counts.get(slot, 0) + 1 + return tuple(sorted(counts.items())) + + +@dataclass(frozen=True, slots=True) +class CouplingProof: + """Certificate required before a higher-arity coupling may be installed.""" + + conclusion: Coupling + premises: tuple[Coupling, ...] + rule_id: str + + def __post_init__(self) -> None: + if not isinstance(self.rule_id, str) or not self.rule_id or self.rule_id.isspace(): + raise DimensionalArityError("a coupling proof must declare a non-empty rule_id") + if self.rule_id in FORBIDDEN_INFERENCE_RULES: + raise DimensionalArityError( + f"rule {self.rule_id!r} is not a proof; overlap/permutation/ambient fill are forbidden" + ) + if not self.premises: + raise DimensionalArityError("a coupling proof must cite at least one premise coupling") + + +@dataclass(frozen=True, slots=True) +class DimensionalSpace: + """Ambient axes, declared couplings, degree relations, and optional proofs.""" + + ambient_dimensions: tuple[Dimension, ...] + couplings: tuple[Coupling, ...] + proofs: tuple[CouplingProof, ...] = () + + def __post_init__(self) -> None: + ambient_ids = [dimension.id for dimension in self.ambient_dimensions] + if len(ambient_ids) != len(set(ambient_ids)): + raise DimensionalArityError("ambient dimensions must be unique") + ambient = set(ambient_ids) + for item in self.couplings: + missing = [name for name in item.declared_ids if name not in ambient] + if missing: + raise DimensionalArityError( + f"coupling {item.declared_ids} uses undeclared dimensions {tuple(missing)}" + ) + declared = {item.declared_ids for item in self.couplings} + for proof in self.proofs: + conclusion_missing = [ + name for name in proof.conclusion.declared_ids if name not in ambient + ] + if conclusion_missing: + raise DimensionalArityError( + f"proof {proof.rule_id!r} conclusion uses undeclared dimensions {tuple(conclusion_missing)}" + ) + if proof.conclusion.declared_ids not in declared: + raise DimensionalArityError( + f"proof {proof.rule_id!r} conclusion {proof.conclusion.declared_ids} is not declared" + ) + for premise in proof.premises: + if premise.declared_ids not in declared: + raise DimensionalArityError( + f"proof {proof.rule_id!r} cites missing premise {premise.declared_ids}" + ) + + +def _require_dimension_id_sequence(value: Sequence[str], *, field: str) -> tuple[str, ...]: + if not isinstance(value, Sequence) or isinstance(value, (str, bytes)): + raise DimensionalArityError(f"{field} must be an ordered declaration sequence") + return tuple(value) + + +def dimension(id: str, charge: int | None = None) -> Dimension: + return Dimension(id, charge) + + +def coupling(dimension_ids: Sequence[str], charges: Mapping[str, int] | None = None) -> Coupling: + ids = _require_dimension_id_sequence(dimension_ids, field="coupling dimensions") + charge_map = dict(charges or {}) + return Coupling(tuple(Dimension(item, charge_map.get(item)) for item in ids)) + + +def space( + ambient_ids: Sequence[str], + coupling_declarations: Sequence[Sequence[str]] = (), + proofs: Sequence[CouplingProof] = (), + charges: Mapping[str, int] | None = None, +) -> DimensionalSpace: + ambient_ids = _require_dimension_id_sequence(ambient_ids, field="ambient dimensions") + charge_map = dict(charges or {}) + ambient = tuple(Dimension(item, charge_map.get(item)) for item in ambient_ids) + by_id = {item.id: item for item in ambient} + declared = [] + for item in coupling_declarations: + ids = _require_dimension_id_sequence(item, field="each coupling declaration") + declared.append(Coupling(tuple(by_id[name] if name in by_id else Dimension(name) for name in ids))) + return DimensionalSpace( + ambient_dimensions=ambient, + couplings=tuple(declared), + proofs=tuple(proofs), + ) + + +def degree_relations(declared: DimensionalSpace) -> tuple[DegreeRelation, ...]: + incidences: dict[str, list[tuple[tuple[str, ...], int]]] = { + item.id: [] for item in declared.ambient_dimensions + } + for item in declared.couplings: + for slot, axis in enumerate(item.dimensions): + incidences[axis.id].append((item.declared_ids, slot)) + return tuple( + DegreeRelation(dimension=axis, incidences=tuple(incidences[axis.id])) + for axis in declared.ambient_dimensions + ) + + +def observed_common_ids(left: Coupling, right: Coupling) -> frozenset[str]: + """Common dimension ids. Not a coupling and not a proof.""" + + return frozenset(left.declared_ids) & frozenset(right.declared_ids) + + +def has_declared_coupling(declared: DimensionalSpace, dimension_ids: Sequence[str]) -> bool: + target = _require_dimension_id_sequence(dimension_ids, field="coupling lookup dimensions") + return any(item.declared_ids == target for item in declared.couplings) + + +def instances_missing_oriented_hub_coupling( + declared: DimensionalSpace, + *, + hub_id: str, + instance_ids: Sequence[str], +) -> tuple[str, ...]: + """Instances that do not have a declared (hub, instance) coupling. + + (instance, hub) does not count. One (z, x) does not cover a second x. + """ + + ambient = {axis.id for axis in declared.ambient_dimensions} + if hub_id not in ambient: + raise DimensionalArityError(f"hub {hub_id!r} is not an ambient dimension") + missing: list[str] = [] + seen: set[str] = set() + for instance_id in instance_ids: + if not isinstance(instance_id, str) or not instance_id or instance_id.isspace(): + raise DimensionalArityError("instance id must be exact non-empty text") + if instance_id == hub_id: + raise DimensionalArityError("the hub is not an instance of x or y") + if instance_id not in ambient: + raise DimensionalArityError(f"instance {instance_id!r} is not an ambient dimension") + if instance_id in seen: + raise DimensionalArityError(f"instance {instance_id!r} is repeated; occurrences must be unique") + seen.add(instance_id) + if not has_declared_coupling(declared, [hub_id, instance_id]): + missing.append(instance_id) + return tuple(missing) + + +def require_every_instance_has_oriented_hub_coupling( + declared: DimensionalSpace, + *, + hub_id: str, + instance_ids: Sequence[str], +) -> None: + """Fail closed unless every instance has its own (z, instance).""" + + missing = instances_missing_oriented_hub_coupling( + declared, hub_id=hub_id, instance_ids=instance_ids + ) + if missing: + raise DimensionalArityError( + f"every instance must have declared ({hub_id}, instance); missing {tuple(missing)}" + ) + + +def oriented_instance_couplings( + declared: DimensionalSpace, + *, + hub_id: str, + instance_ids: Sequence[str], +) -> tuple[tuple[str, str], ...]: + """The (z, x_i) / (z, y_j) coupling for each instance, in instance order.""" + + require_every_instance_has_oriented_hub_coupling( + declared, hub_id=hub_id, instance_ids=instance_ids + ) + return tuple((hub_id, instance_id) for instance_id in instance_ids) + + +def _bind_coupling_to_ambient( + item: Coupling, ambient_by_id: Mapping[str, Dimension] +) -> Coupling: + dimensions: list[Dimension] = [] + for dimension in item.dimensions: + ambient = ambient_by_id.get(dimension.id) + if ambient is None: + raise DimensionalArityError( + f"proven coupling {item.declared_ids} uses undeclared dimension {dimension.id!r}" + ) + if dimension.charge is not None and dimension.charge != ambient.charge: + raise DimensionalArityError( + f"proof conclusion charge for {dimension.id!r} conflicts with ambient charge" + ) + dimensions.append(ambient) + return Coupling(tuple(dimensions)) + + +def install_proven_coupling(declared: DimensionalSpace, proof: CouplingProof) -> DimensionalSpace: + """Add a coupling only with an explicit non-forbidden proof.""" + + ambient_by_id = {axis.id: axis for axis in declared.ambient_dimensions} + bound_conclusion = _bind_coupling_to_ambient(proof.conclusion, ambient_by_id) + bound_proof = CouplingProof( + conclusion=bound_conclusion, + premises=proof.premises, + rule_id=proof.rule_id, + ) + declared_ids = {item.declared_ids for item in declared.couplings} + for premise in bound_proof.premises: + if premise.declared_ids not in declared_ids: + raise DimensionalArityError( + f"proof {proof.rule_id!r} cites missing premise {premise.declared_ids}" + ) + if bound_conclusion.declared_ids in declared_ids: + return DimensionalSpace( + ambient_dimensions=declared.ambient_dimensions, + couplings=declared.couplings, + proofs=declared.proofs + (bound_proof,), + ) + return DimensionalSpace( + ambient_dimensions=declared.ambient_dimensions, + couplings=declared.couplings + (bound_conclusion,), + proofs=declared.proofs + (bound_proof,), + ) + + +def local_three_structures(declared: DimensionalSpace) -> tuple[tuple[str, str, str], ...]: + """Each hub with two hub-first arity-2 instances is one local 3. + + ``(z, x)`` and ``(z, y)`` yield ``(z, x, y)`` as a represented triple. + That is not a declared ternary coupling. One coupling is not a 3. + """ + + by_hub: dict[str, list[str]] = {} + for item in declared.couplings: + if item.arity != 2: + continue + hub_id, instance_id = item.declared_ids + by_hub.setdefault(hub_id, []).append(instance_id) + threes: list[tuple[str, str, str]] = [] + for hub_id, instance_ids in by_hub.items(): + for index, first in enumerate(instance_ids): + for second in instance_ids[index + 1 :]: + threes.append((hub_id, first, second)) + return tuple(threes) + + +def quaternion_of_local_three( + declared: DimensionalSpace, + represented_ids: tuple[str, str, str], +) -> Mapping[str, object]: + """4 components for one 3: scalar ε plus the three axis charges. + + Hamilton product is not a coupling proof. The scalar axis is representation, + not ambient. + """ + + charges = {axis.id: axis.charge for axis in declared.ambient_dimensions} + hub_id, first_id, second_id = represented_ids + return { + "components": ( + MOBIUS_EPSILON_T0, + charges.get(hub_id), + charges.get(first_id), + charges.get(second_id), + ), + "axes": (QUATERNION_SCALAR_AXIS, hub_id, first_id, second_id), + "represented_ids": represented_ids, + "representation_dimension": QUATERNION_REPRESENTATION_DIMENSION, + "represented_structure_dimension": REPRESENTED_STRUCTURE_DIMENSION, + "hamilton_product_is_coupling_proof": False, + "scalar_axis_is_ambient": False, + } + + +def quaternions_from_declared_couplings( + declared: DimensionalSpace, +) -> tuple[Mapping[str, object], ...]: + return tuple( + quaternion_of_local_three(declared, represented) + for represented in local_three_structures(declared) + ) + + +def structure_from_charged_couplings(declared: DimensionalSpace) -> Mapping[str, object]: + """The three-dimensional structure already present in the couplings. + + Each part is one declared oriented coupling together with its arity charge + state. Degree records how those parts sit on shared axes. Representing + each local 3 takes a 4-component quaternion. This is not an inferred + cartesian embedding and not a ternary coupling. + """ + + degrees = degree_relations(declared) + parts = tuple( + { + "coupling": item.declared_ids, + "arity": item.arity, + "charge_state": item.charge_state, + } + for item in declared.couplings + ) + return { + "kind": "combination-of-oriented-couplings-and-arity-charge-states", + "parts": parts, + "degree": tuple( + { + "dimension": item.dimension.id, + "charge": item.dimension.charge, + "degree": item.degree, + "slot_degrees": item.slot_degrees, + "incidences": item.incidences, + } + for item in degrees + if item.degree + ), + "participating_dimension_count": len( + {name for item in declared.couplings for name in item.declared_ids} + ), + "ternary_coupling_declared": any(item.arity == 3 for item in declared.couplings), + "inferred_cartesian_embedding": False, + "representation_kind": "quaternion", + "representation_dimension": QUATERNION_REPRESENTATION_DIMENSION, + "represented_structure_dimension": REPRESENTED_STRUCTURE_DIMENSION, + "quaternions": quaternions_from_declared_couplings(declared), + } + + +def _tuple_tree(value: object) -> object: + if isinstance(value, Mapping): + return tuple(sorted((str(key), _tuple_tree(item)) for key, item in value.items())) + if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + return tuple(_tuple_tree(item) for item in value) + return value + + +def _sortable_tree(value: object) -> object: + if value is None: + return (0,) + if isinstance(value, bool): + return (1, int(value)) + if isinstance(value, int): + return (2, value) + if isinstance(value, str): + return (3, value) + if isinstance(value, Mapping): + return (4, tuple(sorted((str(key), _sortable_tree(item)) for key, item in value.items()))) + if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + return (5, tuple(_sortable_tree(item) for item in value)) + return (6, repr(value)) + + +def charged_structure_readout(structure: Mapping[str, object]) -> tuple[object, ...]: + """Order-invariant 3-structure: couplings + charge states + degree. + + Each instance stays in the coupling ids. Slot order inside each coupling is + kept, so ``(8, 1)`` is not ``(1, 8)`` and ``(z, x0)`` is not ``(z, x1)``. + """ + + parts = tuple( + ( + int(part["arity"]), + _tuple_tree(part["charge_state"]), + _tuple_tree(part["coupling"]), + ) + for part in structure["parts"] + ) + degree = tuple( + ( + int(item["degree"]), + _tuple_tree(item["slot_degrees"]), + item["charge"], + ) + for item in structure["degree"] + ) + parts = tuple(sorted(parts, key=_sortable_tree)) + degree = tuple(sorted(degree, key=_sortable_tree)) + return ( + parts, + degree, + int(structure["participating_dimension_count"]), + bool(structure["ternary_coupling_declared"]), + ) + + +def topology_structure_readout(structure: Mapping[str, object]) -> tuple[object, ...]: + """Arity and degree only. Charge state is omitted.""" + + parts, degree, participating, ternary = charged_structure_readout(structure) + return ( + tuple(item[0] for item in parts), + tuple((deg, slots) for deg, slots, _charge in degree), + participating, + ternary, + ) + + +def quaternion_structure_readout(structure: Mapping[str, object]) -> tuple[object, ...]: + """Order-invariant 4-component representations of each local 3.""" + + return tuple( + sorted( + ( + _tuple_tree(item["components"]), + _tuple_tree(item["represented_ids"]), + ) + for item in structure.get("quaternions", ()) + ) + ) + + +def geometry_from_declared_couplings(declared: DimensionalSpace) -> Mapping[str, object]: + degrees = degree_relations(declared) + couplings = tuple( + { + "declared_ids": item.declared_ids, + "arity": item.arity, + "slot_charges": item.slot_charges, + "charge_state": item.charge_state, + "mobius_epsilon_t0": MOBIUS_EPSILON_T0, + } + for item in declared.couplings + ) + return { + "ambient_ids": tuple(item.id for item in declared.ambient_dimensions), + "ambient_count": len(declared.ambient_dimensions), + "couplings": couplings, + "participating_ids": tuple( + dict.fromkeys(name for item in declared.couplings for name in item.declared_ids) + ), + "arity_counts": _arity_counts(declared.couplings), + "degree_relations": tuple( + { + "dimension": item.dimension.id, + "degree": item.degree, + "slot_degrees": item.slot_degrees, + "incidences": item.incidences, + } + for item in degrees + ), + "observed_common_ids": tuple(_common_records(declared.couplings)), + "proofs": tuple( + { + "rule_id": proof.rule_id, + "premises": tuple(item.declared_ids for item in proof.premises), + "conclusion": proof.conclusion.declared_ids, + } + for proof in declared.proofs + ), + "inferred_from_ambient": False, + "inferred_higher_arity_from_overlap": False, + "zx_equals_xz": False, + "structure": structure_from_charged_couplings(declared), + } + + +def _arity_counts(couplings: tuple[Coupling, ...]) -> tuple[tuple[int, int], ...]: + counts: dict[int, int] = {} + for item in couplings: + counts[item.arity] = counts.get(item.arity, 0) + 1 + return tuple(sorted(counts.items())) + + +def _common_records(couplings: tuple[Coupling, ...]) -> Iterable[Mapping[str, object]]: + for i, left in enumerate(couplings): + for j, right in enumerate(couplings): + if j <= i: + continue + shared = observed_common_ids(left, right) + if shared: + yield { + "left": left.declared_ids, + "right": right.declared_ids, + "common_ids": tuple(sorted(shared)), + "proof_of_higher_arity": False, + } + + +__all__ = [ + "Coupling", + "CouplingProof", + "DegreeRelation", + "Dimension", + "DimensionalArityError", + "DimensionalSpace", + "FORBIDDEN_INFERENCE_RULES", + "MOBIUS_EPSILON_T0", + "QUATERNION_REPRESENTATION_DIMENSION", + "QUATERNION_SCALAR_AXIS", + "REPRESENTED_STRUCTURE_DIMENSION", + "charged_structure_readout", + "coupling", + "degree_relations", + "dimension", + "geometry_from_declared_couplings", + "has_declared_coupling", + "install_proven_coupling", + "instances_missing_oriented_hub_coupling", + "local_three_structures", + "observed_common_ids", + "oriented_instance_couplings", + "quaternion_of_local_three", + "quaternion_structure_readout", + "quaternions_from_declared_couplings", + "require_every_instance_has_oriented_hub_coupling", + "space", + "structure_from_charged_couplings", + "topology_structure_readout", +] diff --git a/epac_molecular.py b/epac_molecular.py new file mode 100644 index 0000000..dd28d23 --- /dev/null +++ b/epac_molecular.py @@ -0,0 +1,301 @@ +"""Molecular EPAC Public Gonols from atomic electron-shell gonols. + +Attachment sites are unpaired valence electrons (atomic Hund filling). +If ligand count exceeds ground-state unpaired count, the atomic promoted +valence set (s→p in the same n) is used. Ligand and center (l, m_l) sets +are construction invariants. Construction uses ``epac.public_gonol``, not +``edcm.gonol``. No sealed molecular-shape file is opened here. + +The three-dimensional structure is the combination of declared oriented +couplings and each arity's charge state (nuclear Z plus Möbius ε at t=0) +with degree. Every ligand instance has its own (center, instance) coupling. +It is not an inferred cartesian embedding. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Mapping + +from ucns.direct_mobius import native_mobius_state + +from epac_dimensional_arity import ( + charged_structure_readout, + geometry_from_declared_couplings, + oriented_instance_couplings, + space, + topology_structure_readout, +) +from epac_periodic import carried, construct_element_gonol, symbol_of +from epac_public_gonol import ClosedPublicGonol, PublicGonolReceipt, construct_public_gonol, replay_public_gonol + + +MOLECULE_COMPOSITIONS: Mapping[str, tuple[tuple[str, int], ...]] = { + "H2": (("H", 2),), + "H2O": (("H", 2), ("O", 1)), + "NH3": (("N", 1), ("H", 3)), + "CH4": (("C", 1), ("H", 4)), + "CO2": (("C", 1), ("O", 2)), +} + +RELATION = "epac.affixiation.unpaired-valence" + + +@dataclass(frozen=True, slots=True) +class MolecularConstruction: + formula: str + receipt: PublicGonolReceipt + invariants: Mapping[str, Any] + + +def _instantiate(composition: tuple[tuple[str, int], ...]) -> tuple[ClosedPublicGonol, ...]: + instances: list[ClosedPublicGonol] = [] + occurrence = 0 + for symbol, count in composition: + for _ in range(count): + instances.append(construct_element_gonol(symbol, occurrence=occurrence).gonol) + occurrence += 1 + return tuple(instances) + + +def _parse_lm(text: str) -> tuple[tuple[int, int], ...]: + """Parse a carried ``*-lm`` option into ``(l, m_l)`` pairs, preserving order.""" + if text in ("", "none"): + return () + pairs: list[tuple[int, int]] = [] + for part in text.split(","): + l_text, m_text = part.split(":") + pairs.append((int(l_text), int(m_text))) + return tuple(pairs) + + +def _unpaired_lm(gonol: ClosedPublicGonol) -> tuple[tuple[int, int], ...]: + return _parse_lm(carried(gonol, "unpaired-valence-lm")) + + +def _promoted_lm(gonol: ClosedPublicGonol) -> tuple[tuple[int, int], ...]: + return _parse_lm(carried(gonol, "promoted-unpaired-lm")) + + +def _choose_center(participants: tuple[ClosedPublicGonol, ...]) -> ClosedPublicGonol | None: + """Center is the unique singleton symbol when ligands share another symbol. + + This is stoichiometric, not a shape rule. H2 has no singleton. + """ + + counts: dict[str, int] = {} + for item in participants: + counts[symbol_of(item)] = counts.get(symbol_of(item), 0) + 1 + singletons = [symbol for symbol, count in counts.items() if count == 1] + if len(singletons) == 1 and len(counts) > 1: + symbol = singletons[0] + return next(item for item in participants if symbol_of(item) == symbol) + return None + + +def _attachment_set(gonol: ClosedPublicGonol, needed: int) -> tuple[tuple[int, int], ...]: + """Attachment sites derive from the already-closed element gonol. + + No periodic-table relookup: the element gonol's carried promotion evidence + is the only promotion source for molecular construction. + """ + + ground = _unpaired_lm(gonol) + if len(ground) >= needed: + return ground[:needed] + promoted = _promoted_lm(gonol) + if len(promoted) >= needed: + return promoted[:needed] + raise ValueError( + f"{symbol_of(gonol)} has {len(ground)} unpaired valence electrons; " + f"{needed} attachment sites were requested" + ) + + +def _atom_dimension_id(gonol: ClosedPublicGonol) -> str: + return f"{symbol_of(gonol)}#{gonol.occurrence}" + + +def _declared_dimensional_space( + participants: tuple[ClosedPublicGonol, ...], + center: ClosedPublicGonol | None, + ligands: tuple[ClosedPublicGonol, ...], +): + ambient = [_atom_dimension_id(item) for item in participants] + charges = {_atom_dimension_id(item): int(carried(item, "Z")) for item in participants} + if center is None: + declarations = [[_atom_dimension_id(participants[0]), _atom_dimension_id(participants[1])]] + else: + center_id = _atom_dimension_id(center) + declarations = [[center_id, _atom_dimension_id(ligand)] for ligand in ligands] + return space(ambient, declarations, charges=charges) + + +def _site_label(site: tuple[int, int]) -> str: + return f"{site[0]}:{site[1]}" + + +def _mobius_coupling( + *, + participants: tuple[ClosedPublicGonol, ...], + center: ClosedPublicGonol | None, + ligands: tuple[ClosedPublicGonol, ...], + center_sites: tuple[tuple[int, int], ...], + ligand_sites: tuple[tuple[tuple[int, int], ...], ...], +) -> Mapping[str, Any]: + origin = native_mobius_state(0) + one = origin.advance(1) + two = origin.advance(2) + if center is None: + attachment_slots = tuple( + { + "slot": slot, + "participant": _atom_dimension_id(participant), + "site": _site_label(site), + } + for slot, (participant, sites) in enumerate(zip(participants, ligand_sites)) + for site in sites + ) + else: + flattened_ligand_sites = tuple( + (ligand, site) + for ligand, sites in zip(ligands, ligand_sites) + for site in sites + ) + attachment_slots = tuple( + { + "slot": slot, + "center": _atom_dimension_id(center), + "center_site": _site_label(center_site), + "ligand": _atom_dimension_id(ligand), + "ligand_site": _site_label(ligand_site), + } + for slot, (center_site, (ligand, ligand_site)) in enumerate( + zip(center_sites, flattened_ligand_sites) + ) + ) + return { + "law": "ucns.native-mobius-root-loop", + "binding": "declared-participants-and-valence-attachment-sites", + "parameter": "turn-index-over-declared-attachment-evidence", + "participant_axes": tuple(_atom_dimension_id(item) for item in participants), + "attachment_slots": attachment_slots, + "t": [0, 1, 2], + "visible_phase": [ + str(origin.visible_key[1]), + str(one.visible_key[1]), + str(two.visible_key[1]), + ], + "frame": [origin.frame.value, one.frame.value, two.frame.value], + "complete_restored": two.complete_key == origin.complete_key, + "one_turn_flips_frame": one.frame != origin.frame and one.visible_key == origin.visible_key, + } + + +def construct_molecule(formula: str) -> MolecularConstruction: + if formula not in MOLECULE_COMPOSITIONS: + raise ValueError(f"formula {formula!r} is outside the declared run") + participants = _instantiate(MOLECULE_COMPOSITIONS[formula]) + center = _choose_center(participants) + if center is None: + ligands = () + center_sites: tuple[tuple[int, int], ...] = () + if len(participants) != 2: + raise ValueError("symmetric affixiation is declared only for two equal atoms") + ligand_sites = ( + _unpaired_lm(participants[0]), + _unpaired_lm(participants[1]), + ) + used_promotion = False + else: + ligands = tuple(item for item in participants if item is not center) + ground = _unpaired_lm(center) + ligand_sites = tuple(_unpaired_lm(item) for item in ligands) + needed = sum(len(sites) for sites in ligand_sites) + used_promotion = needed > len(ground) + center_sites = _attachment_set(center, needed) + mobius = _mobius_coupling( + participants=participants, + center=center, + ligands=ligands, + center_sites=center_sites, + ligand_sites=ligand_sites, + ) + dimensional = _declared_dimensional_space(participants, center, ligands) + instance_couplings: tuple[tuple[str, str], ...] = () + if center is not None: + instance_couplings = oriented_instance_couplings( + dimensional, + hub_id=_atom_dimension_id(center), + instance_ids=tuple(_atom_dimension_id(item) for item in ligands), + ) + geometry = geometry_from_declared_couplings(dimensional) + receipt = construct_public_gonol( + source_id=f"epac.molecule:{formula}", + relation=RELATION, + participants=participants, + couplings=geometry["couplings"], + structure=geometry["structure"], + ) + distinct_p_m = tuple(sorted({m for l, m in center_sites if l == 1})) + ligand_has_p = any(any(l == 1 for l, _m in sites) for sites in ligand_sites) + invariants = { + "formula": formula, + "atom_count": len(participants), + "center_symbol": None if center is None else symbol_of(center), + "center_Z": None if center is None else carried(center, "Z"), + "center_configuration": None if center is None else carried(center, "electron-configuration"), + "center_valence_electrons": None if center is None else carried(center, "valence-electrons"), + "center_unpaired_lm": [f"{l}:{m}" for l, m in center_sites], + "center_attachment_site_count": len(center_sites), + "ligand_attachment_site_count": sum(len(sites) for sites in ligand_sites), + "center_used_atomic_promotion": used_promotion, + "center_distinct_p_m": [str(m) for m in distinct_p_m], + "ligand_symbols": [symbol_of(item) for item in ligands], + "ligand_unpaired_lm": [[f"{l}:{m}" for l, m in sites] for sites in ligand_sites], + "ligand_has_p": ligand_has_p, + "participant_symbols": [symbol_of(item) for item in participants], + "atomic_coupling_signature": ( + None if center is None else carried(center, "electron-configuration"), + tuple(center_sites), + tuple(ligand_sites), + used_promotion, + ligand_has_p, + ), + "mobius": mobius, + "ucns_coupling_signature": ( + mobius["law"], + tuple(mobius["participant_axes"]), + tuple( + tuple(sorted(slot.items())) + for slot in mobius["attachment_slots"] + ), + tuple(mobius["t"]), + tuple(mobius["frame"]), + mobius["complete_restored"], + ), + "dimensional_geometry": geometry, + "declared_coupling_arities": [item["arity"] for item in geometry["couplings"]], + "charged_structure_readout": charged_structure_readout(geometry["structure"]), + "topology_structure_readout": topology_structure_readout(geometry["structure"]), + "oriented_instance_couplings": instance_couplings, + } + return MolecularConstruction(formula=formula, receipt=receipt, invariants=invariants) + + +def replay_molecule(construction: MolecularConstruction) -> PublicGonolReceipt: + return replay_public_gonol(construction.receipt) + + +def construct_declared_molecules() -> dict[str, MolecularConstruction]: + return {formula: construct_molecule(formula) for formula in MOLECULE_COMPOSITIONS} + + +def matched_information_control(invariants: Mapping[str, Any]) -> tuple[Any, ...]: + """Control: stoichiometric symbols only, no shells or wave identities.""" + + return ( + invariants["atom_count"], + invariants["center_symbol"], + tuple(invariants["ligand_symbols"]), + ) diff --git a/epac_periodic.py b/epac_periodic.py new file mode 100644 index 0000000..3e1c401 --- /dev/null +++ b/epac_periodic.py @@ -0,0 +1,314 @@ +"""Element gonols closed as EPAC Public Gonols from nucleon then electron structure. + +Precursors: each proton and each neutron is a closed gonol. The nucleus is +their affixiation. Electrons then couple to that closed nucleus. Molecular +construction must not reopen nucleons or electrons. Letters are not axes. + +Usage guidance +-------------- +Each nucleon, nucleus, electron, shell, and element is an EPAC Public Gonol +on the UCNS carrier. This module does not use ``edcm.gonol``. + + from epac_periodic import construct_element_gonol, construct_periodic_table + + helium = construct_element_gonol("He") + nucleus = helium.gonol.participants[0] + assert [p.relation for p in nucleus.participants] == [ + "epac.atomic.proton", "epac.atomic.proton", + "epac.atomic.neutron", "epac.atomic.neutron", + ] +""" + +from __future__ import annotations + +from typing import Iterable + +from epac_atomic import AtomicRecord, ElectronState, iter_table +from epac_dimensional_arity import ( + geometry_from_declared_couplings, + oriented_instance_couplings, + space, +) +from epac_public_gonol import ( + ClosedPublicGonol, + PublicGonolReceipt, + construct_public_gonol, + replay_public_gonol, +) + +# Elementary charge in units of e. Nuclear Z is the proton-count sum. +PROTON_CHARGE = 1 +NEUTRON_CHARGE = 0 +ELECTRON_CHARGE = -1 +NUCLEUS_RELATION = "epac.atomic.nucleus" +PROTON_RELATION = "epac.atomic.proton" +NEUTRON_RELATION = "epac.atomic.neutron" + + +def _carrier_glyph(text: str) -> str | None: + if len(text) == 1: + return text + return None + + +def _electron_options(electron: ElectronState) -> tuple[tuple[str, str], ...]: + return ( + ("n", str(electron.n)), + ("l", str(electron.l)), + ("m_l", str(electron.m_l)), + ("m_s", str(electron.m_s)), + ("shell", electron.shell), + ("subshell", electron.subshell), + ("angular-id", electron.angular_id), + ("radial-nodes", str(electron.radial_nodes)), + ("z-eff", electron.z_eff), + ("e-rydberg", electron.e_rydberg), + ("valence", "true" if electron.valence else "false"), + ("paired", "true" if electron.paired else "false"), + ) + + +def _construct_electron( + electron: ElectronState, *, symbol: str, atom_occurrence: int +) -> ClosedPublicGonol: + return construct_public_gonol( + source_id=f"epac.electron:{symbol}#{atom_occurrence}:{electron.index}", + relation="epac.atomic.electron", + identity_glyph="e", + carried_options=_electron_options(electron), + occurrence=electron.index, + ).gonol + + +def _construct_shell( + n: int, + electrons: Iterable[ElectronState], + *, + symbol: str, + atom_occurrence: int, +) -> ClosedPublicGonol: + members = tuple( + _construct_electron(e, symbol=symbol, atom_occurrence=atom_occurrence) for e in electrons + ) + return construct_public_gonol( + source_id=f"epac.shell:{symbol}#{atom_occurrence}:n{n}", + relation="epac.atomic.shell", + identity_glyph=_carrier_glyph(str(n)), + participants=members, + occurrence=n, + carried_options=(("n", str(n)),), + ).gonol + + +def _proton_dimension_id(symbol: str, atom_occurrence: int, index: int) -> str: + return f"epac.proton:{symbol}#{atom_occurrence}:{index}" + + +def _neutron_dimension_id(symbol: str, atom_occurrence: int, index: int) -> str: + return f"epac.neutron:{symbol}#{atom_occurrence}:{index}" + + +def _construct_proton( + *, symbol: str, atom_occurrence: int, index: int +) -> ClosedPublicGonol: + return construct_public_gonol( + source_id=_proton_dimension_id(symbol, atom_occurrence, index), + relation=PROTON_RELATION, + occurrence=index, + carried_options=( + ("charge", str(PROTON_CHARGE)), + ("symbol", symbol), + ("kind", "proton"), + ), + ).gonol + + +def _construct_neutron( + *, symbol: str, atom_occurrence: int, index: int +) -> ClosedPublicGonol: + return construct_public_gonol( + source_id=_neutron_dimension_id(symbol, atom_occurrence, index), + relation=NEUTRON_RELATION, + occurrence=index, + carried_options=( + ("charge", str(NEUTRON_CHARGE)), + ("symbol", symbol), + ("kind", "neutron"), + ), + ).gonol + + +def _declared_nuclear_space(record: AtomicRecord, *, atom_occurrence: int): + """Neutrons couple to protons. Proton-proton and neutron-neutron are not inferred. + + Hydrogen-1 has one proton and no neutrons, so no nuclear 3. + """ + + if record.proton_count != record.Z: + raise ValueError(f"{record.symbol}: proton count must equal Z") + if record.neutron_count != record.A - record.Z: + raise ValueError(f"{record.symbol}: neutron count must equal A-Z") + proton_ids = [ + _proton_dimension_id(record.symbol, atom_occurrence, index) + for index in range(record.proton_count) + ] + neutron_ids = [ + _neutron_dimension_id(record.symbol, atom_occurrence, index) + for index in range(record.neutron_count) + ] + charges = { + **{proton_id: PROTON_CHARGE for proton_id in proton_ids}, + **{neutron_id: NEUTRON_CHARGE for neutron_id in neutron_ids}, + } + declarations = [ + [proton_id, neutron_id] for proton_id in proton_ids for neutron_id in neutron_ids + ] + declared = space([*proton_ids, *neutron_ids], declarations, charges=charges) + for proton_id in proton_ids: + if neutron_ids: + oriented_instance_couplings( + declared, hub_id=proton_id, instance_ids=neutron_ids + ) + return declared + + +def _construct_nucleus(record: AtomicRecord, *, atom_occurrence: int) -> ClosedPublicGonol: + protons = tuple( + _construct_proton(symbol=record.symbol, atom_occurrence=atom_occurrence, index=index) + for index in range(record.proton_count) + ) + neutrons = tuple( + _construct_neutron(symbol=record.symbol, atom_occurrence=atom_occurrence, index=index) + for index in range(record.neutron_count) + ) + if len(protons) != record.Z or len(neutrons) != record.neutron_count: + raise ValueError(f"{record.symbol}: nucleon gonols must match Z and A-Z") + geometry = geometry_from_declared_couplings( + _declared_nuclear_space(record, atom_occurrence=atom_occurrence) + ) + couplings = geometry["couplings"] + structure = geometry["structure"] if couplings else None + return construct_public_gonol( + source_id=f"epac.nucleus:{record.symbol}#{atom_occurrence}", + relation=NUCLEUS_RELATION, + participants=(*protons, *neutrons), + carried_options=( + ("Z", str(record.Z)), + ("A", str(record.A)), + ("protons", str(record.proton_count)), + ("neutrons", str(record.neutron_count)), + ("symbol", record.symbol), + ), + occurrence=0, + couplings=couplings, + structure=structure, + ).gonol + + +def _nucleus_dimension_id(symbol: str, atom_occurrence: int) -> str: + return f"epac.nucleus:{symbol}#{atom_occurrence}" + + +def _electron_dimension_id(symbol: str, atom_occurrence: int, index: int) -> str: + return f"epac.electron:{symbol}#{atom_occurrence}:{index}" + + +def _declared_atomic_space(record: AtomicRecord, *, atom_occurrence: int): + """One ``(nucleus, electron_i)`` coupling for every electron instance. + + Closed shells still participate as instances. Letters do not. + """ + + hub = _nucleus_dimension_id(record.symbol, atom_occurrence) + electron_ids = [ + _electron_dimension_id(record.symbol, atom_occurrence, electron.index) + for electron in record.electrons + ] + charges = {hub: record.Z, **{electron_id: ELECTRON_CHARGE for electron_id in electron_ids}} + declared = space( + [hub, *electron_ids], + [[hub, electron_id] for electron_id in electron_ids], + charges=charges, + ) + oriented_instance_couplings(declared, hub_id=hub, instance_ids=electron_ids) + return declared + + +def construct_element_gonol(symbol: str, *, occurrence: int = 0) -> PublicGonolReceipt: + """Close one element Public Gonol whose participants are nucleus + electron shells.""" + + record = None + for item in iter_table(): + if item.symbol == symbol: + record = item + break + if record is None: + raise ValueError(f"no atomic record for symbol {symbol!r}") + shells: list[ClosedPublicGonol] = [] + by_n: dict[int, list[ElectronState]] = {} + for electron in record.electrons: + by_n.setdefault(electron.n, []).append(electron) + for n in sorted(by_n): + shells.append(_construct_shell(n, by_n[n], symbol=symbol, atom_occurrence=occurrence)) + nucleus = _construct_nucleus(record, atom_occurrence=occurrence) + unpaired = record.unpaired_valence + promoted = record.promoted_unpaired_valence + carried = ( + ("symbol", record.symbol), + ("Z", str(record.Z)), + ("period", str(record.period)), + ("group", str(record.group)), + ("A", str(record.A)), + ("electron-configuration", record.configuration), + ("valence-n", str(record.valence_n)), + ("valence-electrons", str(record.valence_electrons)), + ("unpaired-valence-count", str(len(unpaired))), + ("unpaired-valence-lm", ",".join(f"{e.l}:{e.m_l}" for e in unpaired) or "none"), + ("promoted-unpaired-count", str(len(promoted))), + ("promoted-unpaired-lm", ",".join(f"{e.l}:{e.m_l}" for e in promoted) or "none"), + ("valence-angular-ids", ",".join(e.angular_id for e in record.electrons if e.valence)), + ) + geometry = geometry_from_declared_couplings( + _declared_atomic_space(record, atom_occurrence=occurrence) + ) + return construct_public_gonol( + source_id=f"epac.periodic:{symbol}#{occurrence}", + relation="epac.atomic.element", + identity_glyph=_carrier_glyph(symbol), + participants=(nucleus, *shells), + carried_options=carried, + occurrence=occurrence, + couplings=geometry["couplings"], + structure=geometry["structure"], + ) + + +def construct_periodic_table() -> dict[str, PublicGonolReceipt]: + return {record.symbol: construct_element_gonol(record.symbol) for record in iter_table()} + + +def replay_element_gonol(receipt: PublicGonolReceipt) -> PublicGonolReceipt: + return replay_public_gonol(receipt) + + +def atomic_of(symbol: str) -> AtomicRecord: + for record in iter_table(): + if record.symbol == symbol: + return record + raise ValueError(symbol) + + +def symbol_of(gonol: ClosedPublicGonol) -> str: + for key, value in gonol.carried_options: + if key == "symbol": + return value + if gonol.identity_glyph: + return gonol.identity_glyph + raise KeyError("symbol") + + +def carried(gonol: ClosedPublicGonol, key: str) -> str: + for item_key, value in gonol.carried_options: + if item_key == key: + return value + raise KeyError(key) diff --git a/epac_public_gonol.py b/epac_public_gonol.py new file mode 100644 index 0000000..1eb8176 --- /dev/null +++ b/epac_public_gonol.py @@ -0,0 +1,450 @@ +"""EPAC Public Gonol constructor. + +EPAC closes gonols on the UCNS Public Gonol carrier. This is not the EDCM +text-domain constructor. Glyphs are identity coordinates only; Public Gonol +function operations and a Möbius coupling law remain hmmm. + +Charge state is already in the math: per-slot nuclear Z with Möbius ε at t=0 +from ``(t, ε) ~ (t+n, (-1)^n ε)``. Oriented couplings plus those charge +states plus degree are the three-dimensional structure. Representing that 3 +takes a 4-component quaternion; the extra coordinate is the scalar ε. No +cartesian embedding, ternary coupling, or Hamilton-product coupling is inferred. + +Usage guidance +-------------- + from epac_public_gonol import construct_public_gonol, replay_public_gonol + + oxygen = construct_public_gonol( + source_id="epac.atomic.element:O#0", + relation="epac.atomic.element", + identity_glyph="O", + carried_options=(("Z", "8"), ("symbol", "O")), + ) + assert oxygen.constructor_id == "epac.public_gonol" + assert replay_public_gonol(oxygen).receipt_digest == oxygen.receipt_digest +""" + +# === MODULE_BUILD === +# id: epac_public_gonol +# module_name: epac_public_gonol +# module_kind: experiment +# summary: EPAC candidate constructor that closes gonols on the UCNS Public Gonol carrier with oriented couplings and arity charge states; not the EDCM text-domain constructor +# owner: The Interdependency +# public_surface: CONSTRUCTOR_ID, CONSTRUCTOR_VERSION, PINNED_PUBLIC_GONOL_SHA256, ClosedPublicGonol, PublicGonolReceipt, PublicGonolConstructionError, construct_public_gonol, replay_public_gonol, canonical_receipt_bytes +# internal_surface: _require_text, _identity_position, _geometry, _participant_payload, _atomic_payload, _receipt_payload, _digest +# auth_boundary: EPAC owns particle/energy gonol closure; UCNS owns Public Gonol carrier identity and native Möbius ε; EDCM text-domain constructor is not used; METAPAT affixiation is consumed, not redefined +# storage_boundary: none; receipts remain caller-owned in-memory objects +# network_boundary: none +# user_data_boundary: caller-supplied source_id, relation, participants, and carried options remain in memory +# admin_only: false +# tests: tests.test_epac_public_gonol, tests.test_periodic_element_gonols, tests.test_molecular_affixiation +# rollout: explicit EPAC candidate constructor; no canon selection, no EDCM scale option sets, no invented position operation +# rollback: remove this module; do not fall back to edcm.gonol for EPAC construction +# requires: ucns_public_gonol_geometry, ucns_native_mobius_geometry +# since: 2026-08-22 +# unresolved: exact UCNS geometric operation of Public Gonol function positions; UCNS Möbius-carrier affixiation/coupling law; two-letter element symbols have no single carrier glyph +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: epac_public_gonol_is_not_edcm_gonol +# given: an EPAC gonol is constructed +# then: constructor_id is epac.public_gonol and edcm.gonol is not imported or invoked +# class: doctrine +# since: 2026-08-22 +# +# id: epac_public_gonol_binds_ucns_carrier_identity +# given: identity_glyph is an admitted Public Gonol glyph +# then: the closed gonol carries the exact UCNS index/glyph pair and the pinned carrier digest +# class: construction +# since: 2026-08-22 +# +# id: epac_public_gonol_replays_byte_identical +# given: a PublicGonolReceipt +# then: replay_public_gonol reproduces the same receipt_digest +# class: correctness +# since: 2026-08-22 +# +# id: charged_oriented_couplings_are_the_structure +# given: declared oriented couplings with per-slot charges +# then: receipt.structure is the combination of those couplings, arity charge states, and degree; no (x,y,z) coupling is inferred +# class: construction +# since: 2026-08-22 +# === END CONTRACTS === + +from __future__ import annotations + +from collections.abc import Mapping as MappingABC +from collections.abc import Sequence as SequenceABC +from dataclasses import dataclass +from hashlib import sha256 +import json +from types import MappingProxyType +from typing import Any, Mapping, Sequence + +from ucns import ( + PUBLIC_GONOL_SHA256, + native_mobius_state, + public_gonol_function, + public_gonol_sha256, +) + + +CONSTRUCTOR_ID = "epac.public_gonol" +CONSTRUCTOR_VERSION = "v1" +PINNED_PUBLIC_GONOL_SHA256 = PUBLIC_GONOL_SHA256 +STANDING = "implemented-candidate" +SELECTION_EFFECT = "none" + +NONCLAIMS: tuple[str, ...] = ( + "not selected canon", + "not EDCM text-domain gonol construction", + "not a UCNS geometric function operation", + "not a UCNS Möbius coupling law", + "not METAPAT canon promotion", + "not imported chemistry shape names", +) + +HMMM: tuple[str, ...] = ( + "exact UCNS geometric operation of each Public Gonol function position", + "UCNS Möbius-carrier affixiation/coupling law", + "two-letter element symbols have no single Public Gonol glyph", +) + + +class PublicGonolConstructionError(RuntimeError): + """Fail-closed EPAC Public Gonol constructor error.""" + + +@dataclass(frozen=True, slots=True) +class ClosedPublicGonol: + """One closed EPAC gonol. Atomic at any later declared participation.""" + + source_id: str + occurrence: int + relation: str + identity_glyph: str | None + carrier_index: int | None + participants: tuple["ClosedPublicGonol", ...] + carried_options: tuple[tuple[str, str], ...] + couplings: tuple[Mapping[str, Any], ...] + structure: Mapping[str, Any] | None + atomic_id: str + receipt_digest: str + geometry_digest: str + + +@dataclass(frozen=True, slots=True) +class PublicGonolReceipt: + """Deterministic construction receipt for one EPAC Public Gonol.""" + + constructor_id: str + constructor_version: str + standing: str + selection_effect: str + source_id: str + gonol: ClosedPublicGonol + receipt_digest: str + structure: Mapping[str, Any] | None + nonclaims: tuple[str, ...] + hmmm: tuple[str, ...] + + +def _require_text(value: str, *, field: str) -> str: + if not isinstance(value, str) or not value or value.isspace(): + raise PublicGonolConstructionError(f"{field} must be exact non-empty text") + return value + + +def _identity_position(identity_glyph: str | None) -> tuple[str | None, int | None]: + if identity_glyph is None: + return (None, None) + if not isinstance(identity_glyph, str) or len(identity_glyph) != 1: + raise PublicGonolConstructionError( + "identity_glyph must be one admitted Public Gonol scalar or None" + ) + try: + position = public_gonol_function(identity_glyph) + except (TypeError, ValueError) as exc: + raise PublicGonolConstructionError(str(exc)) from exc + return (position.glyph, position.index) + + +def _geometry(identity_glyph: str | None, carrier_index: int | None) -> dict[str, Any]: + digest = public_gonol_sha256() + if digest != PINNED_PUBLIC_GONOL_SHA256: + raise PublicGonolConstructionError( + "UCNS Public Gonol digest mismatch: " + f"constructor pins {PINNED_PUBLIC_GONOL_SHA256}, computed {digest}" + ) + origin = native_mobius_state(0) + identity: dict[str, Any] | None = None + if identity_glyph is not None and carrier_index is not None: + identity = {"index": carrier_index, "glyph": identity_glyph} + return { + "state": "bound", + "authority": "ucns.public_gonol", + "authority_binding": "explicit", + "carrier_digest": digest, + "identity_position": identity, + "mobius_epsilon_t0": origin.frame.sign, + "position_operation": "hmmm", + } + + +def _freeze_json(value: Any) -> Any: + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, MappingABC): + return MappingProxyType({str(key): _freeze_json(item) for key, item in value.items()}) + if isinstance(value, SequenceABC) and not isinstance(value, (str, bytes)): + return tuple(_freeze_json(item) for item in value) + raise PublicGonolConstructionError(f"value is not JSON-stable: {type(value)!r}") + + +def _json_ready(value: Any) -> Any: + if isinstance(value, MappingABC): + return {str(key): _json_ready(item) for key, item in value.items()} + if isinstance(value, SequenceABC) and not isinstance(value, (str, bytes)): + return [_json_ready(item) for item in value] + return value + + +def _tuple_tree(value: Any) -> Any: + if isinstance(value, MappingABC): + return tuple(sorted((str(key), _tuple_tree(item)) for key, item in value.items())) + if isinstance(value, SequenceABC) and not isinstance(value, (str, bytes)): + return tuple(_tuple_tree(item) for item in value) + return value + + +def _coupling_signature(item: Mapping[str, Any]) -> tuple[Any, int, Any]: + declared = item.get("declared_ids", item.get("coupling")) + charge_state = item.get("charge_state") + if charge_state is None: + charge_state = (item.get("slot_charges"), item.get("mobius_epsilon_t0")) + return (_tuple_tree(declared), int(item.get("arity", -1)), _tuple_tree(charge_state)) + + +def _structure_part_signature(item: Mapping[str, Any]) -> tuple[Any, int, Any]: + return ( + _tuple_tree(item.get("coupling")), + int(item.get("arity", -1)), + _tuple_tree(item.get("charge_state")), + ) + + +def _validate_structure_matches_couplings( + couplings: Sequence[Mapping[str, Any]], + structure: Mapping[str, Any] | None, +) -> None: + if not couplings and structure is None: + return + if not couplings or structure is None: + raise PublicGonolConstructionError( + "couplings and structure must be supplied together" + ) + parts = structure.get("parts") + if not isinstance(parts, SequenceABC) or isinstance(parts, (str, bytes)): + raise PublicGonolConstructionError("structure parts must be a sequence") + expected = tuple(sorted((_coupling_signature(item) for item in couplings), key=repr)) + actual = tuple(sorted((_structure_part_signature(item) for item in parts), key=repr)) + if expected != actual: + raise PublicGonolConstructionError( + "structure must match the supplied declared couplings before closure" + ) + + +def _participant_payload(item: ClosedPublicGonol) -> dict[str, Any]: + return { + "source_id": item.source_id, + "occurrence": item.occurrence, + "relation": item.relation, + "identity_glyph": item.identity_glyph, + "carrier_index": item.carrier_index, + "atomic_id": item.atomic_id, + "receipt_digest": item.receipt_digest, + "geometry_digest": item.geometry_digest, + "carried_options": [list(pair) for pair in item.carried_options], + "couplings": _freeze_json(item.couplings), + "structure": _freeze_json(item.structure), + "participants": [_participant_payload(child) for child in item.participants], + } + + +def _atomic_payload( + *, + source_id: str, + occurrence: int, + relation: str, + identity_glyph: str | None, + carrier_index: int | None, + participants: tuple[ClosedPublicGonol, ...], + carried_options: tuple[tuple[str, str], ...], + couplings: tuple[Mapping[str, Any], ...], + structure: Mapping[str, Any] | None, +) -> dict[str, Any]: + return { + "constructor_id": CONSTRUCTOR_ID, + "constructor_version": CONSTRUCTOR_VERSION, + "standing": STANDING, + "selection_effect": SELECTION_EFFECT, + "source_id": source_id, + "occurrence": occurrence, + "relation": relation, + "identity_glyph": identity_glyph, + "carrier_index": carrier_index, + "participants": [_participant_payload(item) for item in participants], + "carried_options": [list(pair) for pair in carried_options], + "couplings": _freeze_json(couplings), + "structure": _freeze_json(structure), + "closure_invariant": "once closed, a gonol is atomic at any later participation", + } + + +def _receipt_payload( + *, + source_id: str, + gonol_payload: Mapping[str, Any], + geometry: Mapping[str, Any], + atomic_id: str, + geometry_digest: str, +) -> dict[str, Any]: + return { + "constructor_id": CONSTRUCTOR_ID, + "constructor_version": CONSTRUCTOR_VERSION, + "standing": STANDING, + "selection_effect": SELECTION_EFFECT, + "source_id": source_id, + "gonol": gonol_payload, + "atomic_id": atomic_id, + "geometry": _freeze_json(geometry), + "geometry_digest": geometry_digest, + "nonclaims": list(NONCLAIMS), + "hmmm": list(HMMM), + } + + +def canonical_receipt_bytes(payload: Mapping[str, Any]) -> bytes: + return json.dumps( + _json_ready(payload), + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _digest(payload: Mapping[str, Any]) -> str: + return sha256(canonical_receipt_bytes(payload)).hexdigest() + + +def construct_public_gonol( + *, + source_id: str, + relation: str, + participants: Sequence[ClosedPublicGonol] = (), + identity_glyph: str | None = None, + occurrence: int = 0, + carried_options: Sequence[tuple[str, str]] = (), + couplings: Sequence[Mapping[str, Any]] = (), + structure: Mapping[str, Any] | None = None, +) -> PublicGonolReceipt: + """Close one EPAC gonol on the UCNS Public Gonol carrier.""" + + source_id = _require_text(source_id, field="source_id") + relation = _require_text(relation, field="relation") + if isinstance(occurrence, bool) or not isinstance(occurrence, int) or occurrence < 0: + raise PublicGonolConstructionError("occurrence must be a non-negative int") + closed_participants = tuple(participants) + for item in closed_participants: + if not isinstance(item, ClosedPublicGonol): + raise PublicGonolConstructionError("participants must already be closed EPAC public gonols") + options = tuple( + ( + _require_text(key, field="carried option key"), + _require_text(value, field="carried option value"), + ) + for key, value in carried_options + ) + frozen_couplings = tuple(_freeze_json(item) for item in couplings) + frozen_structure = None if structure is None else _freeze_json(structure) + _validate_structure_matches_couplings(frozen_couplings, frozen_structure) + glyph, index = _identity_position(identity_glyph) + geometry = _geometry(glyph, index) + gonol_payload = _atomic_payload( + source_id=source_id, + occurrence=occurrence, + relation=relation, + identity_glyph=glyph, + carrier_index=index, + participants=closed_participants, + carried_options=options, + couplings=frozen_couplings, + structure=frozen_structure, + ) + atomic_id = _digest({"atomic": gonol_payload}) + geometry_digest = _digest({"geometry": geometry}) + receipt_payload = _receipt_payload( + source_id=source_id, + gonol_payload=gonol_payload, + geometry=geometry, + atomic_id=atomic_id, + geometry_digest=geometry_digest, + ) + receipt_digest = _digest(receipt_payload) + gonol = ClosedPublicGonol( + source_id=source_id, + occurrence=occurrence, + relation=relation, + identity_glyph=glyph, + carrier_index=index, + participants=closed_participants, + carried_options=options, + couplings=frozen_couplings, + structure=frozen_structure, + atomic_id=atomic_id, + receipt_digest=receipt_digest, + geometry_digest=geometry_digest, + ) + return PublicGonolReceipt( + constructor_id=CONSTRUCTOR_ID, + constructor_version=CONSTRUCTOR_VERSION, + standing=STANDING, + selection_effect=SELECTION_EFFECT, + source_id=source_id, + gonol=gonol, + receipt_digest=receipt_digest, + structure=frozen_structure, + nonclaims=NONCLAIMS, + hmmm=HMMM, + ) + + +def replay_public_gonol(receipt: PublicGonolReceipt) -> PublicGonolReceipt: + """Replay one receipt from its closed gonol. Reproduces construction identity.""" + + gonol = receipt.gonol + return construct_public_gonol( + source_id=gonol.source_id, + relation=gonol.relation, + participants=gonol.participants, + identity_glyph=gonol.identity_glyph, + occurrence=gonol.occurrence, + carried_options=gonol.carried_options, + couplings=gonol.couplings, + structure=gonol.structure, + ) + + +__all__ = [ + "CONSTRUCTOR_ID", + "CONSTRUCTOR_VERSION", + "ClosedPublicGonol", + "HMMM", + "NONCLAIMS", + "PINNED_PUBLIC_GONOL_SHA256", + "PublicGonolConstructionError", + "PublicGonolReceipt", + "canonical_receipt_bytes", + "construct_public_gonol", + "replay_public_gonol", +] diff --git a/subatomic/element_affixiation_candidate.py b/subatomic/element_affixiation_candidate.py new file mode 100644 index 0000000..8789fb6 --- /dev/null +++ b/subatomic/element_affixiation_candidate.py @@ -0,0 +1,268 @@ +"""Identity-only subatomic element affixiation candidate. + +This module implements the provisional baseline from +``subatomic-affixiation-baseline.md``: hydrogen, helium, lithium, and carbon +element-gonol candidates over the established UCNS carrier identity surfaces +(Public Gonol 157) and the native Möbius root-loop quotient, using the Möbius +turn index as the time-agnostic ordered parameter. + +It consumes exactly two UCNS public surfaces: + +- ``ucns.public_gonol_function`` for carrier identity positions; +- ``ucns.native_mobius_state`` for the established Möbius framing. + +No Public Gonol position operation is defined, inferred, or asserted here. +Status: CROSS-DOMAIN-HYPOTHESIS / provisional. Not org canon. + +Usage guidance: + + PYTHONPATH=/src python3 - <<'PY' + from element_affixiation_candidate import affixiate_element, replay_element + + he = affixiate_element("He") + print(he.receipt) + ok, replay_receipt = replay_element("He") + print("replay byte-identical:", ok and replay_receipt == he.receipt) + PY +""" + +# === MODULE_BUILD === +# id: epac_subatomic_element_affixiation_candidate +# module_name: element_affixiation_candidate +# module_kind: experiment +# summary: identity-only H/He/Li/C element-gonol candidates over established UCNS carrier identity and native Möbius framing; no position operation invented +# owner: The Interdependency +# public_surface: ISOTOPE_DEFAULTS, CONSTRUCTION_IDS, ElementCandidate, affixiate_element, replay_element, element_receipt +# internal_surface: _canonical_record, _t_states +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: subatomic.test_element_affixiation_candidate +# rollout: local candidate module under stack/research/epac/subatomic/ +# rollback: remove module, tests, and generated receipts +# requires: ucns_public_gonol_geometry, ucns_native_mobius_geometry +# since: 2026-08-22 +# unresolved: Public Gonol position operations; harmonic notation; isotope defaults are instance-resolved; epac canonical repository absent +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: candidate_uses_only_established_ucns_surfaces +# given: the candidate module is imported and executed +# then: only ucns.public_gonol_function and ucns.native_mobius_state are consumed; no position operation is defined, inferred, or called +# class: safety +# +# id: element_identity_positions_exact +# given: an element symbol with default isotope (Z, A) +# then: proton positions are exactly 1..Z and neutron positions are exactly Z+1..A on the 157-position carrier, as identity coordinates only +# class: correctness +# +# id: mobius_parameter_sequence_exact +# given: the Möbius turn index t in {0, 1, 2} is traversed +# then: visible phase is unchanged, the local frame sequence is POSITIVE -> REVERSED -> POSITIVE, and complete_key differs only at t=1 +# class: correctness +# +# id: receipt_deterministic_and_replayable +# given: the same element and the same pinned source identities +# then: the receipt is byte-identical across independent constructions +# class: correctness +# +# id: no_physics_or_canon_claim +# given: any constructed candidate +# then: status remains CROSS-DOMAIN-HYPOTHESIS and no empirical validity, theorem status, measurement validity, or canon promotion is claimed +# class: doctrine +# === END CONTRACTS === + +from __future__ import annotations + +from dataclasses import dataclass +from fractions import Fraction +import hashlib +import json + +from ucns import native_mobius_state, public_gonol_function + +SOURCE_COMMITS = { + "metapat": "34d954aa1e2092e615b03a180500f6b6977f501e", + "ucns": "1975fe70cf4e0826a8020c2da3047569e277af64", +} + +CONSTRUCTION_IDS = { + "relation": "metapat.affixiation_harmonics.affixiation", + "ordered_parameter": "ucns.native-mobius-turn-index", + "closure_scale": "epac.subatomic.atomic", + "status": "CROSS-DOMAIN-HYPOTHESIS", +} + +# Default isotope instances are instance-resolved, not canonical admission law. +# Extended to Z=1..26 (through iron) for the subatomic gonol program. +ISOTOPE_DEFAULTS = { + "H": (1, 1), "He": (2, 4), "Li": (3, 7), "Be": (4, 9), + "B": (5, 11), "C": (6, 12), "N": (7, 14), "O": (8, 16), + "F": (9, 19), "Ne": (10, 20), "Na": (11, 23), "Mg": (12, 24), + "Al": (13, 27), "Si": (14, 28), "P": (15, 31), "S": (16, 32), + "Cl": (17, 35), "Ar": (18, 40), "K": (19, 39), "Ca": (20, 40), + "Sc": (21, 45), "Ti": (22, 48), "V": (23, 51), "Cr": (24, 52), + "Mn": (25, 55), "Fe": (26, 56), +} + + +@dataclass(frozen=True, slots=True) +class ElementCandidate: + """One closed element-gonol candidate record with deterministic receipt.""" + + element_id: str + symbol: str + Z: int + A: int + proton_positions: tuple[int, ...] + proton_glyphs: tuple[str, ...] + neutron_positions: tuple[int, ...] + neutron_glyphs: tuple[str, ...] + t_states: tuple[dict, ...] + relation_id: str + ordered_parameter_id: str + closure_scale: str + source_commits: dict + status: str + receipt: str + + +def _t_states() -> tuple[dict, ...]: + """Traverse the Möbius turn index t in {0, 1, 2}. + + Uses only the established native Möbius root-loop quotient. Time is not + inserted: t is a declared ordered parameter, not physical time. + """ + states = [] + for t in (0, 1, 2): + state = native_mobius_state(Fraction(t)) + states.append( + { + "t": t, + "visible_key": [state.visible_key[0], str(state.visible_key[1])], + "complete_key": [ + state.complete_key[0], + str(state.complete_key[1]), + state.complete_key[2].value, + ], + "frame": state.frame.value, + } + ) + return tuple(states) + + +def _canonical_record( + element_id: str, + symbol: str, + Z: int, + A: int, + proton_positions: tuple[int, ...], + proton_glyphs: tuple[str, ...], + neutron_positions: tuple[int, ...], + neutron_glyphs: tuple[str, ...], +) -> dict: + return { + "element_id": element_id, + "symbol": symbol, + "Z": Z, + "A": A, + "proton_positions": list(proton_positions), + "proton_glyphs": list(proton_glyphs), + "neutron_positions": list(neutron_positions), + "neutron_glyphs": list(neutron_glyphs), + "relation_id": CONSTRUCTION_IDS["relation"], + "ordered_parameter_id": CONSTRUCTION_IDS["ordered_parameter"], + "t_states": list(_t_states()), + "closure_scale": CONSTRUCTION_IDS["closure_scale"], + "source_commits": SOURCE_COMMITS, + "status": CONSTRUCTION_IDS["status"], + } + + +def element_receipt(record: dict) -> str: + """SHA-256 over canonical JSON of the construction record.""" + payload = json.dumps(record, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + +def affixiate_element(symbol: str) -> ElementCandidate: + """Construct one element-gonol candidate from its default isotope instance. + + Raises ``ValueError`` for symbols outside the declared isotope defaults. + """ + if symbol not in ISOTOPE_DEFAULTS: + raise ValueError( + f"element {symbol!r} has no declared isotope default; " + f"declared: {sorted(ISOTOPE_DEFAULTS)}" + ) + Z, A = ISOTOPE_DEFAULTS[symbol] + proton_positions = tuple(range(1, Z + 1)) + neutron_positions = tuple(range(Z + 1, A + 1)) + + # Identity coordinates only. public_gonol_function resolves the exact + # carrier identity position; no operation is requested or inferred. + proton_glyphs = tuple(public_gonol_function(i).glyph for i in proton_positions) + neutron_glyphs = tuple(public_gonol_function(i).glyph for i in neutron_positions) + + record = _canonical_record( + element_id=f"epac.subatomic_affixiation.{symbol.lower()}", + symbol=symbol, + Z=Z, + A=A, + proton_positions=proton_positions, + proton_glyphs=proton_glyphs, + neutron_positions=neutron_positions, + neutron_glyphs=neutron_glyphs, + ) + receipt = element_receipt(record) + return ElementCandidate( + element_id=record["element_id"], + symbol=symbol, + Z=Z, + A=A, + proton_positions=proton_positions, + proton_glyphs=proton_glyphs, + neutron_positions=neutron_positions, + neutron_glyphs=neutron_glyphs, + t_states=record["t_states"], + relation_id=record["relation_id"], + ordered_parameter_id=record["ordered_parameter_id"], + closure_scale=record["closure_scale"], + source_commits=SOURCE_COMMITS, + status=record["status"], + receipt=receipt, + ) + + +def replay_element(symbol: str) -> tuple[bool, str]: + """Independently reconstruct and compare the receipt. + + Returns ``(matches, receipt)``. Replay establishes reproducibility of the + declared construction only — not geometry, physics, or measurement. + """ + candidate = affixiate_element(symbol) + record = _canonical_record( + element_id=candidate.element_id, + symbol=candidate.symbol, + Z=candidate.Z, + A=candidate.A, + proton_positions=candidate.proton_positions, + proton_glyphs=candidate.proton_glyphs, + neutron_positions=candidate.neutron_positions, + neutron_glyphs=candidate.neutron_glyphs, + ) + replay_receipt = element_receipt(record) + return (replay_receipt == candidate.receipt, replay_receipt) + + +__all__ = [ + "CONSTRUCTION_IDS", + "ElementCandidate", + "ISOTOPE_DEFAULTS", + "SOURCE_COMMITS", + "affixiate_element", + "element_receipt", + "replay_element", +] diff --git a/subatomic/extended_atomic.py b/subatomic/extended_atomic.py new file mode 100644 index 0000000..c5db19e --- /dev/null +++ b/subatomic/extended_atomic.py @@ -0,0 +1,236 @@ +"""Extended atomic quantum layer Z=1..26 for subatomic gonols. + +Delegates Z<=18 to ``epac_atomic`` (byte-identical electron records, so +existing H/He/Li/C receipts do not move). Adds Z=19..26 from declared +ground-state configurations with a standard Aufbau extension through 4s/3d and +a Slater-screening extension for d electrons. + +Candidate rules declared here (consistent with the sibling ``epac_atomic``): + +- valence electrons are those with ``n == max occupied n``; +- angular identities are hydrogenic ``Y_l{l}_m{m_l}`` labels; +- Slater screening: same-shell 0.35 (same-group), n-1 shell 0.85, deeper 1.00; + for d electrons (l=2) all inner shells count 1.00. + +Status: application-layer candidate data. Not physics canon. + +Usage guidance: + + from extended_atomic import atomic_record, iter_table + + iron = atomic_record(26) + print(iron.symbol, iron.configuration) +""" + +# === MODULE_BUILD === +# id: epac_subatomic_extended_atomic +# module_name: extended_atomic +# module_kind: schema +# summary: atomic quantum-layer records Z=1..26 for subatomic gonols; Z<=18 delegates to epac_atomic, Z=19..26 from declared ground-state configurations with Aufbau/Slater extension +# owner: The Interdependency +# public_surface: EXTENDED_SYMBOLS, SYMBOL_TO_Z, atomic_record, iter_table +# internal_surface: _config_occupancy, _fill_from_config, _slater_zeff_extended +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: subatomic.test_extended_atomic +# rollout: local candidate module under stack/research/epac/subatomic/ +# rollback: remove module; subatomic_gonol returns to Z<=18 epac_atomic delegation +# requires: epac_atomic +# since: 2026-08-22 +# unresolved: configurations beyond Z=26; full f-block Aufbau; Slater rules are candidate extensions, not exact physics +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: extended_atomic_preserves_z_le_18 +# given: atomic_record(Z) for 1 <= Z <= 18 +# then: the record is byte-identical to epac_atomic.atomic_record(Z) +# class: correctness +# +# id: extended_atomic_uses_declared_configurations +# given: atomic_record(Z) for 19 <= Z <= 26 +# then: electron occupancy matches the declared ground-state configuration, including the Cr 4s1.3d5 exception +# class: correctness +# +# id: extended_atomic_stays_candidate +# given: any extended record +# then: values are candidate application-layer data, not physics validation +# class: doctrine +# === END CONTRACTS === + +from __future__ import annotations + +from epac_atomic import ( + AtomicRecord, + ElectronState, + atomic_record as base_atomic_record, +) + +EXTENDED_SYMBOLS: tuple[str, ...] = ( + "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", + "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", + "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", +) +SYMBOL_TO_Z: dict[str, int] = {symbol: index + 1 for index, symbol in enumerate(EXTENDED_SYMBOLS)} + +ISOTOPE_DEFAULTS_19_26: dict[int, int] = { + 19: 39, 20: 40, 21: 45, 22: 48, 23: 51, 24: 52, 25: 55, 26: 56, +} + +PERIOD_GROUP_19_26: dict[int, tuple[int, int]] = { + 19: (4, 1), 20: (4, 2), 21: (4, 3), 22: (4, 4), + 23: (4, 5), 24: (4, 6), 25: (4, 7), 26: (4, 8), +} + +# Declared ground-state configurations (standard Aufbau with the Cr exception). +CONFIGURATIONS_19_26: dict[int, str] = { + 19: "1s2.2s2.2p6.3s2.3p6.4s1", + 20: "1s2.2s2.2p6.3s2.3p6.4s2", + 21: "1s2.2s2.2p6.3s2.3p6.4s2.3d1", + 22: "1s2.2s2.2p6.3s2.3p6.4s2.3d2", + 23: "1s2.2s2.2p6.3s2.3p6.4s2.3d3", + 24: "1s2.2s2.2p6.3s2.3p6.4s1.3d5", + 25: "1s2.2s2.2p6.3s2.3p6.4s2.3d5", + 26: "1s2.2s2.2p6.3s2.3p6.4s2.3d6", +} + +_SUBSHELL_NAME = "spdf" + + +def _ml_down(l: int) -> tuple[int, ...]: + return tuple(range(l, -l - 1, -1)) + + +def _config_occupancy(config: str) -> list[tuple[int, int, int]]: + """Parse ``1s2.2s2...`` into ordered (n, l, count) entries.""" + entries: list[tuple[int, int, int]] = [] + for part in config.split("."): + part = part.strip() + n = int(part[0]) + l = _SUBSHELL_NAME.index(part[1]) + count = int(part[2:]) + entries.append((n, l, count)) + return entries + + +def _slater_zeff_extended( + Z: int, n: int, l: int, occupied: tuple[tuple[int, int], ...] +) -> float: + """Slater screening, extended for 4s/3d while matching epac_atomic for l<=1.""" + others = list(occupied) + others.remove((n, l)) + sigma = 0.0 + same_group = 0 + for on, ol in others: + if n == 1 and l == 0: + if on == 1 and ol == 0: + sigma += 0.30 + continue + if l == 2: + # d electron: same subshell 0.35, all inner shells 1.00. + if on == n and ol == l: + same_group += 1 + elif on < n: + sigma += 1.00 + continue + if on == n and ((l in {0, 1} and ol in {0, 1}) or ol == l): + same_group += 1 + elif on == n - 1: + sigma += 0.85 + elif on <= n - 2: + sigma += 1.00 + sigma += 0.35 * same_group + return round(Z - sigma, 3) + + +def _fill_from_config(Z: int, config: str) -> tuple[ElectronState, ...]: + occupancy = _config_occupancy(config) + raw: list[tuple[int, int, int, int]] = [] + occupied_pairs: list[tuple[int, int]] = [] + for n, l, count in occupancy: + slots = [(m_l, 1) for m_l in _ml_down(l)] + [(m_l, -1) for m_l in _ml_down(l)] + for m_l, m_s in slots[:count]: + raw.append((n, l, m_l, m_s)) + occupied_pairs.append((n, l)) + valence_n = max(n for n, _l, _ml, _ms in raw) + occupied = tuple(occupied_pairs) + occupancy_counts: dict[tuple[int, int, int], int] = {} + for n, l, m_l, _m_s in raw: + key = (n, l, m_l) + occupancy_counts[key] = occupancy_counts.get(key, 0) + 1 + electrons: list[ElectronState] = [] + for index, (n, l, m_l, m_s) in enumerate(raw): + z_eff = _slater_zeff_extended(Z, n, l, occupied) + energy = round(-(z_eff**2) / (n**2), 6) + electrons.append( + ElectronState( + index=index, + n=n, + l=l, + m_l=m_l, + m_s=m_s, + shell=f"n{n}", + subshell=f"{n}{_SUBSHELL_NAME[l]}", + angular_id=f"Y_l{l}_m{m_l}", + radial_nodes=n - l - 1, + z_eff=str(z_eff), + e_rydberg=str(energy), + valence=(n == valence_n), + paired=occupancy_counts[(n, l, m_l)] == 2, + ) + ) + return tuple(electrons) + + +def _configuration_string(electrons: tuple[ElectronState, ...]) -> str: + counts: dict[str, int] = {} + order: list[str] = [] + for electron in electrons: + name = electron.subshell + if name not in counts: + order.append(name) + counts[name] = 0 + counts[name] += 1 + return ".".join(f"{name}{counts[name]}" for name in order) + + +def atomic_record(Z: int) -> AtomicRecord: + if not 1 <= Z <= 26: + raise ValueError("extended atomic table is Z=1..26") + if Z <= 18: + return base_atomic_record(Z) + electrons = _fill_from_config(Z, CONFIGURATIONS_19_26[Z]) + period, group = PERIOD_GROUP_19_26[Z] + A = ISOTOPE_DEFAULTS_19_26[Z] + unpaired = tuple(e for e in electrons if e.valence and not e.paired and e.m_s == 1) + return AtomicRecord( + Z=Z, + symbol=EXTENDED_SYMBOLS[Z - 1], + period=period, + group=group, + A=A, + proton_count=Z, + neutron_count=A - Z, + electrons=electrons, + configuration=_configuration_string(electrons), + valence_n=max(e.n for e in electrons), + valence_electrons=sum(1 for e in electrons if e.valence), + unpaired_valence=unpaired, + promoted_unpaired_valence=(), + ) + + +def iter_table(): + for Z in range(1, 27): + yield atomic_record(Z) + + +__all__ = [ + "EXTENDED_SYMBOLS", + "ISOTOPE_DEFAULTS_19_26", + "SYMBOL_TO_Z", + "atomic_record", + "iter_table", +] diff --git a/subatomic/nuclear_harmonic_candidates.py b/subatomic/nuclear_harmonic_candidates.py new file mode 100644 index 0000000..03c33ec --- /dev/null +++ b/subatomic/nuclear_harmonic_candidates.py @@ -0,0 +1,328 @@ +"""Physically sourced nuclear harmonic-relation candidates (H -> He -> Li/C). + +This module applies current METAPAT harmonic semantics — repeatable +commensurability, ratio, symmetry, inversion, phase relation, or recurrence +mapping — to physically sourced nuclear states of H-1/H-2, He-4, Li-7, and +C-12. It does NOT wait for a UCNS harmonic notation and it does NOT invent +Public Gonol position operations or unsourced phase. + +Every candidate record declares the six METAPAT evidence fields: + + participants, ordered parameter, recurrence mapping, + equivalence condition, information loss, physical provenance. + +Ordered parameters are nucleon-content sequences (A, Z), which are +time-agnostic. No temporal phase is introduced. + +Status: CROSS-DOMAIN-HYPOTHESIS / hmmm. No physics claim is advanced beyond +the cited nuclear data and declared candidate mappings. + +Usage guidance: + + python3 - <<'PY' + from nuclear_harmonic_candidates import CANDIDATES, recurrence_test + + for candidate in CANDIDATES: + print(candidate.candidate_id, candidate.receipt) + for candidate in CANDIDATES: + print(candidate.candidate_id, recurrence_test(candidate)) + PY +""" + +# === MODULE_BUILD === +# id: epac_subatomic_nuclear_harmonic_candidates +# module_name: nuclear_harmonic_candidates +# module_kind: experiment +# summary: physically sourced H/He/Li/C nuclear harmonic-relation candidates over METAPAT harmonic semantics with declared recurrence mappings and provenance +# owner: The Interdependency +# public_surface: NUCLIDE_FACTS, CANDIDATES, HarmonicCandidate, recurrence_test, harmonic_receipt +# internal_surface: _canonical_record +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: subatomic.test_nuclear_harmonic_candidates +# rollout: local candidate module under stack/research/epac/subatomic/ +# rollback: remove module, tests, and generated receipts +# requires: none (pure stdlib; METAPAT semantics consumed as documented doctrine, not imported code) +# since: 2026-08-22 +# unresolved: UCNS harmonic notation; exact alpha-cluster citations; approximate isospin symmetry ignores Coulomb effects +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: every_harmonic_candidate_declares_six_evidence_fields +# given: any harmonic candidate record +# then: participants, ordered_parameter, recurrence_mapping, equivalence_condition, information_loss, and physical_provenance are all non-empty and source-declared +# class: doctrine +# +# id: harmonic_parameter_is_time_agnostic +# given: any harmonic candidate ordered parameter +# then: the parameter is an explicitly declared non-temporal sequence (nucleon content A, Z), never an unsourced phase or time +# class: doctrine +# +# id: no_public_gonol_position_operation_invented +# given: the harmonic candidate module is imported +# then: no Public Gonol position operation is defined, inferred, or asserted +# class: safety +# +# id: recurrence_test_is_deterministic +# given: the same candidate record and the same declared equivalence condition +# then: recurrence_test returns the same boolean and the receipt is byte-identical across independent constructions +# class: correctness +# +# id: all_results_remain_cross_domain_hypothesis +# given: any candidate or recurrence result +# then: status remains CROSS-DOMAIN-HYPOTHESIS / hmmm and no physics validation, canon promotion, or theorem status is claimed +# class: doctrine +# === END CONTRACTS === + +from __future__ import annotations + +from dataclasses import dataclass, field +import hashlib +import json + +# Physically sourced nuclear facts. Provenance: compiled nuclear data +# (NNDC/AME-style ground-state table); values web-pinned 2026-08-22. +NUCLIDE_FACTS = { + "H-1": { + "Z": 1, "A": 1, "N": 0, "J_pi": "1/2+", + "BE_total_MeV": 0.0, "BE_per_A_MeV": 0.0, + "provenance": "compiled nuclear data; web-pinned 2026-08-22", + }, + "H-2": { + "Z": 1, "A": 2, "N": 1, "J_pi": "1+", + "BE_total_MeV": 2.22, "BE_per_A_MeV": 1.11, + "provenance": "compiled nuclear data; web-pinned 2026-08-22", + }, + "He-4": { + "Z": 2, "A": 4, "N": 2, "J_pi": "0+", + "BE_total_MeV": 28.3, "BE_per_A_MeV": 7.07, + "provenance": "compiled nuclear data; web-pinned 2026-08-22", + }, + "Li-7": { + "Z": 3, "A": 7, "N": 4, "J_pi": "3/2-", + "BE_total_MeV": 39.2, "BE_per_A_MeV": 5.6, + "provenance": "compiled nuclear data; web-pinned 2026-08-22", + }, + "C-12": { + "Z": 6, "A": 12, "N": 6, "J_pi": "0+", + "BE_total_MeV": 92.2, "BE_per_A_MeV": 7.68, + "provenance": "compiled nuclear data; web-pinned 2026-08-22", + }, +} + +ORDERED_PARAMETER = { + "kind": "nucleon-content-sequence", + "declaration": "ordered by increasing (A, Z): H-1, H-2, He-4, Li-7, C-12", + "time_agnostic": True, +} + + +@dataclass(frozen=True, slots=True) +class HarmonicCandidate: + """One harmonic-relation candidate with the six METAPAT evidence fields.""" + + candidate_id: str + relation_kind: str + participants: tuple[str, ...] + ordered_parameter: dict + recurrence_mapping: str + equivalence_condition: str + information_loss: str + physical_provenance: tuple[str, ...] + status: str = "CROSS-DOMAIN-HYPOTHESIS" + receipt: str = field(default="") + + +def harmonic_receipt(record: dict) -> str: + payload = json.dumps(record, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + +def _canonical_record(candidate: HarmonicCandidate) -> dict: + return { + "candidate_id": candidate.candidate_id, + "relation_kind": candidate.relation_kind, + "participants": list(candidate.participants), + "ordered_parameter": candidate.ordered_parameter, + "recurrence_mapping": candidate.recurrence_mapping, + "equivalence_condition": candidate.equivalence_condition, + "information_loss": candidate.information_loss, + "physical_provenance": list(candidate.physical_provenance), + "status": candidate.status, + } + + +def _seal(candidate: HarmonicCandidate) -> HarmonicCandidate: + record = _canonical_record(candidate) + receipt = harmonic_receipt(record) + return HarmonicCandidate( + candidate_id=candidate.candidate_id, + relation_kind=candidate.relation_kind, + participants=candidate.participants, + ordered_parameter=candidate.ordered_parameter, + recurrence_mapping=candidate.recurrence_mapping, + equivalence_condition=candidate.equivalence_condition, + information_loss=candidate.information_loss, + physical_provenance=candidate.physical_provenance, + status=candidate.status, + receipt=receipt, + ) + + +CANDIDATES = ( + _seal(HarmonicCandidate( + candidate_id="alpha_cluster_recurrence", + relation_kind="recurrence", + participants=("He-4", "Li-7", "C-12"), + ordered_parameter=ORDERED_PARAMETER, + recurrence_mapping=( + "The closed-shell He-4 cluster (2p2n, J^pi=0+, doubly magic) recurs " + "as a constituent: Li-7 ~ alpha + triton; C-12 ~ 3 x alpha " + "(3-alpha cluster model; Hoyle 0+ state near 7.65 MeV excitation)." + ), + equivalence_condition=( + "constituent decomposition contains one or more He-4 closed-shell " + "clusters, each 2p2n with J^pi=0+; equivalence is cluster " + "decomposition, not full state equality." + ), + information_loss=( + "excited-state spectrum, cluster relative motion, and non-alpha " + "constituents (triton, deuteron) are reduced to cluster labels." + ), + physical_provenance=( + "standard nuclear cluster models; Hoyle (1954) prediction of the " + "C-12 7.65 MeV 0+ state", + "hmmm: exact literature citation not web-pinned this session", + ), + )), + _seal(HarmonicCandidate( + candidate_id="n_z_ratio_commensurability", + relation_kind="ratio", + participants=("H-1", "H-2", "He-4", "Li-7", "C-12"), + ordered_parameter=ORDERED_PARAMETER, + recurrence_mapping=( + "Neutron/proton ratio N/Z as an exact rational: H-1 0/1, H-2 1/1, " + "He-4 2/2 = 1, Li-7 4/3, C-12 6/6 = 1. The value N/Z = 1 recurs " + "for the even-even N=Z nuclei He-4 and C-12." + ), + equivalence_condition="N/Z == 1 exactly (rational equality).", + information_loss=( + "reduces each nuclide to its (N, Z) pair; drops spin, excitation " + "spectrum, and binding energy." + ), + physical_provenance=( + "nuclide chart (N, Z) counts; standard nuclear data", + "compiled nuclear data; web-pinned 2026-08-22", + ), + )), + _seal(HarmonicCandidate( + candidate_id="ground_state_spin_parity_symmetry", + relation_kind="symmetry", + participants=("H-1", "H-2", "He-4", "Li-7", "C-12"), + ordered_parameter=ORDERED_PARAMETER, + recurrence_mapping=( + "Ground-state spin-parity J^pi: H-1 1/2+, H-2 1+, He-4 0+, " + "Li-7 3/2-, C-12 0+. The value 0+ recurs for even-even, " + "paired, closed-shell nuclei He-4 and C-12; odd-mass nuclei take " + "half-integer spins." + ), + equivalence_condition='J^pi == "0+" for the even-even symmetry class.', + information_loss=( + "drops excited states, magnetic moments, and full level schemes." + ), + physical_provenance=( + "compiled nuclear data; web-pinned 2026-08-22", + ), + )), + _seal(HarmonicCandidate( + candidate_id="binding_per_nucleon_commensurability", + relation_kind="commensurability", + participants=("H-2", "He-4", "Li-7", "C-12"), + ordered_parameter=ORDERED_PARAMETER, + recurrence_mapping=( + "Binding energy per nucleon (MeV): H-2 1.11, He-4 7.07, Li-7 5.6, " + "C-12 7.68. He-4 and C-12 are commensurable within a declared " + "10% tolerance; Li-7 dips, reproducing the even-even peak / " + "odd-mass dip recurrence of the light-nucleus binding curve." + ), + equivalence_condition=( + "|BE/A(x) - BE/A(He-4)| / BE/A(He-4) <= 0.10 (declared tolerance)." + ), + information_loss=( + "scalar reduction of the full binding relation; per METAPAT " + "theory.5 this candidate is read together with the complete " + "(Z, N, A) relation, not as one scalar difference alone." + ), + physical_provenance=( + "compiled nuclear data; web-pinned 2026-08-22", + ), + )), + _seal(HarmonicCandidate( + candidate_id="proton_neutron_inversion_symmetry", + relation_kind="inversion", + participants=("He-4", "C-12"), + ordered_parameter=ORDERED_PARAMETER, + recurrence_mapping=( + "Proton <-> neutron inversion (isospin mirror symmetry): N=Z " + "nuclei He-4 and C-12 map to themselves under p <-> n exchange. " + "H-1 inverts to the free neutron, which is unbound — a declared " + "asymmetry, not a phase." + ), + equivalence_condition="N == Z (self-mirror under p <-> n exchange).", + information_loss=( + "ignores Coulomb/electromagnetic effects; isospin symmetry is " + "approximate, not exact." + ), + physical_provenance=( + "isospin symmetry; standard nuclear physics (Wigner)", + "hmmm: exact citation not web-pinned this session", + ), + )), +) + + +def recurrence_test(candidate: HarmonicCandidate) -> dict: + """Test whether the declared equivalence condition recurs in Li-7 and C-12. + + Returns ``{"Li-7": bool, "C-12": bool}``. Declared, source-bound outcome + mapping. This is not a physics validation. + """ + he4 = NUCLIDE_FACTS["He-4"] + li7 = NUCLIDE_FACTS["Li-7"] + c12 = NUCLIDE_FACTS["C-12"] + + def be_a_deviation(facts: dict) -> float: + return abs(facts["BE_per_A_MeV"] - he4["BE_per_A_MeV"]) / he4["BE_per_A_MeV"] + + if candidate.candidate_id == "alpha_cluster_recurrence": + # Li-7 = alpha + triton; C-12 = 3 x alpha. Survives both. + return {"Li-7": True, "C-12": True} + if candidate.candidate_id == "n_z_ratio_commensurability": + # N/Z == 1: Li-7 is 4/3 (no); C-12 is 6/6 (yes). + return {"Li-7": li7["N"] == li7["Z"], "C-12": c12["N"] == c12["Z"]} + if candidate.candidate_id == "ground_state_spin_parity_symmetry": + # J^pi == 0+: Li-7 is 3/2- (no); C-12 is 0+ (yes). + return {"Li-7": li7["J_pi"] == "0+", "C-12": c12["J_pi"] == "0+"} + if candidate.candidate_id == "binding_per_nucleon_commensurability": + tolerance = 0.10 + return { + "Li-7": be_a_deviation(li7) <= tolerance, + "C-12": be_a_deviation(c12) <= tolerance, + } + if candidate.candidate_id == "proton_neutron_inversion_symmetry": + # N == Z self-mirror: Li-7 (4/3) no; C-12 (6/6) yes. + return {"Li-7": li7["N"] == li7["Z"], "C-12": c12["N"] == c12["Z"]} + raise ValueError(f"no declared recurrence test for {candidate.candidate_id!r}") + + +__all__ = [ + "CANDIDATES", + "HarmonicCandidate", + "NUCLIDE_FACTS", + "ORDERED_PARAMETER", + "harmonic_receipt", + "recurrence_test", +] diff --git a/subatomic/receipts/c.json b/subatomic/receipts/c.json new file mode 100644 index 0000000..27b55d2 --- /dev/null +++ b/subatomic/receipts/c.json @@ -0,0 +1,88 @@ +{ + "A": 12, + "Z": 6, + "closure_scale": "epac.subatomic.atomic", + "element_id": "epac.subatomic_affixiation.c", + "neutron_glyphs": [ + "C", + "%", + "(", + "D", + "&", + "'" + ], + "neutron_positions": [ + 7, + 8, + 9, + 10, + 11, + 12 + ], + "ordered_parameter_id": "ucns.native-mobius-turn-index", + "proton_glyphs": [ + "A", + "!", + "\"", + "B", + "#", + "$" + ], + "proton_positions": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "receipt": "a4026f197d6a0425b4ea5b3ff72d09d49fd159d5f59440480b5f97793b64cdc6", + "relation_id": "metapat.affixiation_harmonics.affixiation", + "source_commits": { + "metapat": "34d954aa1e2092e615b03a180500f6b6977f501e", + "ucns": "1975fe70cf4e0826a8020c2da3047569e277af64" + }, + "status": "CROSS-DOMAIN-HYPOTHESIS", + "symbol": "C", + "t_states": [ + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 0, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "reversed-local-frame" + ], + "frame": "reversed-local-frame", + "t": 1, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 2, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + } + ] +} diff --git a/subatomic/receipts/gonol_c.json b/subatomic/receipts/gonol_c.json new file mode 100644 index 0000000..5c4b13c --- /dev/null +++ b/subatomic/receipts/gonol_c.json @@ -0,0 +1,66 @@ +{ + "atomic_id": "4ca1dcad3d5620a2b4637db0caaade65d9681d20de556c7bca30d89a8099dd03", + "carried_options": [ + [ + "Z", + "6" + ], + [ + "period", + "2" + ], + [ + "group", + "14" + ], + [ + "A", + "12" + ], + [ + "electron-configuration", + "1s2.2s2.2p2" + ], + [ + "valence-electrons", + "4" + ], + [ + "harmonic-surviving", + "alpha_cluster_recurrence,n_z_ratio_commensurability,ground_state_spin_parity_symmetry,binding_per_nucleon_commensurability,proton_neutron_inversion_symmetry" + ], + [ + "status", + "CROSS-DOMAIN-HYPOTHESIS" + ] + ], + "constructor_id": "edcm.gonol", + "constructor_version": "v1", + "hmmm": [ + "exact UCNS geometric operation of each Public Gonol function position", + "UCNS Mobius-carrier affixiation/coupling law", + "which scales and relations, if any, are later selected", + "source-supported complete English morphology law" + ], + "nonclaims": [ + "not selected canon", + "not EDCM measurement validity", + "not a mandatory character-word-definition-recursive ladder", + "not complete English morphology law", + "not a UCNS geometric function operation", + "not a UCNS Mobius coupling law", + "not METAPAT canon promotion" + ], + "participant_kinds": [ + "nucleus", + "shell", + "shell" + ], + "receipt_digest": "f951b64828d67219ea4430cd5e2e4f4607e8762b5ec3e0185ac54e5f45f0e368", + "relation": "epac.subatomic.element", + "replay_digest": "f951b64828d67219ea4430cd5e2e4f4607e8762b5ec3e0185ac54e5f45f0e368", + "scale": "word", + "selection_effect": "none", + "source_id": "epac.subatomic.element:C#0", + "standing": "implemented-candidate" +} diff --git a/subatomic/receipts/gonol_h.json b/subatomic/receipts/gonol_h.json new file mode 100644 index 0000000..bdf380e --- /dev/null +++ b/subatomic/receipts/gonol_h.json @@ -0,0 +1,65 @@ +{ + "atomic_id": "0b6a2be5b2527e79a4243a7956fc6af74007c3bd05eb5671131aeb44f3b78e22", + "carried_options": [ + [ + "Z", + "1" + ], + [ + "period", + "1" + ], + [ + "group", + "1" + ], + [ + "A", + "1" + ], + [ + "electron-configuration", + "1s1" + ], + [ + "valence-electrons", + "1" + ], + [ + "harmonic-surviving", + "n_z_ratio_commensurability,ground_state_spin_parity_symmetry,binding_per_nucleon_commensurability" + ], + [ + "status", + "CROSS-DOMAIN-HYPOTHESIS" + ] + ], + "constructor_id": "edcm.gonol", + "constructor_version": "v1", + "hmmm": [ + "exact UCNS geometric operation of each Public Gonol function position", + "UCNS Mobius-carrier affixiation/coupling law", + "which scales and relations, if any, are later selected", + "source-supported complete English morphology law" + ], + "nonclaims": [ + "not selected canon", + "not EDCM measurement validity", + "not a mandatory character-word-definition-recursive ladder", + "not complete English morphology law", + "not a UCNS geometric function operation", + "not a UCNS Mobius coupling law", + "not METAPAT canon promotion" + ], + "participant_kinds": [ + "nucleus", + "shell" + ], + "receipt_digest": "3191f743f47ff9af8539cf73c59c070dee402d70131cb36a004ed8a3f8cbc22b", + "relation": "epac.subatomic.element", + "replay_digest": "3191f743f47ff9af8539cf73c59c070dee402d70131cb36a004ed8a3f8cbc22b", + "scale": "word", + "selection_effect": "none", + "source_id": "epac.subatomic.element:H#0", + "standing": "implemented-candidate" +} diff --git a/subatomic/receipts/gonol_he.json b/subatomic/receipts/gonol_he.json new file mode 100644 index 0000000..05e023f --- /dev/null +++ b/subatomic/receipts/gonol_he.json @@ -0,0 +1,65 @@ +{ + "atomic_id": "5193c57cefb3439d1c3bda35b221b19bd7f83975064661ae9409c90656df6822", + "carried_options": [ + [ + "Z", + "2" + ], + [ + "period", + "1" + ], + [ + "group", + "18" + ], + [ + "A", + "4" + ], + [ + "electron-configuration", + "1s2" + ], + [ + "valence-electrons", + "2" + ], + [ + "harmonic-surviving", + "alpha_cluster_recurrence,n_z_ratio_commensurability,ground_state_spin_parity_symmetry,binding_per_nucleon_commensurability,proton_neutron_inversion_symmetry" + ], + [ + "status", + "CROSS-DOMAIN-HYPOTHESIS" + ] + ], + "constructor_id": "edcm.gonol", + "constructor_version": "v1", + "hmmm": [ + "exact UCNS geometric operation of each Public Gonol function position", + "UCNS Mobius-carrier affixiation/coupling law", + "which scales and relations, if any, are later selected", + "source-supported complete English morphology law" + ], + "nonclaims": [ + "not selected canon", + "not EDCM measurement validity", + "not a mandatory character-word-definition-recursive ladder", + "not complete English morphology law", + "not a UCNS geometric function operation", + "not a UCNS Mobius coupling law", + "not METAPAT canon promotion" + ], + "participant_kinds": [ + "nucleus", + "shell" + ], + "receipt_digest": "37991f4b18442f456643165df94d8085c1d935bc057d8a5456f929272c6c3c37", + "relation": "epac.subatomic.element", + "replay_digest": "37991f4b18442f456643165df94d8085c1d935bc057d8a5456f929272c6c3c37", + "scale": "word", + "selection_effect": "none", + "source_id": "epac.subatomic.element:He#0", + "standing": "implemented-candidate" +} diff --git a/subatomic/receipts/gonol_li.json b/subatomic/receipts/gonol_li.json new file mode 100644 index 0000000..242314b --- /dev/null +++ b/subatomic/receipts/gonol_li.json @@ -0,0 +1,66 @@ +{ + "atomic_id": "f05ba72b602bc646a82fbf516ddd01fef0dc5d7eadd3689e9323d40871bb551d", + "carried_options": [ + [ + "Z", + "3" + ], + [ + "period", + "2" + ], + [ + "group", + "1" + ], + [ + "A", + "7" + ], + [ + "electron-configuration", + "1s2.2s1" + ], + [ + "valence-electrons", + "1" + ], + [ + "harmonic-surviving", + "alpha_cluster_recurrence,n_z_ratio_commensurability,ground_state_spin_parity_symmetry,binding_per_nucleon_commensurability" + ], + [ + "status", + "CROSS-DOMAIN-HYPOTHESIS" + ] + ], + "constructor_id": "edcm.gonol", + "constructor_version": "v1", + "hmmm": [ + "exact UCNS geometric operation of each Public Gonol function position", + "UCNS Mobius-carrier affixiation/coupling law", + "which scales and relations, if any, are later selected", + "source-supported complete English morphology law" + ], + "nonclaims": [ + "not selected canon", + "not EDCM measurement validity", + "not a mandatory character-word-definition-recursive ladder", + "not complete English morphology law", + "not a UCNS geometric function operation", + "not a UCNS Mobius coupling law", + "not METAPAT canon promotion" + ], + "participant_kinds": [ + "nucleus", + "shell", + "shell" + ], + "receipt_digest": "ff23abd7f8230a1fefeb397f373a7af1cd0279383dbc7166d9305cadf9312c95", + "relation": "epac.subatomic.element", + "replay_digest": "ff23abd7f8230a1fefeb397f373a7af1cd0279383dbc7166d9305cadf9312c95", + "scale": "word", + "selection_effect": "none", + "source_id": "epac.subatomic.element:Li#0", + "standing": "implemented-candidate" +} diff --git a/subatomic/receipts/h.json b/subatomic/receipts/h.json new file mode 100644 index 0000000..bbefe36 --- /dev/null +++ b/subatomic/receipts/h.json @@ -0,0 +1,64 @@ +{ + "A": 1, + "Z": 1, + "closure_scale": "epac.subatomic.atomic", + "element_id": "epac.subatomic_affixiation.h", + "neutron_glyphs": [], + "neutron_positions": [], + "ordered_parameter_id": "ucns.native-mobius-turn-index", + "proton_glyphs": [ + "A" + ], + "proton_positions": [ + 1 + ], + "receipt": "be411f204e10c14ac42b2983677f6b22a02d1cb6c4b158bf2026b0b6e88ca3da", + "relation_id": "metapat.affixiation_harmonics.affixiation", + "source_commits": { + "metapat": "34d954aa1e2092e615b03a180500f6b6977f501e", + "ucns": "1975fe70cf4e0826a8020c2da3047569e277af64" + }, + "status": "CROSS-DOMAIN-HYPOTHESIS", + "symbol": "H", + "t_states": [ + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 0, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "reversed-local-frame" + ], + "frame": "reversed-local-frame", + "t": 1, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 2, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + } + ] +} diff --git a/subatomic/receipts/harmonic_alpha_cluster_recurrence.json b/subatomic/receipts/harmonic_alpha_cluster_recurrence.json new file mode 100644 index 0000000..6a1007d --- /dev/null +++ b/subatomic/receipts/harmonic_alpha_cluster_recurrence.json @@ -0,0 +1,27 @@ +{ + "candidate_id": "alpha_cluster_recurrence", + "equivalence_condition": "constituent decomposition contains one or more He-4 closed-shell clusters, each 2p2n with J^pi=0+; equivalence is cluster decomposition, not full state equality.", + "information_loss": "excited-state spectrum, cluster relative motion, and non-alpha constituents (triton, deuteron) are reduced to cluster labels.", + "ordered_parameter": { + "declaration": "ordered by increasing (A, Z): H-1, H-2, He-4, Li-7, C-12", + "kind": "nucleon-content-sequence", + "time_agnostic": true + }, + "participants": [ + "He-4", + "Li-7", + "C-12" + ], + "physical_provenance": [ + "standard nuclear cluster models; Hoyle (1954) prediction of the C-12 7.65 MeV 0+ state", + "hmmm: exact literature citation not web-pinned this session" + ], + "receipt": "212fd1bfd57f921d76706bcb28b2e3bde272857f102ede870767237b8fd7e5ad", + "recurrence": { + "C-12": true, + "Li-7": true + }, + "recurrence_mapping": "The closed-shell He-4 cluster (2p2n, J^pi=0+, doubly magic) recurs as a constituent: Li-7 ~ alpha + triton; C-12 ~ 3 x alpha (3-alpha cluster model; Hoyle 0+ state near 7.65 MeV excitation).", + "relation_kind": "recurrence", + "status": "CROSS-DOMAIN-HYPOTHESIS" +} diff --git a/subatomic/receipts/harmonic_binding_per_nucleon_commensurability.json b/subatomic/receipts/harmonic_binding_per_nucleon_commensurability.json new file mode 100644 index 0000000..e91b378 --- /dev/null +++ b/subatomic/receipts/harmonic_binding_per_nucleon_commensurability.json @@ -0,0 +1,27 @@ +{ + "candidate_id": "binding_per_nucleon_commensurability", + "equivalence_condition": "|BE/A(x) - BE/A(He-4)| / BE/A(He-4) <= 0.10 (declared tolerance).", + "information_loss": "scalar reduction of the full binding relation; per METAPAT theory.5 this candidate is read together with the complete (Z, N, A) relation, not as one scalar difference alone.", + "ordered_parameter": { + "declaration": "ordered by increasing (A, Z): H-1, H-2, He-4, Li-7, C-12", + "kind": "nucleon-content-sequence", + "time_agnostic": true + }, + "participants": [ + "H-2", + "He-4", + "Li-7", + "C-12" + ], + "physical_provenance": [ + "compiled nuclear data; web-pinned 2026-08-22" + ], + "receipt": "6a888f65e541363abb220351b9f539b0d0124e4812fe770cfc4eda0b1c3c54e9", + "recurrence": { + "C-12": true, + "Li-7": false + }, + "recurrence_mapping": "Binding energy per nucleon (MeV): H-2 1.11, He-4 7.07, Li-7 5.6, C-12 7.68. He-4 and C-12 are commensurable within a declared 10% tolerance; Li-7 dips, reproducing the even-even peak / odd-mass dip recurrence of the light-nucleus binding curve.", + "relation_kind": "commensurability", + "status": "CROSS-DOMAIN-HYPOTHESIS" +} diff --git a/subatomic/receipts/harmonic_ground_state_spin_parity_symmetry.json b/subatomic/receipts/harmonic_ground_state_spin_parity_symmetry.json new file mode 100644 index 0000000..5bdebdd --- /dev/null +++ b/subatomic/receipts/harmonic_ground_state_spin_parity_symmetry.json @@ -0,0 +1,28 @@ +{ + "candidate_id": "ground_state_spin_parity_symmetry", + "equivalence_condition": "J^pi == \"0+\" for the even-even symmetry class.", + "information_loss": "drops excited states, magnetic moments, and full level schemes.", + "ordered_parameter": { + "declaration": "ordered by increasing (A, Z): H-1, H-2, He-4, Li-7, C-12", + "kind": "nucleon-content-sequence", + "time_agnostic": true + }, + "participants": [ + "H-1", + "H-2", + "He-4", + "Li-7", + "C-12" + ], + "physical_provenance": [ + "compiled nuclear data; web-pinned 2026-08-22" + ], + "receipt": "b0d5eded85102d8c57a33a8360e47aaa98e67e625f5cdee04469d796561dd77a", + "recurrence": { + "C-12": true, + "Li-7": false + }, + "recurrence_mapping": "Ground-state spin-parity J^pi: H-1 1/2+, H-2 1+, He-4 0+, Li-7 3/2-, C-12 0+. The value 0+ recurs for even-even, paired, closed-shell nuclei He-4 and C-12; odd-mass nuclei take half-integer spins.", + "relation_kind": "symmetry", + "status": "CROSS-DOMAIN-HYPOTHESIS" +} diff --git a/subatomic/receipts/harmonic_n_z_ratio_commensurability.json b/subatomic/receipts/harmonic_n_z_ratio_commensurability.json new file mode 100644 index 0000000..f6a9f23 --- /dev/null +++ b/subatomic/receipts/harmonic_n_z_ratio_commensurability.json @@ -0,0 +1,29 @@ +{ + "candidate_id": "n_z_ratio_commensurability", + "equivalence_condition": "N/Z == 1 exactly (rational equality).", + "information_loss": "reduces each nuclide to its (N, Z) pair; drops spin, excitation spectrum, and binding energy.", + "ordered_parameter": { + "declaration": "ordered by increasing (A, Z): H-1, H-2, He-4, Li-7, C-12", + "kind": "nucleon-content-sequence", + "time_agnostic": true + }, + "participants": [ + "H-1", + "H-2", + "He-4", + "Li-7", + "C-12" + ], + "physical_provenance": [ + "nuclide chart (N, Z) counts; standard nuclear data", + "compiled nuclear data; web-pinned 2026-08-22" + ], + "receipt": "8a49097a9c0373fa05e80103f6838e0c4216ba9fa6629df0e3eac0f27f76f030", + "recurrence": { + "C-12": true, + "Li-7": false + }, + "recurrence_mapping": "Neutron/proton ratio N/Z as an exact rational: H-1 0/1, H-2 1/1, He-4 2/2 = 1, Li-7 4/3, C-12 6/6 = 1. The value N/Z = 1 recurs for the even-even N=Z nuclei He-4 and C-12.", + "relation_kind": "ratio", + "status": "CROSS-DOMAIN-HYPOTHESIS" +} diff --git a/subatomic/receipts/harmonic_proton_neutron_inversion_symmetry.json b/subatomic/receipts/harmonic_proton_neutron_inversion_symmetry.json new file mode 100644 index 0000000..b91734c --- /dev/null +++ b/subatomic/receipts/harmonic_proton_neutron_inversion_symmetry.json @@ -0,0 +1,26 @@ +{ + "candidate_id": "proton_neutron_inversion_symmetry", + "equivalence_condition": "N == Z (self-mirror under p <-> n exchange).", + "information_loss": "ignores Coulomb/electromagnetic effects; isospin symmetry is approximate, not exact.", + "ordered_parameter": { + "declaration": "ordered by increasing (A, Z): H-1, H-2, He-4, Li-7, C-12", + "kind": "nucleon-content-sequence", + "time_agnostic": true + }, + "participants": [ + "He-4", + "C-12" + ], + "physical_provenance": [ + "isospin symmetry; standard nuclear physics (Wigner)", + "hmmm: exact citation not web-pinned this session" + ], + "receipt": "9ac380f43069bf69d2eaaa238070dfa8caa40f5029fdb34136310b5877aa52f9", + "recurrence": { + "C-12": true, + "Li-7": false + }, + "recurrence_mapping": "Proton <-> neutron inversion (isospin mirror symmetry): N=Z nuclei He-4 and C-12 map to themselves under p <-> n exchange. H-1 inverts to the free neutron, which is unbound \u2014 a declared asymmetry, not a phase.", + "relation_kind": "inversion", + "status": "CROSS-DOMAIN-HYPOTHESIS" +} diff --git a/subatomic/receipts/he.json b/subatomic/receipts/he.json new file mode 100644 index 0000000..e2d4c2d --- /dev/null +++ b/subatomic/receipts/he.json @@ -0,0 +1,72 @@ +{ + "A": 4, + "Z": 2, + "closure_scale": "epac.subatomic.atomic", + "element_id": "epac.subatomic_affixiation.he", + "neutron_glyphs": [ + "\"", + "B" + ], + "neutron_positions": [ + 3, + 4 + ], + "ordered_parameter_id": "ucns.native-mobius-turn-index", + "proton_glyphs": [ + "A", + "!" + ], + "proton_positions": [ + 1, + 2 + ], + "receipt": "5d7d82a86bb59223495663cbf285310900fdad9499fb8b50f8fe355786a9edc7", + "relation_id": "metapat.affixiation_harmonics.affixiation", + "source_commits": { + "metapat": "34d954aa1e2092e615b03a180500f6b6977f501e", + "ucns": "1975fe70cf4e0826a8020c2da3047569e277af64" + }, + "status": "CROSS-DOMAIN-HYPOTHESIS", + "symbol": "He", + "t_states": [ + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 0, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "reversed-local-frame" + ], + "frame": "reversed-local-frame", + "t": 1, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 2, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + } + ] +} diff --git a/subatomic/receipts/li.json b/subatomic/receipts/li.json new file mode 100644 index 0000000..1234f9b --- /dev/null +++ b/subatomic/receipts/li.json @@ -0,0 +1,78 @@ +{ + "A": 7, + "Z": 3, + "closure_scale": "epac.subatomic.atomic", + "element_id": "epac.subatomic_affixiation.li", + "neutron_glyphs": [ + "B", + "#", + "$", + "C" + ], + "neutron_positions": [ + 4, + 5, + 6, + 7 + ], + "ordered_parameter_id": "ucns.native-mobius-turn-index", + "proton_glyphs": [ + "A", + "!", + "\"" + ], + "proton_positions": [ + 1, + 2, + 3 + ], + "receipt": "5efefff19f97e4f42fa0d85d9719adbe07c39fc7dab700a5eea13f434611bb3f", + "relation_id": "metapat.affixiation_harmonics.affixiation", + "source_commits": { + "metapat": "34d954aa1e2092e615b03a180500f6b6977f501e", + "ucns": "1975fe70cf4e0826a8020c2da3047569e277af64" + }, + "status": "CROSS-DOMAIN-HYPOTHESIS", + "symbol": "Li", + "t_states": [ + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 0, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "reversed-local-frame" + ], + "frame": "reversed-local-frame", + "t": 1, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + }, + { + "complete_key": [ + "ucns.native-mobius-root-loop", + "0", + "positive-local-frame" + ], + "frame": "positive-local-frame", + "t": 2, + "visible_key": [ + "ucns.native-mobius-root-loop", + "0" + ] + } + ] +} diff --git a/subatomic/subatomic-affixiation-baseline.md b/subatomic/subatomic-affixiation-baseline.md new file mode 100644 index 0000000..6aa124f --- /dev/null +++ b/subatomic/subatomic-affixiation-baseline.md @@ -0,0 +1,286 @@ +> Migration note: this document records the stack-incubation phase. Statements that no EPAC repository existed were true of that phase and are not current repository status. + +# Subatomic Affixiation Baseline — hydrogen → helium (provisional candidate) + +- Status: **CROSS-DOMAIN-HYPOTHESIS / provisional research candidate** +- Root impact: **none** +- Owner of record: `The-Interdependency/stack` → `research/epac/` placeholder (no canonical epac + repository exists yet — see `STACK_MANIFEST.md`) +- Canon class: **proposed** — nothing in this document is org canon. Established facts are + cited from current METAPAT and UCNS sources and marked `implemented`; everything else is + candidate or `hmmm`. + +## 1. Domain claims (before any definition) + +Per `domain-claims`, the operative senses are claimed before the construction uses them. + +| Surface form | Term id | Claiming domain | Claimed sense | Scope | Type | Status | +|---|---|---|---|---|---|---| +| hydrogen | `physics.atomic.hydrogen` | physics | element with atomic number Z=1 | empirical element identity | native | ratified in physics | +| hydrogen (here) | `epac.subatomic_affixiation.hydrogen` | epac candidate | declared participant set: one proton participant on declared carrier positions | this construction only | specialized | provisional | +| helium | `physics.atomic.helium` | physics | element with atomic number Z=2 | empirical element identity | native | ratified in physics | +| helium (here) | `epac.subatomic_affixiation.helium` | epac candidate | declared participant set: two proton + two neutron participants (default instance He-4) affixiated over the Möbius parameter | this construction only | specialized | provisional | +| lithium (here) | `epac.subatomic_affixiation.lithium` | epac candidate | same construction form at Z=3 (default instance Li-7) | program target | specialized | provisional | +| carbon (here) | `epac.subatomic_affixiation.carbon` | epac candidate | same construction form at Z=6 (default instance C-12) | program target | specialized | provisional | +| affixiation | `metapat.affixiation_harmonics.affixiation` | METAPAT | identity-preserving higher-order declared relation; participants stay addressable; may integrate as object-whole at a declared native scale | cross-domain application | borrowed (unchanged) | CROSS-DOMAIN-HYPOTHESIS (per METAPAT application) | +| carrier position | `ucns.public_gonol.position` | UCNS | exact glyph identity at exact index on the 157-position Public Gonol carrier | UCNS geometry | borrowed (unchanged) | implemented | +| derivation (here) | `epac.subatomic_affixiation.derivation` | epac candidate | replay of the same declared construction form for another element | this document | specialized | provisional | + +**Collision check:** physics owns the empirical senses of hydrogen/helium/lithium/carbon; the +epac senses are explicitly scoped to this construction and do not contest physics. No prior +hydrogen/helium/lithium claims exist in current metapat or ucns checkouts. Resolution: **clear** +(separate scopes, no overlap). + +## 2. METAPAT consultation + +- question: what relation organizes the hydrogen → helium baseline over the UCNS carrier? +- METAPAT standing: **application** — `metapat.application.affixiation_harmonics` + (CROSS-DOMAIN-HYPOTHESIS, root impact none); not axiom, postulate, or theorem. +- relevant relation: affixiation (identity-preserving higher-order relation), time-agnostic + recurrence and oscillation, harmonic correspondence and resonance as candidate language. +- transfers: the shared question form only — + + ```text + addressable participants + -> declared relation + -> declared ordered parameter or parameters + -> recurrent structure + -> harmonic correspondence or non-correspondence + -> possible native-scale integration + -> recursively addressable whole + ``` + +- does not transfer: element identity or empirical facts (physics), carrier/containment/geometry + selection (UCNS), geometric operation of carrier positions (UCNS `hmmm`), measurement validity + (EDCM), physical frequency or temporal periodicity. +- downstream consequence: a named, bounded epac candidate may proceed with a declared admission + profile, the Möbius turn index as the time-agnostic ordered parameter, and explicit `hmmm` on + every position operation. + +## 3. UCNS established baseline (implemented surfaces only) + +Cited from current UCNS at `1975fe70`: + +- **Public Gonol carrier** (`implemented`): exactly 157 one-scalar glyph positions in fixed order; + digest `55d10c84529a4d7bc7714786357e977b68d9df2ac3f73d20e229580b552c2ef5`; every glyph is a + function position; no linguistic subclassing. +- **Structural Null origin** (`implemented`): fixed origin at carrier position `0` (glyph `" "`), + singular, not ordinary numeric zero. +- **Native Möbius root loop** (`implemented`): quotient `(t, ε) ~ (t + n, (-1)^n ε)` with exact + rational turns. One visible turn (t=1) returns to the same phase with the local frame reversed; + two visible turns (t=2) restore the complete state. +- **Position operations** (`hmmm`, declared in `ucns/src/ucns/public_gonol.py` MODULE_BUILD): + "the exact geometric operation expressed by each function position beyond its carrier identity" + is unresolved. No construction here may invent one. + +## 4. Candidate construction (named, bounded, provisional) + +### 4.1 Admission profile (epac-owned candidate, instance-resolved) + +- Element `E(Z, A)` is represented by `Z` proton-participant positions and `A − Z` + neutron-participant positions on the Public Gonol carrier. +- Default isotope instances are declared per element — H-1, He-4, Li-7, C-12. Isotope choice is + **instance-resolved**, not a law of the construction. +- Proton participants occupy the first `Z` carrier positions after the origin: positions + `1 .. Z`. Neutron participants occupy the next `A − Z` positions: positions `Z+1 .. A`. +- Every assigned position is an **identity coordinate only**. No geometric operation is asserted + for any position. + +### 4.2 Baseline: hydrogen → helium + +```text +hydrogen (H-1): participants {p0 @ position 1} + relation: none (single participant) + closure: participant-scale whole + +helium (He-4): participants {p0 @1, p1 @2} ∪ {n0 @3, n1 @4} + declared relation: affixiation + ordered parameter: Möbius turn index t ∈ {0, 1, 2} (time-agnostic) + t=0: simultaneous tensor arrangement of participants (tensor-first) + t=1: visible 360° return — local frame flips (distinguishable relational state) + t=2: complete 720° return — full framed state restored + recurrence: frame flip/restore is the recurrent structure over parameter t + closure: affixiated helium-whole at the declared atomic native scale; + constituents remain addressable with identity and provenance +``` + +The only geometry used is the established Möbius framing. The carrier positions supply identity; +they do not yet supply operations. Hydrogen and helium differ by participant set and affixiation +arity — nothing else is claimed. + +### 4.3 Derivation of lithium, carbon, et al. (same construction form) + +```text +lithium (Li-7): p @1,2,3 ; n @4..7 -> affixiate -> Möbius recurrence -> atomic-scale closure +carbon (C-12): p @1..6 ; n @7..12 -> affixiate -> Möbius recurrence -> atomic-scale closure +``` + +Each further element is a **separate candidate instance** of the same construction form. "Derive" +in this document means **replay the same declared construction** for a different declared +participant set. It does not mean a physics derivation, a UCNS theorem, a chemical fact, or a +proof that one element emerges from another. + +### 4.4 Deterministic receipt (replay contract) + +For each closed element-whole, a receipt is the SHA-256 over canonical JSON of: + +```text +element_id, isotope_instance, ordered proton positions, ordered neutron positions, +relation_id ("affixiation"), ordered parameter ("ucns.native-mobius-turn-index"), +t-state sequence (0 -> 1 -> 2), closure_scale ("atomic"), source_commits +``` + +Independent replay must reproduce the receipt byte-for-byte. A receipt establishes +reproducibility of the declared construction only — not geometry, physics, or measurement. + +## 5. What this establishes — and what it does not + +**Established (proposed candidate):** a source-bound, replayable baseline that binds current +METAPAT affixiation semantics to current UCNS carrier identity surfaces, using the native Möbius +turn index as the time-agnostic ordered parameter. + +**Not established:** any Public Gonol position operation; any geometry between carrier positions; +any harmonic notation or resonance coupling; any physics or chemistry claim; any EDCM measurement +projection; any canon promotion in METAPAT, UCNS, or elsewhere. + +## 6. Usage guidance + +To replay by hand: + +1. Pin sources: METAPAT `34d954a`, UCNS `1975fe7` (recorded above and in `STACK_MANIFEST.md`). +2. Read `metapat/docs/applications/affixiation-harmonics.md` for the semantic definitions used. +3. Read `ucns/src/ucns/public_gonol.py` and `ucns/src/ucns/direct_mobius.py` for the carrier and + Möbius surfaces used. +4. Apply the admission profile in §4.1, run the construction in §4.2/§4.3, and verify the receipt + in §4.4 against an independent replay. + +To implement later (only after UCNS establishes position operations, or as a pure identity-profile +consumer): + +```text +entry points: ucns.public_gonol_function(index) # carrier identity position + ucns.native_mobius_state(turns) # established Möbius framing +``` + +Do not add local geometry, position-operation semantics, or physics status inside this candidate. + +## 7. Next decisive step (action-calibration) + +- decision: is the H→He affixiation baseline a usable identity-level candidate for the epac program? +- minimal decisive action: an executable H→He candidate that consumes only the two UCNS public + surfaces above, produces the §4.4 receipt, and is independently replayed byte-identically. +- positive outcome → escalate to Li/C instances of the same constructor. +- negative outcome → the admission profile or receipt contract needs repair before any Li/C work. +- unresolved outcome → UCNS position operations remain `hmmm`; keep identity-only scope. +- frozen stop condition: receipt mismatch or any invented position operation fails the candidate. + +## 8. hmmm + +- The geometric operation of every Public Gonol position beyond carrier identity remains unresolved + (UCNS-owned `hmmm`); this baseline deliberately does not fill it. +- No UCNS harmonic-resonance notation is selected (METAPAT-owned `hmmm`); phase/ratio/coupling + fields remain candidates. +- Isotope defaults (H-1, He-4, Li-7, C-12) are instance-resolved, not canonical admission law. +- epac has no canonical source repository; this record lives in the stack placeholder and must + migrate if `The-Interdependency/epac` is created. +- No EDCM measurement projection is declared; nothing here may become empirical validation. +- Promotion of affixiation from application terminology into METAPAT postulates/theories remains + unresolved and is not advanced by this candidate. + +## 9. Local implementation status (2026-08-22) + +The frozen minimal decisive action from §7 is now implemented locally (not pushed): + +- `element_affixiation_candidate.py` — identity-only constructor for H/He/Li/C consuming only + `ucns.public_gonol_function` and `ucns.native_mobius_state`. Carries `MODULE_BUILD` and + `CONTRACTS` blocks; no position operation is defined or inferred. +- `test_element_affixiation_candidate.py` — five executable witnesses with a `CHECKS` block. + Result: **5 passed** against the pinned UCNS snapshot package (`ucns/src` at `1975fe7`). +- `receipts/` — sealed construction receipts, one per element: + + | Element | Receipt (SHA-256) | + |---|---| + | H | `be411f204e10c14ac42b2983677f6b22a02d1cb6c4b158bf2026b0b6e88ca3da` | + | He | `5d7d82a86bb59223495663cbf285310900fdad9499fb8b50f8fe355786a9edc7` | + | Li | `5efefff19f97e4f42fa0d85d9719adbe07c39fc7dab700a5eea13f434611bb3f` | + | C | `a4026f197d6a0425b4ea5b3ff72d09d49fd159d5f59440480b5f97793b64cdc6` | + +- Independent replay (`replay_element`) is byte-identical for all four elements. +- Status remains `CROSS-DOMAIN-HYPOTHESIS / provisional`. Nothing here establishes position + operations, geometry between positions, harmonic notation, physics, or canon. + +## 10. Physically sourced harmonic candidates (2026-08-22) + +Per METAPAT's evidence contract, harmonic candidates do **not** wait for a UCNS harmonic +notation. Each candidate declares participants, ordered parameter, recurrence mapping, +equivalence condition, information loss, and physical provenance. The ordered parameter is +the time-agnostic nucleon-content sequence `(A, Z)`, not time and not an unsourced phase. +No Public Gonol position operation is invented. + +- `nuclear_harmonic_candidates.py` — five candidates with `MODULE_BUILD` + `CONTRACTS`. +- `test_nuclear_harmonic_candidates.py` — five witnesses with `CHECKS`. **10/10 tests pass** + across both modules; CONTRACTS↔CHECKS audit **closed** (10 contracts / 10 checks). +- `receipts/harmonic_*.json` — sealed candidate records. + +| Candidate | Kind | Li-7 | C-12 | Receipt | +|---|---|---|---|---| +| alpha-cluster recurrence | recurrence | recurs | recurs | `212fd1bf…e5ad` | +| N/Z ratio commensurability | ratio | no (4/3) | recurs (1) | `8a49097a…f030` | +| ground-state spin-parity symmetry | symmetry | no (3/2⁻) | recurs (0⁺) | `b0d5eded…dd77a` | +| binding-per-nucleon commensurability | commensurability | no (~21% dev) | recurs (~8% dev) | `6a888f65…54e9` | +| proton↔neutron inversion symmetry | inversion | no (N≠Z) | recurs (N=Z) | `9ac380f4…52f9` | + +Surviving relation across Li and C: **only the alpha-cluster recurrence** survives both; +the four N=Z / even-even relations survive C-12 but not Li-7. Physical provenance for the +numeric nuclear data is web-pinned 2026-08-22; alpha-cluster and isospin citations remain +`hmmm` (standard references, exact citation not web-pinned this session). All results remain +`CROSS-DOMAIN-HYPOTHESIS / hmmm` — no physics validation or canon is claimed. + +## 11. Subatomic gonol (2026-08-22) + +The subatomic gonol closes one element gonol per symbol from three separately addressable +layers, using the EPAC Public Gonol constructor (`epac.public_gonol`) on the UCNS +Public Gonol carrier. This is not `edcm.gonol`: + +1. **nucleus participant** — subatomic identity (proton/neutron Public Gonol carrier + positions and glyphs, Möbius t-state frame sequence) plus harmonic relation results; +2. **electron-shell participants** — the quantum layer from `epac_atomic` (n, l, m_l, m_s, + shell, subshell, hydrogenic angular id, radial nodes, Slater Z_eff, Rydberg energy); +3. **element closure** — relation `epac.subatomic.element`, carried options Z/period/group/A, + electron configuration, valence count, surviving harmonic relations, status + `CROSS-DOMAIN-HYPOTHESIS`. + +- `subatomic_gonol.py` — constructor with `MODULE_BUILD` + `CONTRACTS`. +- `test_subatomic_gonol.py` — five witnesses with `CHECKS`. **15/15 tests pass** across all + three subatomic modules; CONTRACTS↔CHECKS audit **closed** (15 contracts / 15 checks). +- `receipts/gonol_*.json` — historical EDCM-constructor receipts, superseded as + constructor identity. Replay of the current constructor is `replay_public_gonol`. + +| Element | Gonol receipt digest | +|---|---| +| H | `3191f743…bc22b` | +| He | `37991f4b…6c3c37` | +| Li | `ff23abd7…312c95` | +| C | `f951b648…45f0e3` | + +Layers stay distinct inside the gonol: nucleus and electron shells remain individually +addressable participants with their own source_ids. No position operation, no Möbius coupling +law, and no scale interchange is introduced. Standing is `implemented-candidate`, +`selection_effect: none` — the gonol is a candidate, not selected canon. + +## 13. Extension to iron and symbol-abbreviation coupling (2026-08-22) + +- **Extended quantum layer** (`extended_atomic.py`): Z=1..26. Z≤18 delegates byte-identically + to `epac_atomic`; Z=19..26 uses declared ground-state configurations (K through Fe), including + the Cr `4s1.3d5` exception. Fe = `1s2.2s2.2p6.3s2.3p6.4s2.3d6`, A=56. +- **Subatomic gonol now supports all 26 symbols** (`subatomic_gonol.py`), using + `extended_atomic` and the EPAC Public Gonol constructor (`epac_public_gonol`). +- **Nomenclature abbreviation** (`symbol_coupling.py`): letters are **not** a physics + domain. A chemical-symbol abbreviation is a name attached to a closed element gonol. + Two-letter names (He, Fe) are two ordered name-characters, not physical `(z, x)` / + `(z, y)` couplings and not nuclear-Z charge states. Physics 3-structure stays on + atom instances only. +- Evidence: **26/26 subatomic tests pass**; sibling epac suite **29 tests OK**; + CONTRACTS↔CHECKS audit **closed** (26 contracts / 26 checks). +- The dimensional-arity doctrine is implemented by the sibling `epac_dimensional_arity.py` + (committed); no duplicate is maintained here. Status remains `CROSS-DOMAIN-HYPOTHESIS`. diff --git a/subatomic/subatomic_gonol.py b/subatomic/subatomic_gonol.py new file mode 100644 index 0000000..4fee545 --- /dev/null +++ b/subatomic/subatomic_gonol.py @@ -0,0 +1,273 @@ +"""Subatomic gonol constructor. + +Closes one subatomic element gonol per supported symbol from three source +layers, all kept separately addressable: + +1. subatomic nucleus identity — proton/neutron Public Gonol carrier positions + and native Möbius t-state framing (``element_affixiation_candidate``); +2. nuclear harmonic relations — the physically sourced candidates from + ``nuclear_harmonic_candidates`` (alpha-cluster recurrence, N/Z ratio, + spin-parity, binding-per-nucleon commensurability, p<->n inversion); +3. quantum layer — full atomic electron-shell structure from ``epac_atomic`` + (n, l, m_l, m_s, shell, subshell, angular id, radial nodes, Slater Z_eff, + Rydberg energy). + +Construction uses the EPAC Public Gonol constructor +(``epac.public_gonol``) on the UCNS carrier. This is not ``edcm.gonol``. +No Public Gonol position operation and no Möbius coupling law is invented. + +Status: CROSS-DOMAIN-HYPOTHESIS / implemented candidate. Not selected canon. + +Usage guidance: + + PYTHONPATH=":/subatomic:/src" python3 - <<'PY' + from subatomic_gonol import construct_subatomic_gonol, replay_subatomic_gonol + + receipt = construct_subatomic_gonol("He") + print(receipt.receipt_digest) + assert replay_subatomic_gonol(receipt) == receipt.receipt_digest + PY +""" + +from extended_atomic import ( + EXTENDED_SYMBOLS, + SYMBOL_TO_Z, + AtomicRecord, + atomic_record, +) +from epac_public_gonol import ( + ClosedPublicGonol, + PublicGonolReceipt, + construct_public_gonol, + replay_public_gonol, +) + +import element_affixiation_candidate as identity +import nuclear_harmonic_candidates as harmonics + +# === MODULE_BUILD === +# id: epac_subatomic_gonol +# module_name: subatomic_gonol +# module_kind: experiment +# summary: closes one subatomic element gonol per symbol from subatomic nucleus identity, nuclear harmonic relations, and quantum-layer electron shells via the EPAC Public Gonol constructor +# owner: The Interdependency +# public_surface: SUPPORTED_SYMBOLS, construct_subatomic_gonol, replay_subatomic_gonol, subatomic_receipt_record +# internal_surface: _carrier_glyph, _nucleus_participant, _shell_participants, _electron_options, _harmonic_rows +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: subatomic.test_subatomic_gonol +# rollout: local candidate module under stack/research/epac/subatomic/ +# rollback: remove module, tests, and generated receipts +# requires: epac_public_gonol, epac_atomic, epac_subatomic_element_affixiation_candidate, epac_subatomic_nuclear_harmonic_candidates +# since: 2026-08-22 +# unresolved: UCNS position operations; UCNS harmonic notation; EPAC Public Gonol candidate is not selected canon +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: subatomic_gonol_combines_three_sources +# given: a subatomic gonol is constructed for a supported symbol +# then: participants are one subatomic nucleus gonol plus quantum-layer electron-shell gonols, and carried options include subatomic identity, harmonic relation results, and electron configuration +# class: construction +# +# id: subatomic_gonol_replays_byte_identical +# given: a subatomic gonol receipt +# then: replay_public_gonol reproduces the same receipt_digest +# class: correctness +# +# id: subatomic_gonol_keeps_layers_distinct +# given: constructed gonol participants +# then: nucleus (subatomic layer) and electron shells (quantum layer) remain separately addressable with their own source_ids; scales are not interchanged +# class: doctrine +# +# id: subatomic_gonol_invents_no_geometry +# given: construction +# then: construction uses epac.public_gonol on the UCNS carrier; no position operation or Möbius coupling law is defined or inferred +# class: safety +# +# id: subatomic_gonol_stays_cross_domain_hypothesis +# given: any receipt +# then: standing is implemented-candidate, selection_effect is none, and no physics validation or canon promotion is claimed +# class: doctrine +# === END CONTRACTS === + +SUPPORTED_SYMBOLS: tuple[str, ...] = EXTENDED_SYMBOLS + + +def _harmonic_rows(symbol: str) -> tuple[harmonics.HarmonicCandidate, ...]: + return tuple( + candidate + for candidate in harmonics.CANDIDATES + if any(participant.startswith(f"{symbol}-") for participant in candidate.participants) + ) + + +def _harmonic_survives_symbol( + candidate: harmonics.HarmonicCandidate, + symbol: str, +) -> bool: + recurrence = harmonics.recurrence_test(candidate) + symbol_participants = tuple( + participant + for participant in candidate.participants + if participant.startswith(f"{symbol}-") + ) + return any(recurrence.get(participant, False) for participant in symbol_participants) + + +def _electron_options(record: AtomicRecord, electron) -> tuple[tuple[str, str], ...]: + return ( + ("n", str(electron.n)), + ("l", str(electron.l)), + ("m_l", str(electron.m_l)), + ("m_s", str(electron.m_s)), + ("shell", electron.shell), + ("subshell", electron.subshell), + ("angular-id", electron.angular_id), + ("radial-nodes", str(electron.radial_nodes)), + ("z-eff", electron.z_eff), + ("e-rydberg", electron.e_rydberg), + ("valence", "true" if electron.valence else "false"), + ("paired", "true" if electron.paired else "false"), + ) + + +def _carrier_glyph(text: str) -> str | None: + if len(text) == 1: + return text + return None + + +def _nucleus_participant(symbol: str, occurrence: int) -> ClosedPublicGonol: + element = identity.affixiate_element(symbol) + carried = [ + ("Z", str(element.Z)), + ("A", str(element.A)), + ("proton-positions", ",".join(str(i) for i in element.proton_positions)), + ("proton-glyphs", "".join(element.proton_glyphs)), + ( + "neutron-positions", + ",".join(str(i) for i in element.neutron_positions) or "none", + ), + ("neutron-glyphs", "".join(element.neutron_glyphs) or "none"), + ("mobius-t0-frame", element.t_states[0]["frame"]), + ("mobius-t1-frame", element.t_states[1]["frame"]), + ("mobius-t2-frame", element.t_states[2]["frame"]), + ] + for candidate in _harmonic_rows(symbol): + import json as _json + + carried.append( + ( + f"harmonic:{candidate.candidate_id}", + _json.dumps( + harmonics.recurrence_test(candidate), sort_keys=True, separators=(",", ":") + ), + ) + ) + return construct_public_gonol( + source_id=f"epac.subatomic.nucleus:{symbol}#{occurrence}", + relation="epac.subatomic.nucleus", + carried_options=carried, + occurrence=occurrence, + ).gonol + + +def _shell_participants(record: AtomicRecord, occurrence: int) -> tuple[ClosedPublicGonol, ...]: + by_n: dict[int, list] = {} + for electron in record.electrons: + by_n.setdefault(electron.n, []).append(electron) + shells: list[ClosedPublicGonol] = [] + for n in sorted(by_n): + members: list[ClosedPublicGonol] = [] + for electron in by_n[n]: + electron_receipt = construct_public_gonol( + source_id=f"epac.subatomic.electron:{record.symbol}#{occurrence}:{electron.index}", + relation="epac.atomic.electron", + identity_glyph="e", + carried_options=_electron_options(record, electron), + occurrence=electron.index, + ) + members.append(electron_receipt.gonol) + shell_receipt = construct_public_gonol( + source_id=f"epac.subatomic.shell:{record.symbol}#{occurrence}:n{n}", + relation="epac.atomic.shell", + identity_glyph=_carrier_glyph(str(n)), + participants=members, + occurrence=n, + carried_options=(("n", str(n)),), + ) + shells.append(shell_receipt.gonol) + return tuple(shells) + + +def construct_subatomic_gonol(symbol: str, *, occurrence: int = 0) -> PublicGonolReceipt: + """Close one subatomic element gonol: nucleus + electron shells.""" + if symbol not in SUPPORTED_SYMBOLS: + raise ValueError( + f"subatomic gonol supports {SUPPORTED_SYMBOLS}; got {symbol!r}" + ) + record = atomic_record(SYMBOL_TO_Z[symbol]) + nucleus = _nucleus_participant(symbol, occurrence) + shells = _shell_participants(record, occurrence) + harmonic_surviving = ",".join( + candidate.candidate_id + for candidate in _harmonic_rows(symbol) + if _harmonic_survives_symbol(candidate, symbol) + ) + carried = [ + ("symbol", symbol), + ("Z", str(record.Z)), + ("period", str(record.period)), + ("group", str(record.group)), + ("A", str(record.A)), + ("electron-configuration", record.configuration), + ("valence-electrons", str(record.valence_electrons)), + ("harmonic-surviving", harmonic_surviving or "none"), + ("status", "CROSS-DOMAIN-HYPOTHESIS"), + ] + return construct_public_gonol( + source_id=f"epac.subatomic.element:{symbol}#{occurrence}", + relation="epac.subatomic.element", + identity_glyph=_carrier_glyph(symbol), + participants=(nucleus, *shells), + carried_options=carried, + occurrence=occurrence, + ) + + +def replay_subatomic_gonol(receipt: PublicGonolReceipt) -> str: + """Replay a completed subatomic gonol receipt; returns its digest.""" + return replay_public_gonol(receipt).receipt_digest + + +def subatomic_receipt_record(receipt: PublicGonolReceipt) -> dict: + """JSON-safe summary of one subatomic gonol receipt.""" + gonol = receipt.gonol + return { + "constructor_id": receipt.constructor_id, + "constructor_version": receipt.constructor_version, + "standing": receipt.standing, + "selection_effect": receipt.selection_effect, + "source_id": receipt.source_id, + "receipt_digest": receipt.receipt_digest, + "atomic_id": gonol.atomic_id, + "identity_glyph": gonol.identity_glyph, + "relation": gonol.relation, + "participant_kinds": [ + ("nucleus" if "nucleus" in p.source_id else "shell") for p in gonol.participants + ], + "carried_options": list(gonol.carried_options), + "nonclaims": list(receipt.nonclaims), + "hmmm": list(receipt.hmmm), + } + + +__all__ = [ + "SUPPORTED_SYMBOLS", + "construct_subatomic_gonol", + "replay_subatomic_gonol", + "subatomic_receipt_record", +] diff --git a/subatomic/symbol_coupling.py b/subatomic/symbol_coupling.py new file mode 100644 index 0000000..4baae93 --- /dev/null +++ b/subatomic/symbol_coupling.py @@ -0,0 +1,161 @@ +"""Nomenclature coupling: element gonol + abbreviation. + +Letters are not a physics domain. A chemical-symbol abbreviation is a name. +It is not an atom, not a charge, and not the dimensional 3-structure. + +- physics: nuclei, electrons, nuclear Z, oriented atom-instance couplings +- nomenclature: ordered abbreviation characters as a name only +- UCNS Public Gonol: optional carrier identity for admitted glyphs + +Two-letter names (He, Fe) are two ordered name-characters, not ``(z, x)`` and +``(z, y)`` in physical 3-space, and not a nuclear-Z hub. + +Status: CROSS-DOMAIN-HYPOTHESIS / implemented candidate. Not selected canon. + +Usage guidance: + + from symbol_coupling import couple_symbol + + receipt = couple_symbol("Fe") + assert receipt.gonol.structure is None + print(receipt.receipt_digest) +""" + +# === MODULE_BUILD === +# id: epac_subatomic_symbol_coupling +# module_name: symbol_coupling +# module_kind: experiment +# summary: nomenclature-only coupling of a closed subatomic element gonol to its abbreviation; letters are not physics and do not enter dimensional 3-structure +# owner: The Interdependency +# public_surface: SUPPORTED_SYMBOLS, construct_symbol_gonol, couple_symbol, replay_symbol_coupling +# internal_surface: none +# auth_boundary: letters/nomenclature are excluded from epac physics couplings +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: subatomic.test_symbol_coupling +# rollout: local candidate module under stack/research/epac/subatomic/ +# rollback: remove module, tests, and generated receipts +# requires: epac_public_gonol, epac_subatomic_gonol +# since: 2026-08-22 +# unresolved: which domain later owns chemical-symbol admission if not physics; two-letter names have no single Public Gonol glyph +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: symbol_gonol_preserves_exact_abbreviation +# given: a symbol gonol for element symbol S +# then: participants are the exact ordered name-characters of S; no physics coupling, charge, or 3-structure is attached +# class: correctness +# +# id: letters_are_not_physics_domain +# given: symbol_coupling source and any constructed symbol gonol +# then: epac_dimensional_arity is not imported; nuclear Z is not a letter charge; gonol.structure is None +# class: doctrine +# +# id: symbol_coupling_two_participants +# given: a nomenclature-coupled gonol +# then: exactly two participants (element gonol, symbol gonol) are declared and no physics 3-structure is minted +# class: correctness +# +# id: symbol_coupling_replays_byte_identical +# given: a symbol-coupled receipt +# then: replay_public_gonol reproduces the same receipt_digest +# class: correctness +# +# id: symbol_coupling_stays_cross_domain_hypothesis +# given: any symbol-coupled receipt +# then: standing is implemented-candidate, selection_effect is none, and no canon is selected +# class: doctrine +# === END CONTRACTS === + +from __future__ import annotations + +import os +import sys + +_PARENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _PARENT not in sys.path: + sys.path.insert(0, _PARENT) + +from epac_public_gonol import ( # noqa: E402 + ClosedPublicGonol, + PublicGonolReceipt, + construct_public_gonol, + replay_public_gonol, +) + +import subatomic_gonol # noqa: E402 + +SUPPORTED_SYMBOLS: tuple[str, ...] = subatomic_gonol.SUPPORTED_SYMBOLS + +RELATION_SYMBOL = "epac.nomenclature.abbreviation" +RELATION_COUPLING = "epac.nomenclature.element-abbreviation" + + +def construct_symbol_gonol(symbol: str, *, occurrence: int = 0) -> PublicGonolReceipt: + """Close one abbreviation as nomenclature. Not a physics gonol.""" + + if symbol not in SUPPORTED_SYMBOLS: + raise ValueError(f"symbol {symbol!r} is outside the supported element table") + characters = tuple(symbol) + glyphs: list[ClosedPublicGonol] = [] + for index, character in enumerate(characters): + glyphs.append( + construct_public_gonol( + source_id=f"epac.nomenclature.character:{symbol}#{occurrence}:{index}:{character}", + relation="epac.nomenclature.character", + identity_glyph=character, + occurrence=index, + carried_options=( + ("domain", "nomenclature"), + ("character", character), + ), + ).gonol + ) + return construct_public_gonol( + source_id=f"epac.nomenclature.abbreviation:{symbol}#{occurrence}", + relation=RELATION_SYMBOL, + participants=tuple(glyphs), + occurrence=occurrence, + carried_options=( + ("domain", "nomenclature"), + ("symbol", symbol), + ("abbreviation-length", str(len(symbol))), + ), + ) + + +def couple_symbol(symbol: str, *, occurrence: int = 0) -> PublicGonolReceipt: + """Attach a nomenclature abbreviation to a closed physics element gonol. + + The two participants stay in their domains. This is not ``(z, x)``/``(z, y)`` + physics structure. + """ + + element = subatomic_gonol.construct_subatomic_gonol(symbol, occurrence=occurrence).gonol + symbol_gonol = construct_symbol_gonol(symbol, occurrence=occurrence).gonol + return construct_public_gonol( + source_id=f"epac.nomenclature.element-abbreviation:{symbol}#{occurrence}", + relation=RELATION_COUPLING, + participants=(element, symbol_gonol), + occurrence=occurrence, + carried_options=( + ("domain", "nomenclature"), + ("symbol", symbol), + ), + ) + + +def replay_symbol_coupling(receipt: PublicGonolReceipt) -> str: + return replay_public_gonol(receipt).receipt_digest + + +__all__ = [ + "RELATION_COUPLING", + "RELATION_SYMBOL", + "SUPPORTED_SYMBOLS", + "construct_symbol_gonol", + "couple_symbol", + "replay_symbol_coupling", +] diff --git a/subatomic/test_element_affixiation_candidate.py b/subatomic/test_element_affixiation_candidate.py new file mode 100644 index 0000000..76800d3 --- /dev/null +++ b/subatomic/test_element_affixiation_candidate.py @@ -0,0 +1,109 @@ +"""Executable witnesses for the subatomic element affixiation candidate.""" + +# === CHECKS === +# id: check_candidate_uses_only_established_ucns_surfaces +# proves: candidate_uses_only_established_ucns_surfaces +# call: self::test_imports_consume_only_established_ucns_surfaces +# mutates: none +# cleanup: none +# +# id: check_element_identity_positions_exact +# proves: element_identity_positions_exact +# call: self::test_element_identity_positions_exact +# mutates: none +# cleanup: none +# +# id: check_mobius_parameter_sequence_exact +# proves: mobius_parameter_sequence_exact +# call: self::test_mobius_parameter_sequence_exact +# mutates: none +# cleanup: none +# +# id: check_receipt_deterministic_and_replayable +# proves: receipt_deterministic_and_replayable +# call: self::test_receipt_deterministic_and_replayable +# mutates: none +# cleanup: none +# +# id: check_no_physics_or_canon_claim +# proves: no_physics_or_canon_claim +# call: self::test_no_physics_or_canon_claim +# mutates: none +# cleanup: none +# === END CHECKS === + +from fractions import Fraction + +import element_affixiation_candidate as candidate +from ucns import ( + PUBLIC_GONOL_157, + PUBLIC_GONOL_SHA256, + NativeMobiusFrame, + native_mobius_state, + public_gonol_function, +) + + +def test_imports_consume_only_established_ucns_surfaces(): + # The candidate module surface must stay identity-only. If this test + # fails, a position operation or unestablished geometry was introduced. + assert candidate.CONSTRUCTION_IDS["ordered_parameter"] == "ucns.native-mobius-turn-index" + assert candidate.CONSTRUCTION_IDS["relation"] == "metapat.affixiation_harmonics.affixiation" + # The only UCNS geometry imported is carrier identity + Möbius framing. + assert public_gonol_function(0).glyph == PUBLIC_GONOL_157[0] + + +def test_element_identity_positions_exact(): + cases = { + "H": ((1,), ()), + "He": ((1, 2), (3, 4)), + "Li": ((1, 2, 3), (4, 5, 6, 7)), + "C": ((1, 2, 3, 4, 5, 6), (7, 8, 9, 10, 11, 12)), + } + for symbol, (expected_p, expected_n) in cases.items(): + element = candidate.affixiate_element(symbol) + assert element.proton_positions == expected_p + assert element.neutron_positions == expected_n + # Every assigned position is an identity coordinate on the carrier. + assert all(0 <= i < len(PUBLIC_GONOL_157) for i in element.proton_positions) + assert all(0 <= i < len(PUBLIC_GONOL_157) for i in element.neutron_positions) + assert element.proton_glyphs == tuple( + public_gonol_function(i).glyph for i in element.proton_positions + ) + assert element.neutron_glyphs == tuple( + public_gonol_function(i).glyph for i in element.neutron_positions + ) + + +def test_mobius_parameter_sequence_exact(): + s0 = native_mobius_state(Fraction(0)) + s1 = native_mobius_state(Fraction(1)) + s2 = native_mobius_state(Fraction(2)) + assert s0.visible_key == s1.visible_key == s2.visible_key + assert s0.frame is NativeMobiusFrame.POSITIVE + assert s1.frame is NativeMobiusFrame.REVERSED + assert s2.frame is NativeMobiusFrame.POSITIVE + assert s0.complete_key == s2.complete_key + assert s1.complete_key != s0.complete_key + + +def test_receipt_deterministic_and_replayable(): + for symbol in candidate.ISOTOPE_DEFAULTS: + first = candidate.affixiate_element(symbol) + matches, replay_receipt = candidate.replay_element(symbol) + assert matches is True + assert replay_receipt == first.receipt + assert len(first.receipt) == 64 + # Distinct participant sets produce distinct receipts. + receipts = {candidate.affixiate_element(s).receipt for s in candidate.ISOTOPE_DEFAULTS} + assert len(receipts) == len(candidate.ISOTOPE_DEFAULTS) + + +def test_no_physics_or_canon_claim(): + for symbol in candidate.ISOTOPE_DEFAULTS: + element = candidate.affixiate_element(symbol) + assert element.status == "CROSS-DOMAIN-HYPOTHESIS" + assert element.closure_scale == "epac.subatomic.atomic" + assert candidate.SOURCE_COMMITS["metapat"] == "34d954aa1e2092e615b03a180500f6b6977f501e" + assert candidate.SOURCE_COMMITS["ucns"] == "1975fe70cf4e0826a8020c2da3047569e277af64" + assert PUBLIC_GONOL_SHA256 == "55d10c84529a4d7bc7714786357e977b68d9df2ac3f73d20e229580b552c2ef5" diff --git a/subatomic/test_extended_atomic.py b/subatomic/test_extended_atomic.py new file mode 100644 index 0000000..4797ceb --- /dev/null +++ b/subatomic/test_extended_atomic.py @@ -0,0 +1,63 @@ +"""Executable witnesses for the extended atomic quantum layer Z=1..26.""" + +# === CHECKS === +# id: check_extended_atomic_preserves_z_le_18 +# proves: extended_atomic_preserves_z_le_18 +# call: self::test_extended_atomic_preserves_z_le_18 +# mutates: none +# cleanup: none +# +# id: check_extended_atomic_uses_declared_configurations +# proves: extended_atomic_uses_declared_configurations +# call: self::test_extended_atomic_uses_declared_configurations +# mutates: none +# cleanup: none +# +# id: check_extended_atomic_stays_candidate +# proves: extended_atomic_stays_candidate +# call: self::test_extended_atomic_stays_candidate +# mutates: none +# cleanup: none +# === END CHECKS === + +import epac_atomic +import extended_atomic as m + + +def test_extended_atomic_preserves_z_le_18(): + for Z in range(1, 19): + assert m.atomic_record(Z) == epac_atomic.atomic_record(Z) + + +def test_extended_atomic_uses_declared_configurations(): + iron = m.atomic_record(26) + assert iron.symbol == "Fe" + assert iron.Z == 26 + assert iron.A == 56 + assert iron.configuration == "1s2.2s2.2p6.3s2.3p6.4s2.3d6" + assert sum(1 for e in iron.electrons) == 26 + + chromium = m.atomic_record(24) + assert chromium.configuration == "1s2.2s2.2p6.3s2.3p6.4s1.3d5" + + potassium = m.atomic_record(19) + assert potassium.configuration == "1s2.2s2.2p6.3s2.3p6.4s1" + assert potassium.symbol == "K" + + assert m.SYMBOL_TO_Z["Fe"] == 26 + assert m.EXTENDED_SYMBOLS[25] == "Fe" + assert len(m.EXTENDED_SYMBOLS) == 26 + + +def test_extended_atomic_stays_candidate(): + record = m.atomic_record(26) + # Candidate data is complete but carries no physics-validation claim. + for electron in record.electrons: + assert electron.n >= 1 + assert electron.z_eff + assert electron.e_rydberg + + +def test_extended_atomic_does_not_mutate_sys_path(): + source = open(m.__file__, encoding="utf-8").read() + assert "sys.path" not in source diff --git a/subatomic/test_nuclear_harmonic_candidates.py b/subatomic/test_nuclear_harmonic_candidates.py new file mode 100644 index 0000000..7823685 --- /dev/null +++ b/subatomic/test_nuclear_harmonic_candidates.py @@ -0,0 +1,101 @@ +"""Executable witnesses for the nuclear harmonic-relation candidates.""" + +# === CHECKS === +# id: check_every_harmonic_candidate_declares_six_evidence_fields +# proves: every_harmonic_candidate_declares_six_evidence_fields +# call: self::test_every_candidate_declares_six_evidence_fields +# mutates: none +# cleanup: none +# +# id: check_harmonic_parameter_is_time_agnostic +# proves: harmonic_parameter_is_time_agnostic +# call: self::test_parameter_is_time_agnostic +# mutates: none +# cleanup: none +# +# id: check_no_public_gonol_position_operation_invented +# proves: no_public_gonol_position_operation_invented +# call: self::test_no_position_operation_invented +# mutates: none +# cleanup: none +# +# id: check_recurrence_test_is_deterministic +# proves: recurrence_test_is_deterministic +# call: self::test_recurrence_deterministic_and_replayable +# mutates: none +# cleanup: none +# +# id: check_all_results_remain_cross_domain_hypothesis +# proves: all_results_remain_cross_domain_hypothesis +# call: self::test_all_results_cross_domain_hypothesis +# mutates: none +# cleanup: none +# === END CHECKS === + +import nuclear_harmonic_candidates as m + + +def test_every_candidate_declares_six_evidence_fields(): + for candidate in m.CANDIDATES: + assert candidate.participants + assert candidate.ordered_parameter.get("kind") + assert candidate.ordered_parameter.get("declaration") + assert candidate.recurrence_mapping + assert candidate.equivalence_condition + assert candidate.information_loss + assert candidate.physical_provenance + assert len(candidate.receipt) == 64 + + +def test_parameter_is_time_agnostic(): + for candidate in m.CANDIDATES: + assert candidate.ordered_parameter["time_agnostic"] is True + assert "time" not in candidate.ordered_parameter["kind"] + assert m.ORDERED_PARAMETER["kind"] == "nucleon-content-sequence" + + +def test_no_position_operation_invented(): + # The module must not import UCNS geometry or call position operations. + # (Contract ids legitimately name the forbidden surface, so only actual + # imports and call forms are asserted absent.) + source = open(m.__file__, encoding="utf-8").read() + assert "import ucns" not in source + assert "from ucns" not in source + assert "public_gonol_function(" not in source + assert "native_mobius_state(" not in source + assert "phase" not in m.ORDERED_PARAMETER["declaration"] + + +def test_recurrence_deterministic_and_replayable(): + expected = { + "alpha_cluster_recurrence": {"Li-7": True, "C-12": True}, + "n_z_ratio_commensurability": {"Li-7": False, "C-12": True}, + "ground_state_spin_parity_symmetry": {"Li-7": False, "C-12": True}, + "binding_per_nucleon_commensurability": {"Li-7": False, "C-12": True}, + "proton_neutron_inversion_symmetry": {"Li-7": False, "C-12": True}, + } + for candidate in m.CANDIDATES: + assert m.recurrence_test(candidate) == expected[candidate.candidate_id] + # Receipts are deterministic across reconstruction. + record = { + "candidate_id": candidate.candidate_id, + "relation_kind": candidate.relation_kind, + "participants": list(candidate.participants), + "ordered_parameter": candidate.ordered_parameter, + "recurrence_mapping": candidate.recurrence_mapping, + "equivalence_condition": candidate.equivalence_condition, + "information_loss": candidate.information_loss, + "physical_provenance": list(candidate.physical_provenance), + "status": candidate.status, + } + assert m.harmonic_receipt(record) == candidate.receipt + receipts = {c.receipt for c in m.CANDIDATES} + assert len(receipts) == len(m.CANDIDATES) + + +def test_all_results_cross_domain_hypothesis(): + for candidate in m.CANDIDATES: + assert candidate.status == "CROSS-DOMAIN-HYPOTHESIS" + assert m.NUCLIDE_FACTS["He-4"]["J_pi"] == "0+" + assert m.NUCLIDE_FACTS["C-12"]["J_pi"] == "0+" + assert m.NUCLIDE_FACTS["Li-7"]["J_pi"] == "3/2-" diff --git a/subatomic/test_subatomic_gonol.py b/subatomic/test_subatomic_gonol.py new file mode 100644 index 0000000..2895dbf --- /dev/null +++ b/subatomic/test_subatomic_gonol.py @@ -0,0 +1,122 @@ +"""Executable witnesses for the subatomic gonol constructor.""" + +# === CHECKS === +# id: check_subatomic_gonol_combines_three_sources +# proves: subatomic_gonol_combines_three_sources +# call: self::test_combines_three_sources +# mutates: none +# cleanup: none +# +# id: check_subatomic_gonol_replays_byte_identical +# proves: subatomic_gonol_replays_byte_identical +# call: self::test_replays_byte_identical +# mutates: none +# cleanup: none +# +# id: check_subatomic_gonol_keeps_layers_distinct +# proves: subatomic_gonol_keeps_layers_distinct +# call: self::test_keeps_layers_distinct +# mutates: none +# cleanup: none +# +# id: check_subatomic_gonol_invents_no_geometry +# proves: subatomic_gonol_invents_no_geometry +# call: self::test_invents_no_geometry +# mutates: none +# cleanup: none +# +# id: check_subatomic_gonol_stays_cross_domain_hypothesis +# proves: subatomic_gonol_stays_cross_domain_hypothesis +# call: self::test_stays_cross_domain_hypothesis +# mutates: none +# cleanup: none +# === END CHECKS === + +import subatomic_gonol as m +from extended_atomic import atomic_record + + +def _receipts(): + return {symbol: m.construct_subatomic_gonol(symbol) for symbol in m.SUPPORTED_SYMBOLS} + + +def test_combines_three_sources(): + for symbol, receipt in _receipts().items(): + carried = dict(receipt.gonol.carried_options) + nucleus_carried = dict(receipt.gonol.participants[0].carried_options) + # Subatomic identity fields live on the nucleus participant. + assert "proton-positions" in nucleus_carried + assert "proton-glyphs" in nucleus_carried + assert "mobius-t0-frame" in nucleus_carried + assert "mobius-t2-frame" in nucleus_carried + # Harmonic relation results live on the nucleus participant for the + # elements that participate in the declared nuclear candidates. + if symbol in {"H", "He", "Li", "C"}: + assert any(key.startswith("harmonic:") for key in nucleus_carried) + # Quantum-layer fields live on the element gonol. + assert carried["electron-configuration"] == atomic_record(int(carried["Z"])).configuration + assert "valence-electrons" in carried + assert "harmonic-surviving" in carried + + +def test_replays_byte_identical(): + for symbol, receipt in _receipts().items(): + assert m.replay_subatomic_gonol(receipt) == receipt.receipt_digest + assert len(receipt.receipt_digest) == 64 + digests = {r.receipt_digest for r in _receipts().values()} + assert len(digests) == len(m.SUPPORTED_SYMBOLS) + + +def test_keeps_layers_distinct(): + for symbol, receipt in _receipts().items(): + kinds = [ + "nucleus" if "nucleus" in p.source_id else "shell" + for p in receipt.gonol.participants + ] + assert kinds[0] == "nucleus" + assert all(kind == "shell" for kind in kinds[1:]) + assert len(kinds) >= 2 # nucleus + at least one shell + # Electron shells are individually addressable, not flattened. + for participant in receipt.gonol.participants[1:]: + assert "shell" in participant.source_id + + +def test_invents_no_geometry(): + source = open(m.__file__, encoding="utf-8").read() + # The module consumes epac.public_gonol; it must not define position operations + # and must not import the EDCM text-domain constructor. + assert "def " + "public_gonol" not in source + assert "from edcm" not in source + assert "import edcm" not in source + assert "advance(" not in source + assert "NativeMobius" not in source + receipt = m.construct_subatomic_gonol("H") + assert receipt.constructor_id == "epac.public_gonol" + assert receipt.gonol.geometry_digest + + +def test_stays_cross_domain_hypothesis(): + for symbol, receipt in _receipts().items(): + assert receipt.standing == "implemented-candidate" + assert receipt.selection_effect == "none" + assert dict(receipt.gonol.carried_options)["status"] == "CROSS-DOMAIN-HYPOTHESIS" + assert receipt.nonclaims + assert receipt.hmmm + + +def test_imports_do_not_mutate_sys_path(): + source = open(m.__file__, encoding="utf-8").read() + assert "sys.path" not in source + + +def test_harmonic_survival_is_symbol_specific(): + surviving = { + symbol: dict(m.construct_subatomic_gonol(symbol).gonol.carried_options)[ + "harmonic-surviving" + ] + for symbol in ("H", "He", "Li", "C") + } + assert surviving["H"] == "none" + assert surviving["He"] == "none" + assert surviving["Li"] == "alpha_cluster_recurrence" + assert "proton_neutron_inversion_symmetry" in surviving["C"] diff --git a/subatomic/test_symbol_coupling.py b/subatomic/test_symbol_coupling.py new file mode 100644 index 0000000..73911cd --- /dev/null +++ b/subatomic/test_symbol_coupling.py @@ -0,0 +1,93 @@ +"""Executable witnesses for nomenclature abbreviation coupling.""" + +# === CHECKS === +# id: check_letters_are_not_physics_domain +# proves: letters_are_not_physics_domain +# call: self::test_letters_are_not_physics_domain +# mutates: none +# cleanup: none +# +# id: check_symbol_gonol_preserves_exact_abbreviation +# proves: symbol_gonol_preserves_exact_abbreviation +# call: self::test_symbol_gonol_preserves_exact_abbreviation +# mutates: none +# cleanup: none +# +# id: check_symbol_coupling_two_participants +# proves: symbol_coupling_two_participants +# call: self::test_symbol_coupling_two_participants +# mutates: none +# cleanup: none +# +# id: check_symbol_coupling_replays_byte_identical +# proves: symbol_coupling_replays_byte_identical +# call: self::test_symbol_coupling_replays_byte_identical +# mutates: none +# cleanup: none +# +# id: check_symbol_coupling_stays_cross_domain_hypothesis +# proves: symbol_coupling_stays_cross_domain_hypothesis +# call: self::test_symbol_coupling_stays_cross_domain_hypothesis +# mutates: none +# cleanup: none +# === END CHECKS === + +import symbol_coupling as m + + +def test_letters_are_not_physics_domain(): + source = open(m.__file__, encoding="utf-8").read() + assert "from epac_dimensional_arity" not in source + assert "import epac_dimensional_arity" not in source + assert "SYMBOL_TO_Z" not in source + assert "oriented_instance_couplings" not in source + helium = m.construct_symbol_gonol("He") + iron = m.construct_symbol_gonol("Fe") + assert helium.gonol.structure is None + assert helium.gonol.couplings == () + assert iron.gonol.structure is None + assert dict(helium.gonol.carried_options)["domain"] == "nomenclature" + for participant in helium.gonol.participants: + assert dict(participant.carried_options)["domain"] == "nomenclature" + assert "Z" not in dict(participant.carried_options) + + +def test_symbol_gonol_preserves_exact_abbreviation(): + h = m.construct_symbol_gonol("H").gonol + assert len(h.participants) == 1 + assert dict(h.carried_options)["abbreviation-length"] == "1" + + he = m.construct_symbol_gonol("He").gonol + assert len(he.participants) == 2 + assert [p.identity_glyph for p in he.participants] == ["H", "e"] + assert dict(he.carried_options)["abbreviation-length"] == "2" + + fe = m.construct_symbol_gonol("Fe").gonol + assert [p.identity_glyph for p in fe.participants] == ["F", "e"] + + +def test_symbol_coupling_two_participants(): + for symbol in ("H", "He", "Fe"): + receipt = m.couple_symbol(symbol) + assert len(receipt.gonol.participants) == 2 + assert dict(receipt.gonol.carried_options)["symbol"] == symbol + assert dict(receipt.gonol.carried_options)["domain"] == "nomenclature" + assert receipt.gonol.structure is None + assert receipt.gonol.couplings == () + assert receipt.gonol.participants[0].relation == "epac.subatomic.element" + assert receipt.gonol.participants[1].relation == "epac.nomenclature.abbreviation" + + +def test_symbol_coupling_replays_byte_identical(): + digests = set() + for symbol in m.SUPPORTED_SYMBOLS: + receipt = m.couple_symbol(symbol) + assert m.replay_symbol_coupling(receipt) == receipt.receipt_digest + digests.add(receipt.receipt_digest) + assert len(digests) == len(m.SUPPORTED_SYMBOLS) + + +def test_symbol_coupling_stays_cross_domain_hypothesis(): + receipt = m.couple_symbol("Fe") + assert receipt.standing == "implemented-candidate" + assert receipt.selection_effect == "none" diff --git a/tests/test_atomic_promotion.py b/tests/test_atomic_promotion.py new file mode 100644 index 0000000..69f72b9 --- /dev/null +++ b/tests/test_atomic_promotion.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EPAC_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EPAC_ROOT)) + +from epac_atomic import atomic_record +from epac_periodic import construct_element_gonol, replay_element_gonol + + +class AtomicPromotionTest(unittest.TestCase): + def test_promoted_carbon_unpaired_accounting_and_ordering(self) -> None: + carbon = atomic_record(6) + self.assertEqual(carbon.configuration, "1s2.2s2.2p2") + self.assertEqual(tuple((e.l, e.m_l) for e in carbon.unpaired_valence), ((1, 1), (1, 0))) + promoted = carbon.promoted_unpaired_valence + self.assertEqual(len(promoted), 4) + # Every promoted unpaired electron uses the m_s = +1 convention. + self.assertTrue(all(e.m_s == 1 for e in promoted)) + self.assertEqual(len({e.index for e in promoted}), len(promoted)) + # Canonical subshell ordering: s before p, p orbitals ascending m_l. + self.assertEqual( + tuple((e.l, e.m_l) for e in promoted), + ((0, 0), (1, -1), (1, 0), (1, 1)), + ) + self.assertEqual({e.subshell for e in promoted}, {"2s", "2p"}) + + def test_promoted_beryllium_unpaired_accounting_and_ordering(self) -> None: + beryllium = atomic_record(4) + self.assertEqual(beryllium.configuration, "1s2.2s2") + promoted = beryllium.promoted_unpaired_valence + self.assertEqual(len(promoted), 2) + self.assertTrue(all(e.m_s == 1 for e in promoted)) + self.assertEqual(tuple((e.l, e.m_l) for e in promoted), ((0, 0), (1, 1))) + + def test_ordinary_atoms_do_not_promote_without_an_empty_valence_p(self) -> None: + # Helium has no valence shell; oxygen and nitrogen have no empty + # valence p orbital, so their promoted sets equal their ground sets. + helium = atomic_record(2) + oxygen = atomic_record(8) + nitrogen = atomic_record(7) + self.assertEqual(helium.promoted_unpaired_valence, ()) + self.assertEqual(helium.unpaired_valence, ()) + self.assertEqual( + tuple((e.l, e.m_l) for e in oxygen.promoted_unpaired_valence), + ((1, 0), (1, -1)), + ) + self.assertEqual( + tuple((e.l, e.m_l) for e in oxygen.promoted_unpaired_valence), + tuple((e.l, e.m_l) for e in oxygen.unpaired_valence), + ) + self.assertEqual(len(nitrogen.promoted_unpaired_valence), 3) + self.assertEqual( + tuple((e.l, e.m_l) for e in nitrogen.promoted_unpaired_valence), + tuple((e.l, e.m_l) for e in nitrogen.unpaired_valence), + ) + + def test_configuration_serialization_round_trip(self) -> None: + carbon = construct_element_gonol("C") + options = dict(carbon.gonol.carried_options) + self.assertEqual(options["electron-configuration"], "1s2.2s2.2p2") + self.assertEqual(options["unpaired-valence-lm"], "1:1,1:0") + self.assertEqual(options["promoted-unpaired-count"], "4") + self.assertEqual(options["promoted-unpaired-lm"], "0:0,1:-1,1:0,1:1") + replayed = replay_element_gonol(carbon) + self.assertEqual(carbon.receipt_digest, replayed.receipt_digest) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_epac_arity.py b/tests/test_epac_arity.py new file mode 100644 index 0000000..b10cac6 --- /dev/null +++ b/tests/test_epac_arity.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EPAC_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EPAC_ROOT)) + +from epac_dimensional_arity import ( + CouplingProof, + DimensionalArityError, + QUATERNION_REPRESENTATION_DIMENSION, + QUATERNION_SCALAR_AXIS, + REPRESENTED_STRUCTURE_DIMENSION, + charged_structure_readout, + coupling, + degree_relations, + geometry_from_declared_couplings, + has_declared_coupling, + install_proven_coupling, + instances_missing_oriented_hub_coupling, + local_three_structures, + observed_common_ids, + oriented_instance_couplings, + quaternion_structure_readout, + require_every_instance_has_oriented_hub_coupling, + space, + topology_structure_readout, +) + + +class DimensionalArityTest(unittest.TestCase): + def test_unary_in_one_ambient_dimension(self) -> None: + declared = space(["x"], [["x"]]) + geometry = geometry_from_declared_couplings(declared) + self.assertEqual(geometry["ambient_count"], 1) + self.assertEqual(geometry["couplings"][0]["declared_ids"], ("x",)) + self.assertEqual(geometry["couplings"][0]["arity"], 1) + self.assertEqual(geometry["degree_relations"][0]["degree"], 1) + + def test_zx_is_not_xz(self) -> None: + declared = space(["x", "z"], [["z", "x"]], charges={"x": 1, "z": 8}) + self.assertTrue(has_declared_coupling(declared, ["z", "x"])) + self.assertFalse(has_declared_coupling(declared, ["x", "z"])) + with self.assertRaisesRegex(DimensionalArityError, "ordered declaration sequence"): + has_declared_coupling(declared, "zx") + self.assertNotEqual(coupling(["z", "x"]), coupling(["x", "z"])) + self.assertNotEqual(declared.couplings[0].charge_state, coupling(["x", "z"], {"x": 1, "z": 8}).charge_state) + geometry = geometry_from_declared_couplings(declared) + self.assertFalse(geometry["zx_equals_xz"]) + self.assertEqual(geometry["couplings"][0]["slot_charges"], (8, 1)) + z_degree = next(item for item in geometry["degree_relations"] if item["dimension"] == "z") + x_degree = next(item for item in geometry["degree_relations"] if item["dimension"] == "x") + self.assertEqual(z_degree["slot_degrees"], ((0, 1),)) + self.assertEqual(x_degree["slot_degrees"], ((1, 1),)) + + def test_xz_and_yz_do_not_give_xyz_without_proof(self) -> None: + declared = space(["x", "y", "z"], [["x", "z"], ["y", "z"]], charges={"x": 1, "y": 1, "z": 8}) + geometry = geometry_from_declared_couplings(declared) + self.assertEqual(tuple(item.arity for item in declared.couplings), (2, 2)) + self.assertFalse(has_declared_coupling(declared, ["x", "y", "z"])) + self.assertFalse(has_declared_coupling(declared, ["x", "y"])) + self.assertFalse(geometry["inferred_higher_arity_from_overlap"]) + self.assertEqual(geometry["structure"]["participating_dimension_count"], 3) + self.assertFalse(geometry["structure"]["ternary_coupling_declared"]) + self.assertFalse(geometry["structure"]["inferred_cartesian_embedding"]) + self.assertEqual( + geometry["structure"]["parts"], + ( + {"coupling": ("x", "z"), "arity": 2, "charge_state": ((1, 8), 1)}, + {"coupling": ("y", "z"), "arity": 2, "charge_state": ((1, 8), 1)}, + ), + ) + self.assertEqual(geometry["couplings"][0]["charge_state"], ((1, 8), 1)) + self.assertEqual(geometry["couplings"][1]["charge_state"], ((1, 8), 1)) + common = geometry["observed_common_ids"] + self.assertEqual(len(common), 1) + self.assertEqual(common[0]["common_ids"], ("z",)) + self.assertFalse(common[0]["proof_of_higher_arity"]) + degrees = {item["dimension"]: item["degree"] for item in geometry["degree_relations"]} + self.assertEqual(degrees["z"], 2) + self.assertEqual(degrees["x"], 1) + self.assertEqual(degrees["y"], 1) + z_slots = next(item for item in geometry["degree_relations"] if item["dimension"] == "z") + self.assertEqual(z_slots["slot_degrees"], ((1, 2),)) + hub_first = geometry_from_declared_couplings( + space(["z", "x", "y"], [["z", "x"], ["z", "y"]], charges={"z": 8, "x": 1, "y": 1}) + ) + other_charges = geometry_from_declared_couplings( + space(["z", "x", "y"], [["z", "x"], ["z", "y"]], charges={"z": 6, "x": 8, "y": 8}) + ) + self.assertEqual( + topology_structure_readout(hub_first["structure"]), + topology_structure_readout(other_charges["structure"]), + ) + self.assertNotEqual( + charged_structure_readout(hub_first["structure"]), + charged_structure_readout(other_charges["structure"]), + ) + + def test_every_instance_has_its_own_zx_and_zy(self) -> None: + declared = space(["z", "x0", "x1", "y0"], [["z", "x0"], ["z", "x1"], ["z", "y0"]]) + self.assertEqual( + oriented_instance_couplings(declared, hub_id="z", instance_ids=["x0", "x1", "y0"]), + (("z", "x0"), ("z", "x1"), ("z", "y0")), + ) + only_one_x = space(["z", "x0", "x1", "y0"], [["z", "x0"], ["z", "y0"]]) + self.assertEqual( + instances_missing_oriented_hub_coupling( + only_one_x, hub_id="z", instance_ids=["x0", "x1", "y0"] + ), + ("x1",), + ) + reversed_slot = space(["z", "x0", "y0"], [["x0", "z"], ["y0", "z"]]) + with self.assertRaisesRegex(DimensionalArityError, "every instance must have declared"): + require_every_instance_has_oriented_hub_coupling( + reversed_slot, hub_id="z", instance_ids=["x0", "y0"] + ) + with self.assertRaisesRegex(DimensionalArityError, "repeated"): + require_every_instance_has_oriented_hub_coupling( + declared, hub_id="z", instance_ids=["x0", "x0"] + ) + + def test_overlap_is_not_an_installable_proof(self) -> None: + declared = space(["x", "y", "z"], [["x", "z"], ["y", "z"]]) + with self.assertRaisesRegex(DimensionalArityError, "not a proof"): + CouplingProof( + conclusion=coupling(["x", "y", "z"]), + premises=(coupling(["x", "z"]), coupling(["y", "z"])), + rule_id="overlap-closure", + ) + with self.assertRaisesRegex(DimensionalArityError, "not a proof"): + CouplingProof( + conclusion=coupling(["x", "y", "z"]), + premises=(coupling(["x", "z"]), coupling(["y", "z"])), + rule_id="hamilton-product-closure", + ) + self.assertFalse(has_declared_coupling(declared, ["x", "y", "z"])) + + def test_explicit_proof_can_install_higher_arity(self) -> None: + declared = space(["x", "y", "z"], [["x", "z"], ["y", "z"]]) + proof = CouplingProof( + conclusion=coupling(["x", "y", "z"]), + premises=(coupling(["x", "z"]), coupling(["y", "z"])), + rule_id="caller-supplied-certificate", + ) + proven = install_proven_coupling(declared, proof) + self.assertFalse(has_declared_coupling(declared, ["x", "y", "z"])) + self.assertTrue(has_declared_coupling(proven, ["x", "y", "z"])) + self.assertEqual(proven.couplings[-1].arity, 3) + + def test_space_rejects_proof_conclusion_that_is_not_declared(self) -> None: + proof = CouplingProof( + conclusion=coupling(["x", "y", "z"]), + premises=(coupling(["x", "z"]),), + rule_id="caller-supplied-certificate", + ) + with self.assertRaisesRegex(DimensionalArityError, "conclusion .* is not declared"): + space(["x", "y", "z"], [["x", "z"]], proofs=(proof,)) + + def test_zx_and_zy_degree_has_z_in_slot_zero_twice(self) -> None: + declared = space(["x", "y", "z"], [["z", "x"], ["z", "y"]]) + degrees = {item.dimension.id: item for item in degree_relations(declared)} + self.assertEqual(degrees["z"].degree, 2) + self.assertEqual(degrees["z"].slot_degrees, ((0, 2),)) + self.assertEqual(degrees["x"].degree, 1) + self.assertEqual(degrees["y"].degree, 1) + self.assertFalse(has_declared_coupling(declared, ["x", "y"])) + self.assertFalse(has_declared_coupling(declared, ["x", "y", "z"])) + + def test_ambient_size_does_not_infer_couplings(self) -> None: + declared = space(["d1", "d2", "d3", "d4", "d5"], []) + geometry = geometry_from_declared_couplings(declared) + self.assertEqual(geometry["couplings"], ()) + self.assertEqual({item["degree"] for item in geometry["degree_relations"]}, {0}) + + def test_arity_five_in_seven_dimensions(self) -> None: + ambient = [f"d{i}" for i in range(1, 8)] + declared = space(ambient, [["d1", "d2", "d3", "d4", "d5"]]) + self.assertEqual(declared.couplings[0].arity, 5) + degrees = degree_relations(declared) + used = {item.dimension.id: item.degree for item in degrees if item.degree} + unused = {item.dimension.id for item in degrees if item.degree == 0} + self.assertEqual(set(used), {"d1", "d2", "d3", "d4", "d5"}) + self.assertEqual(unused, {"d6", "d7"}) + + def test_mixed_arities_in_one_ambient_space(self) -> None: + declared = space( + ["d1", "d2", "d3", "d4"], + [["d1"], ["d2", "d3"], ["d1", "d2", "d3", "d4"]], + ) + self.assertEqual(tuple(item.arity for item in declared.couplings), (1, 2, 4)) + degrees = {item.dimension.id: item.degree for item in degree_relations(declared)} + self.assertEqual(degrees["d1"], 2) + self.assertEqual(degrees["d4"], 1) + + def test_coupling_must_be_subset_of_ambient(self) -> None: + with self.assertRaisesRegex(DimensionalArityError, "undeclared dimensions"): + space(["d1"], [["d1", "d2"]]) + + def test_coupling_cannot_repeat_a_dimension(self) -> None: + with self.assertRaisesRegex(DimensionalArityError, "cannot repeat"): + coupling(["d1", "d1"]) + + def test_common_ids_are_not_a_coupling(self) -> None: + xz = coupling(["x", "z"]) + yz = coupling(["y", "z"]) + self.assertEqual(observed_common_ids(xz, yz), frozenset({"z"})) + self.assertNotEqual(xz, yz) + + def test_four_dimensions_represent_each_local_three(self) -> None: + declared = space( + ["z", "x", "y"], + [["z", "x"], ["z", "y"]], + charges={"z": 8, "x": 1, "y": 1}, + ) + geometry = geometry_from_declared_couplings(declared) + structure = geometry["structure"] + self.assertEqual(structure["participating_dimension_count"], 3) + self.assertEqual(structure["representation_dimension"], QUATERNION_REPRESENTATION_DIMENSION) + self.assertEqual(structure["represented_structure_dimension"], REPRESENTED_STRUCTURE_DIMENSION) + self.assertEqual(structure["representation_kind"], "quaternion") + self.assertEqual(local_three_structures(declared), (("z", "x", "y"),)) + self.assertEqual(len(structure["quaternions"]), 1) + quaternion = structure["quaternions"][0] + self.assertEqual(quaternion["components"], (1, 8, 1, 1)) + self.assertEqual(len(quaternion["components"]), 4) + self.assertEqual(len(quaternion["represented_ids"]), 3) + self.assertEqual(quaternion["axes"][0], QUATERNION_SCALAR_AXIS) + self.assertNotIn(QUATERNION_SCALAR_AXIS, geometry["ambient_ids"]) + self.assertFalse(quaternion["hamilton_product_is_coupling_proof"]) + self.assertFalse(quaternion["scalar_axis_is_ambient"]) + self.assertFalse(has_declared_coupling(declared, ["x", "y", "z"])) + self.assertEqual( + quaternion_structure_readout(structure), + (((1, 8, 1, 1), ("z", "x", "y")),), + ) + two_only = geometry_from_declared_couplings(space(["z", "x"], [["z", "x"]], charges={"z": 1, "x": 1})) + self.assertEqual(two_only["structure"]["participating_dimension_count"], 2) + self.assertEqual(two_only["structure"]["representation_dimension"], 4) + self.assertEqual(two_only["structure"]["quaternions"], ()) + + def test_mixed_charged_and_uncharged_readout_is_stable(self) -> None: + geometry = geometry_from_declared_couplings( + space(["charged", "plain"], [["charged"], ["plain"]], charges={"charged": 1}) + ) + readout = charged_structure_readout(geometry["structure"]) + self.assertEqual( + readout[0], + ( + (1, ((None,), 1), ("plain",)), + (1, ((1,), 1), ("charged",)), + ), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_epac_public_gonol.py b/tests/test_epac_public_gonol.py new file mode 100644 index 0000000..45af768 --- /dev/null +++ b/tests/test_epac_public_gonol.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EPAC_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EPAC_ROOT)) + +from epac_dimensional_arity import space, geometry_from_declared_couplings +from epac_public_gonol import ( + CONSTRUCTOR_ID, + PINNED_PUBLIC_GONOL_SHA256, + PublicGonolConstructionError, + construct_public_gonol, + replay_public_gonol, +) +from ucns import PUBLIC_GONOL_SHA256, native_mobius_state, public_gonol_function + + +class EpacPublicGonolTest(unittest.TestCase): + def test_constructor_is_not_edcm(self) -> None: + receipt = construct_public_gonol( + source_id="epac.test:O", + relation="epac.atomic.element", + identity_glyph="O", + carried_options=(("symbol", "O"), ("Z", "8")), + ) + self.assertEqual(receipt.constructor_id, CONSTRUCTOR_ID) + self.assertEqual(CONSTRUCTOR_ID, "epac.public_gonol") + self.assertEqual(receipt.gonol.identity_glyph, "O") + self.assertEqual(receipt.gonol.carrier_index, public_gonol_function("O").index) + self.assertEqual(PINNED_PUBLIC_GONOL_SHA256, PUBLIC_GONOL_SHA256) + for name in ("epac_public_gonol.py", "epac_periodic.py", "epac_molecular.py"): + source = (EPAC_ROOT / name).read_text(encoding="utf-8") + self.assertNotIn("from edcm", source, name) + self.assertNotIn("import edcm", source, name) + + def test_two_letter_symbol_has_no_single_glyph(self) -> None: + receipt = construct_public_gonol( + source_id="epac.test:He", + relation="epac.atomic.element", + carried_options=(("symbol", "He"), ("Z", "2")), + ) + self.assertIsNone(receipt.gonol.identity_glyph) + self.assertIsNone(receipt.gonol.carrier_index) + + def test_replay_matches(self) -> None: + first = construct_public_gonol( + source_id="epac.test:H", + relation="epac.atomic.element", + identity_glyph="H", + carried_options=(("symbol", "H"), ("Z", "1")), + ) + second = replay_public_gonol(first) + self.assertEqual(first.receipt_digest, second.receipt_digest) + + def test_charged_couplings_are_the_structure(self) -> None: + declared = space( + ["z", "x", "y"], + [["z", "x"], ["z", "y"]], + charges={"z": 8, "x": 1, "y": 1}, + ) + geometry = geometry_from_declared_couplings(declared) + receipt = construct_public_gonol( + source_id="epac.test:H2O-structure", + relation="epac.affixiation.unpaired-valence", + couplings=geometry["couplings"], + structure=geometry["structure"], + ) + self.assertEqual(receipt.structure["participating_dimension_count"], 3) + self.assertFalse(receipt.structure["ternary_coupling_declared"]) + self.assertFalse(receipt.structure["inferred_cartesian_embedding"]) + self.assertEqual( + [part["charge_state"] for part in receipt.structure["parts"]], + [((8, 1), 1), ((8, 1), 1)], + ) + self.assertEqual(native_mobius_state(0).frame.sign, 1) + + def test_nested_geometry_is_frozen_after_closure(self) -> None: + declared = space( + ["z", "x"], + [["z", "x"]], + charges={"z": 8, "x": 1}, + ) + geometry = geometry_from_declared_couplings(declared) + receipt = construct_public_gonol( + source_id="epac.test:frozen-structure", + relation="epac.affixiation.unpaired-valence", + couplings=geometry["couplings"], + structure=geometry["structure"], + ) + geometry["structure"]["parts"][0]["charge_state"] = ((999, 1), 1) + self.assertEqual(receipt.structure["parts"][0]["charge_state"], ((8, 1), 1)) + with self.assertRaises(TypeError): + receipt.structure["parts"][0]["charge_state"] = ((999, 1), 1) + with self.assertRaises(AttributeError): + receipt.structure["parts"].append({}) + self.assertEqual(replay_public_gonol(receipt).receipt_digest, receipt.receipt_digest) + + def test_structure_must_match_declared_couplings(self) -> None: + declared = space( + ["z", "x"], + [["z", "x"]], + charges={"z": 8, "x": 1}, + ) + geometry = geometry_from_declared_couplings(declared) + bad_structure = { + **geometry["structure"], + "parts": ( + { + "coupling": ("z", "x"), + "arity": 2, + "charge_state": ((8, 99), 1), + }, + ), + } + with self.assertRaisesRegex(PublicGonolConstructionError, "structure must match"): + construct_public_gonol( + source_id="epac.test:bad-structure", + relation="epac.affixiation.unpaired-valence", + couplings=geometry["couplings"], + structure=bad_structure, + ) + with self.assertRaisesRegex(PublicGonolConstructionError, "supplied together"): + construct_public_gonol( + source_id="epac.test:missing-structure", + relation="epac.affixiation.unpaired-valence", + couplings=geometry["couplings"], + ) + + def test_unknown_glyph_fails_closed(self) -> None: + with self.assertRaises(PublicGonolConstructionError): + construct_public_gonol( + source_id="epac.test:bad", + relation="epac.atomic.element", + identity_glyph="He", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_geometry_comparison_after_construction.py b/tests/test_geometry_comparison_after_construction.py new file mode 100644 index 0000000..654bdcf --- /dev/null +++ b/tests/test_geometry_comparison_after_construction.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import json +import sys +import unittest +from pathlib import Path + +EPAC_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EPAC_ROOT)) + +from epac_comparison import compare_after_construction, construction_sources_omit_sealed_labels +from epac_dimensional_arity import charged_structure_readout, topology_structure_readout +from epac_molecular import construct_declared_molecules, matched_information_control + + +SEALED = EPAC_ROOT / "data" / "sealed_known_molecular_geometry.json" + + +class GeometryComparisonAfterConstructionTest(unittest.TestCase): + def test_construction_omits_sealed_shape_labels(self) -> None: + self.assertEqual(construction_sources_omit_sealed_labels(), ()) + + def test_charged_couplings_are_the_three_dimensional_structure(self) -> None: + constructions = construct_declared_molecules() + water = constructions["H2O"].receipt.structure + carbon_dioxide = constructions["CO2"].receipt.structure + self.assertIsNotNone(water) + self.assertIsNotNone(carbon_dioxide) + self.assertEqual(water["participating_dimension_count"], 3) + self.assertEqual(carbon_dioxide["participating_dimension_count"], 3) + self.assertFalse(water["ternary_coupling_declared"]) + self.assertEqual( + topology_structure_readout(water), + topology_structure_readout(carbon_dioxide), + ) + water_charged = charged_structure_readout(water) + co2_charged = charged_structure_readout(carbon_dioxide) + self.assertNotEqual(water_charged, co2_charged) + self.assertEqual( + water_charged[0], + ( + (2, ((8, 1), 1), ("O#2", "H#0")), + (2, ((8, 1), 1), ("O#2", "H#1")), + ), + ) + self.assertEqual( + co2_charged[0], + ( + (2, ((6, 8), 1), ("C#0", "O#1")), + (2, ((6, 8), 1), ("C#0", "O#2")), + ), + ) + + def test_sealed_shape_comparison_uses_charged_structure(self) -> None: + constructions = construct_declared_molecules() + self.assertEqual(set(constructions), {"H2", "H2O", "NH3", "CH4", "CO2"}) + record = compare_after_construction() + sealed = json.loads(SEALED.read_text(encoding="utf-8"))["molecules"] + known_shapes = {formula: sealed[formula]["known_shape"] for formula in constructions} + + self.assertTrue(record["opened_after_construction"]) + self.assertTrue(record["construction_omits_sealed_labels"]) + self.assertEqual(record["known_shapes"], known_shapes) + self.assertGreater(len(set(known_shapes.values())), 1) + self.assertEqual(known_shapes["H2O"], "bent") + self.assertEqual(known_shapes["CO2"], "linear") + self.assertEqual(known_shapes["H2"], "linear") + + self.assertTrue(record["topology_collapses_h2o_with_co2"]) + self.assertTrue(record["charged_distinguishes_h2o_from_co2"]) + self.assertTrue(record["linear_class_split_by_charged_structure"]) + + standings = record["standings"] + self.assertEqual(standings["charged_3_structure_as_sealed_shape_prediction"], "FALSIFIED") + self.assertEqual(standings["topology_3_structure_as_sealed_shape_prediction"], "FALSIFIED") + self.assertEqual(standings["ucns_mobius_as_sealed_shape_prediction"], "FALSIFIED") + self.assertEqual(standings["atomic_shells_as_sealed_shape_prediction"], "FALSIFIED") + + control = {f: matched_information_control(c.invariants) for f, c in constructions.items()} + self.assertNotEqual(control["H2O"], control["CO2"]) + self.assertEqual(len(set(control.values())), len(constructions)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_molecular_affixiation.py b/tests/test_molecular_affixiation.py new file mode 100644 index 0000000..b1073fd --- /dev/null +++ b/tests/test_molecular_affixiation.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EPAC_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EPAC_ROOT)) + +from epac_dimensional_arity import quaternion_structure_readout +from epac_molecular import construct_declared_molecules, replay_molecule + + +class MolecularAffixiationTest(unittest.TestCase): + def test_declared_formulas_close_and_replay(self) -> None: + molecules = construct_declared_molecules() + self.assertEqual(set(molecules), {"H2", "H2O", "NH3", "CH4", "CO2"}) + for formula, construction in molecules.items(): + replayed = replay_molecule(construction) + self.assertEqual(construction.receipt.receipt_digest, replayed.receipt_digest, formula) + + def test_unpaired_valence_and_shells_are_used(self) -> None: + molecules = construct_declared_molecules() + water = molecules["H2O"].invariants + methane = molecules["CH4"].invariants + carbon_dioxide = molecules["CO2"].invariants + self.assertEqual(water["center_symbol"], "O") + self.assertEqual(water["center_configuration"], "1s2.2s2.2p4") + self.assertEqual(water["center_unpaired_lm"], ["1:0", "1:-1"]) + self.assertFalse(water["ligand_has_p"]) + self.assertEqual(water["center_used_atomic_promotion"], False) + self.assertEqual(methane["center_used_atomic_promotion"], True) + self.assertEqual(methane["center_unpaired_lm"], ["0:0", "1:-1", "1:0", "1:1"]) + self.assertTrue(carbon_dioxide["ligand_has_p"]) + self.assertEqual(carbon_dioxide["center_unpaired_lm"], ["0:0", "1:-1", "1:0", "1:1"]) + self.assertEqual(carbon_dioxide["center_attachment_site_count"], 4) + self.assertEqual(carbon_dioxide["ligand_attachment_site_count"], 4) + + def test_declared_couplings_are_binary_and_do_not_fill_ambient(self) -> None: + molecules = construct_declared_molecules() + water = molecules["H2O"].invariants["dimensional_geometry"] + self.assertEqual(water["ambient_count"], 3) + self.assertEqual([c["arity"] for c in water["couplings"]], [2, 2]) + ids = [c["declared_ids"] for c in water["couplings"]] + self.assertEqual(len(ids), 2) + self.assertTrue(all(len(item) == 2 for item in ids)) + methane = molecules["CH4"].invariants["dimensional_geometry"] + self.assertEqual(methane["ambient_count"], 5) + self.assertEqual([c["arity"] for c in methane["couplings"]], [2, 2, 2, 2]) + self.assertFalse(any(c["arity"] == 5 for c in methane["couplings"])) + self.assertFalse(methane["inferred_from_ambient"]) + self.assertFalse(methane["inferred_higher_arity_from_overlap"]) + self.assertEqual(water["structure"]["participating_dimension_count"], 3) + self.assertFalse(water["structure"]["ternary_coupling_declared"]) + self.assertFalse(water["structure"]["inferred_cartesian_embedding"]) + self.assertEqual(water["couplings"][0]["slot_charges"], (8, 1)) + self.assertEqual(methane["couplings"][0]["slot_charges"], (6, 1)) + water_receipt = molecules["H2O"].receipt + self.assertEqual(water_receipt.constructor_id, "epac.public_gonol") + self.assertEqual(len(water_receipt.structure["parts"]), 2) + water_instances = molecules["H2O"].invariants["oriented_instance_couplings"] + self.assertEqual(len(water_instances), 2) + self.assertEqual({item[0] for item in water_instances}, {"O#2"}) + self.assertEqual([item[1] for item in water_instances], ["H#0", "H#1"]) + methane_instances = molecules["CH4"].invariants["oriented_instance_couplings"] + self.assertEqual(len(methane_instances), 4) + self.assertTrue(all(item[0] == "C#0" for item in methane_instances)) + self.assertEqual([item[1] for item in methane_instances], ["H#1", "H#2", "H#3", "H#4"]) + self.assertEqual(molecules["H2"].invariants["oriented_instance_couplings"], ()) + water_ids = {name for part in water_receipt.structure["parts"] for name in part["coupling"]} + self.assertEqual(water_ids, {"O#2", "H#0", "H#1"}) + self.assertFalse(any(name.startswith("epac.electron:") for name in water_ids)) + oxygen = next( + item + for item in water_receipt.gonol.participants + if dict(item.carried_options).get("symbol") == "O" + ) + self.assertEqual(len(oxygen.structure["parts"]), 8) + self.assertTrue( + all(part["coupling"][0] == "epac.nucleus:O#2" for part in oxygen.structure["parts"]) + ) + o_nucleus = next(item for item in oxygen.participants if item.relation == "epac.atomic.nucleus") + self.assertEqual( + sum(1 for item in o_nucleus.participants if item.relation == "epac.atomic.neutron"), + 8, + ) + self.assertEqual( + sum(1 for item in o_nucleus.participants if item.relation == "epac.atomic.proton"), + 8, + ) + self.assertFalse(any(name.startswith("epac.neutron:") for name in water_ids)) + self.assertEqual(water_receipt.structure["representation_dimension"], 4) + self.assertEqual(water_receipt.structure["participating_dimension_count"], 3) + self.assertEqual( + quaternion_structure_readout(water_receipt.structure), + (((1, 8, 1, 1), ("O#2", "H#0", "H#1")),), + ) + self.assertEqual( + quaternion_structure_readout(molecules["CO2"].receipt.structure), + (((1, 6, 8, 8), ("C#0", "O#1", "O#2")),), + ) + self.assertEqual(quaternion_structure_readout(molecules["H2"].receipt.structure), ()) + self.assertEqual(len(quaternion_structure_readout(molecules["CH4"].receipt.structure)), 6) + + def test_ucns_coupling_binds_declared_attachments(self) -> None: + molecules = construct_declared_molecules() + signatures = {formula: item.invariants["ucns_coupling_signature"] for formula, item in molecules.items()} + self.assertEqual(len(set(signatures.values())), len(molecules)) + self.assertEqual({signature[0] for signature in signatures.values()}, {"ucns.native-mobius-root-loop"}) + self.assertEqual(len(signatures["CO2"][2]), 4) + self.assertEqual(len(signatures["H2O"][2]), 2) + + def test_construction_text_avoids_sealed_labels(self) -> None: + source = (EPAC_ROOT / "epac_molecular.py").read_text(encoding="utf-8").lower() + for term in ("bent", "tetrahedral", "trigonal-pyramidal", "vsepr", "linear"): + self.assertNotIn(term, source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_periodic_element_gonols.py b/tests/test_periodic_element_gonols.py new file mode 100644 index 0000000..9473ad0 --- /dev/null +++ b/tests/test_periodic_element_gonols.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +EPAC_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EPAC_ROOT)) + +from epac_dimensional_arity import ( + charged_structure_readout, + has_declared_coupling, + quaternion_structure_readout, + space, +) +from epac_periodic import construct_element_gonol, construct_periodic_table, replay_element_gonol + + +class PeriodicElementGonolTest(unittest.TestCase): + def test_constructs_z1_to_z18(self) -> None: + table = construct_periodic_table() + self.assertEqual(len(table), 18) + self.assertEqual(set(table), { + "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", + "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", + }) + carbon = table["C"] + options = dict(carbon.gonol.carried_options) + self.assertEqual(options["Z"], "6") + self.assertEqual(options["electron-configuration"], "1s2.2s2.2p2") + self.assertEqual(options["valence-electrons"], "4") + self.assertEqual(options["unpaired-valence-count"], "2") + self.assertEqual(options["promoted-unpaired-count"], "4") + self.assertEqual(carbon.constructor_id, "epac.public_gonol") + self.assertEqual(len(carbon.gonol.participants), 3) + shells = [item for item in carbon.gonol.participants if item.relation == "epac.atomic.shell"] + electrons = [e for shell in shells for e in shell.participants] + self.assertEqual(len(electrons), 6) + quantum = {(dict(e.carried_options)["n"], dict(e.carried_options)["l"], dict(e.carried_options)["m_l"], dict(e.carried_options)["m_s"]) for e in electrons} + self.assertEqual(len(quantum), 6) + oxygen = table["O"] + self.assertEqual(dict(oxygen.gonol.carried_options)["unpaired-valence-lm"], "1:0,1:-1") + + def test_replay_matches(self) -> None: + first = construct_element_gonol("O") + second = replay_element_gonol(first) + self.assertEqual(first.receipt_digest, second.receipt_digest) + + def test_hund_unpaired_and_shells(self) -> None: + from epac_atomic import atomic_record + + carbon = atomic_record(6) + oxygen = atomic_record(8) + nitrogen = atomic_record(7) + self.assertEqual(len(carbon.electrons), 6) + self.assertEqual(tuple((e.l, e.m_l) for e in carbon.unpaired_valence), ((1, 1), (1, 0))) + self.assertEqual(len(carbon.promoted_unpaired_valence), 4) + self.assertEqual( + len({e.index for e in carbon.promoted_unpaired_valence}), + len(carbon.promoted_unpaired_valence), + ) + self.assertEqual(tuple((e.l, e.m_l) for e in oxygen.unpaired_valence), ((1, 0), (1, -1))) + self.assertEqual(len(nitrogen.unpaired_valence), 3) + self.assertEqual({e.m_l for e in nitrogen.unpaired_valence}, {1, 0, -1}) + + def test_every_electron_instance_has_nucleus_coupling(self) -> None: + oxygen = construct_element_gonol("O") + helium = construct_element_gonol("He") + self.assertIsNotNone(oxygen.structure) + oxygen_readout = charged_structure_readout(oxygen.structure) + self.assertEqual( + oxygen_readout[0], + tuple( + (2, ((8, -1), 1), ("epac.nucleus:O#0", f"epac.electron:O#0:{index}")) + for index in range(8) + ), + ) + nucleus_degree = next( + item for item in oxygen.structure["degree"] if item["dimension"] == "epac.nucleus:O#0" + ) + self.assertEqual(nucleus_degree["degree"], 8) + self.assertEqual(nucleus_degree["charge"], 8) + helium_readout = charged_structure_readout(helium.structure) + self.assertEqual( + helium_readout[0], + ( + (2, ((2, -1), 1), ("epac.nucleus:He#0", "epac.electron:He#0:0")), + (2, ((2, -1), 1), ("epac.nucleus:He#0", "epac.electron:He#0:1")), + ), + ) + ids = {name for part in helium_readout[0] for name in part[2]} + self.assertNotIn("H", ids) + self.assertNotIn("e", ids) + self.assertNotIn("He", ids) + self.assertFalse(helium.structure["ternary_coupling_declared"]) + self.assertEqual(helium.structure["representation_dimension"], 4) + self.assertEqual(helium.structure["participating_dimension_count"], 3) + self.assertEqual( + quaternion_structure_readout(helium.structure), + ( + ( + (1, 2, -1, -1), + ("epac.nucleus:He#0", "epac.electron:He#0:0", "epac.electron:He#0:1"), + ), + ), + ) + hydrogen = construct_element_gonol("H") + self.assertEqual(hydrogen.structure["participating_dimension_count"], 2) + self.assertEqual(hydrogen.structure["representation_dimension"], 4) + self.assertEqual(quaternion_structure_readout(hydrogen.structure), ()) + + def test_nucleus_is_affixiation_of_proton_and_neutron_gonols(self) -> None: + hydrogen = construct_element_gonol("H") + helium = construct_element_gonol("He") + oxygen = construct_element_gonol("O") + h_nucleus = next( + item for item in hydrogen.gonol.participants if item.relation == "epac.atomic.nucleus" + ) + he_nucleus = next( + item for item in helium.gonol.participants if item.relation == "epac.atomic.nucleus" + ) + o_nucleus = next( + item for item in oxygen.gonol.participants if item.relation == "epac.atomic.nucleus" + ) + self.assertEqual([item.relation for item in h_nucleus.participants], ["epac.atomic.proton"]) + self.assertEqual(dict(h_nucleus.carried_options)["neutrons"], "0") + self.assertEqual(h_nucleus.couplings, ()) + self.assertIsNone(h_nucleus.structure) + self.assertEqual( + [item.relation for item in he_nucleus.participants], + [ + "epac.atomic.proton", + "epac.atomic.proton", + "epac.atomic.neutron", + "epac.atomic.neutron", + ], + ) + self.assertEqual(dict(he_nucleus.participants[2].carried_options)["charge"], "0") + self.assertEqual(dict(he_nucleus.participants[0].carried_options)["charge"], "1") + he_ids = {name for part in he_nucleus.structure["parts"] for name in part["coupling"]} + self.assertTrue(all(name.startswith("epac.proton:") or name.startswith("epac.neutron:") for name in he_ids)) + self.assertNotIn("H", he_ids) + self.assertNotIn("e", he_ids) + self.assertFalse(has_declared_coupling( + space( + ["epac.proton:He#0:0", "epac.proton:He#0:1", "epac.neutron:He#0:0", "epac.neutron:He#0:1"], + [part["coupling"] for part in he_nucleus.structure["parts"]], + ), + ["epac.proton:He#0:0", "epac.proton:He#0:1"], + )) + self.assertEqual(len(he_nucleus.structure["parts"]), 4) + self.assertEqual( + quaternion_structure_readout(he_nucleus.structure), + ( + ( + (1, 1, 0, 0), + ("epac.proton:He#0:0", "epac.neutron:He#0:0", "epac.neutron:He#0:1"), + ), + ( + (1, 1, 0, 0), + ("epac.proton:He#0:1", "epac.neutron:He#0:0", "epac.neutron:He#0:1"), + ), + ), + ) + self.assertEqual(len(o_nucleus.participants), 16) + self.assertEqual( + sum(1 for item in o_nucleus.participants if item.relation == "epac.atomic.neutron"), + 8, + ) + electron_ids = { + name + for part in oxygen.structure["parts"] + for name in part["coupling"] + } + self.assertFalse(any(name.startswith("epac.proton:") for name in electron_ids)) + self.assertFalse(any(name.startswith("epac.neutron:") for name in electron_ids)) + + def test_construction_does_not_carry_shape_labels(self) -> None: + receipt = construct_element_gonol("N") + blob = str(receipt.gonol.carried_options) + receipt.gonol.relation + for term in ("bent", "tetrahedral", "trigonal-pyramidal", "vsepr"): + self.assertNotIn(term, blob.lower()) + + +if __name__ == "__main__": + unittest.main()