Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ jobs:
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
cache-dependency-path: tools/orrery-contract/package-lock.json
- name: Sync workspace without country engines
run: uv sync --all-packages --locked
- name: Run engine-free tests
Expand Down
1 change: 1 addition & 0 deletions changelog.d/graph-description-validation.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject whitespace-only graph node and source descriptions at declaration construction while retaining the empty no-description value.
1 change: 1 addition & 0 deletions changelog.d/orrery-schema-cli.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a compiler-derived graph schema, direct Orrery export APIs, and an explicit `python -m microcosm.graph.orrery` command for graph declarations or saved schemas. Recompile imported metadata, compare derived JSON types exactly, retain field providers and declared input roles including expansion-materialization claims, preserve exact large numbers, and verify generated documents with the supported public Orrery package in CI.
1 change: 1 addition & 0 deletions changelog.d/orrery-test-category.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Run Orrery parser compatibility through the registered shared engine-free test category, and reject unregistered behavioral-test jobs in the CI workflow.
39 changes: 27 additions & 12 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ the PEP 420 namespace `microcosm.<x>`: `frame`, `fit`, `calibrate`, `build`,
uv sync --all-packages # set up the whole workspace
uv sync --all-packages --locked --extra us # US engine environment
uv sync --all-packages --locked --extra uk # UK engine environment
uv run pytest # engine-free tests; integration tests remain excluded
bash tools/run_engine_free_tests.sh # complete engine-free test category
uv run pytest <path> # focused test while developing
uv run ruff check . # lint
```

PR CI (`.github/workflows/test.yml`) has `lint`, `engine-free`, `engine-us`,
`engine-uk`, `integration-uk`, and `wheels` jobs.
`engine-uk`, `integration-uk`, and `wheels` jobs, plus the
`select-countries` orchestration job.
`tools/ci_test_plan.py` is the only authority for test-directory ownership, CI
job assignment, country ownership, US timing-report categories, and
changed-path country selection. Its `TEST_GROUPS` registry defines every valid
Expand All @@ -34,13 +36,14 @@ or country mapping.

Each ordinary behavioral job has a Python 3.13/3.14 matrix and reports the 25
slowest tests. The engine-free job installs no country extra, always runs every
engine-free test, and distributes files across two pytest workers with `--dist
loadfile`. Changed-file selection only controls the more resource-intensive
country jobs. A documentation-only pull request selects neither country; a
US-only or UK-only pull request selects that country; shared, mixed, unknown,
or empty changed-path sets select both. Main pushes select both without querying
the pull-request API. The UK integration job follows the same UK selection as
the ordinary UK engine job.
engine-free test, installs its locked JavaScript test dependencies, and
distributes files across two pytest workers with `--dist loadfile`.
Changed-file selection only controls the more resource-intensive country jobs.
A documentation-only pull request selects neither country; a US-only or UK-only
pull request selects that country; shared, mixed, unknown, or empty changed-path
sets select both. Main pushes select both without querying the pull-request API.
The UK integration job follows the same UK selection as the ordinary UK engine
job.

The US engine job runs contract and scenario categories in small pytest
processes with at most two processes active at once, then runs each complete
Expand All @@ -59,9 +62,21 @@ Ordinary behavioral jobs pass `-v --tb=short --maxfail=1 --durations=25`, so eac
names tests as they run, prints a concise first-failure traceback, and reports
its 25 slowest tests.

Every automated test must run from `.github/workflows/test.yml`. Add a new test
group to `TEST_GROUPS` before adding its executor to that workflow; do not create
a separate selection system or test workflow.
Every behavioral or cross-language compatibility assertion must be a pytest
test in a directory registered by `TEST_GROUPS` and must run through that
category's existing workflow job. Supporting programs and data may live under
`tools/`, but they do not receive separate workflow jobs. The test-plan verifier
allows only registered test jobs and the explicitly declared orchestration,
lint, and wheel-building jobs. Add a test category to `TEST_GROUPS` before its
executor; do not create a separate selection system, test workflow, or
single-purpose behavioral test job.

The Orrery parser compatibility assertion lives in
`packages/microcosm-graph/tests/engine_free/shared/test_graph_orrery.py`. The
engine-free runner installs the supported public Orrery range and locked Node
dependency set under `tools/orrery-contract/`; the pytest test generates a
document through Microcosm's public Python API and requires Orrery's public
parser to accept it. It performs no browser rendering.

New commits to a PR cancel older unfinished CI runs for that same PR.
Each main-push run has a unique concurrency group, so all main-push runs
Expand Down
10 changes: 10 additions & 0 deletions docs/graph-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,16 @@ lock unchanged:
recorded the observer opt-in as amendment 25 in the meantime; amendment 26
above was 25 in the same lane.

28. **Descriptions are empty or contain non-whitespace text.**
`SourceRef.description` and `Node.description` keep the empty string as the
explicit no-description sentinel and reject every nonempty value whose
characters are all whitespace. This makes the declaration boundary enforce
the text contract shared by presentation adapters instead of allowing a
graph to compile and later produce a document that its consumer rejects.
Descriptions remain descriptive: they stay outside `Node.normative()`, so
this validation changes no graph computation key. `decl.py` is re-locked.
Adopted during the Orrery export review in #888.


Adding a normative field with a default changes the canonical projection
of every node that carries it, so node keys moved with amendments 11 and
Expand Down
5 changes: 5 additions & 0 deletions docs/graph-explorer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Graph explorer

For declaration and schema inspection with Orrery, see the
[Orrery adapter](orrery-adapter.md). The run
renderer described below remains unchanged, including its separate cache and
gate statuses.

The graph explorer is one self-contained HTML file generated from a compiled
graph and its run manifest. It contains its own CSS, JavaScript, DAG, and small
charts. A reviewer can copy the file to another machine and open it directly in
Expand Down
2 changes: 1 addition & 1 deletion docs/graph-interface.lock
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
b25ae4a62fd2777a5dffa30ecbba7d345a804b74b9d6b6e68c2a5d38f3e0a129 decl.py
6f9271abf7a2f5157b0423e0e79007b5f20c5bb5c431262baff7a104eb91d502 decl.py
24045ab2a62295874520d7940c7f7b59be9d909a3cfbf7e111a734e369b0815b kernel.py
182 changes: 182 additions & 0 deletions docs/orrery-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Orrery adapter

`microcosm.graph.orrery` converts a Microcosm graph declaration and its
compiler-derived metadata into `graph-explorer/v1`, the JSON format accepted by
[Orrery](https://github.com/TheAxiomFoundation/orrery). Microcosm owns the
calculation and field semantics. Orrery owns graph navigation, presentation,
and self-contained HTML export.

This conversion is separate from graph execution:

1. `compile_graph(graph)` validates a `Graph` and derives operation order,
population versions, field owners, and operation predecessors. The executor
uses this `CompiledGraph`.
2. `graph_schema(compiled)` derives a portable static metadata artifact from
that same `CompiledGraph`.
3. `orrery_json(compiled)` converts the static metadata into Orrery input. It
does not execute the graph.
4. The separately installed Orrery command can turn that JSON into a
self-contained HTML file.

The schema is therefore a generated artifact, not an instruction that
Microcosm executes. The direct API performs steps 2 and 3 in memory. A saved
schema lets another process repeat step 3 without importing the original graph
construction code.

## Export from Python

Use the public API when the `Graph` or `CompiledGraph` is already available:

```python
from pathlib import Path

from microcosm.graph import compile_graph, orrery_json

compiled = compile_graph(graph)
Path("orrery.json").write_text(
orrery_json(compiled, title="Microcosm UK"),
encoding="utf-8",
)
```

Passing the original `Graph` to `orrery_json` produces the same bytes because
the function compiles it first. `orrery_document` returns the same result as
detached Python dictionaries and lists.

To persist the intermediate compiler schema, serialize `graph_schema(compiled)`
with `microcosm.graph.canonical.canonical_json`. The schema has exactly these
root fields:

```text
protocol, country, graph_sha256, graph, compiled,
fields, input_bindings, extensions
```

Only `extensions` is producer-defined. Before presentation,
`validate_graph_schema` restores and recompiles the embedded graph and requires
every other field to equal the newly derived result. A caller cannot change an
owner, predecessor, population version, field provider, or input binding while
retaining a valid schema.

## Export from the command line

The command requires an explicit input type and creates a new output file:

```sh
uv run python -m microcosm.graph.orrery \
--graph graph.json \
--output orrery.json \
--title "Microcosm UK"
```

For a previously saved compiler schema, use `--schema` instead of `--graph`:

```sh
uv run python -m microcosm.graph.orrery \
--schema compiled-schema.json \
--output orrery.json
```

The command does not infer the input type. It rejects duplicate JSON keys,
non-finite numbers, streams and devices, oversized input, an invalid graph or
schema, and an existing output path. It does not install Orrery, download
assets, or execute a population operation.

Install the viewer independently, then create the HTML file:

```sh
npm install --save-dev @axiom-foundation/orrery
npx orrery --input orrery.json --output orrery.html
```

Orrery validates the JSON before export. Its HTML contains the viewer assets
and does not require a network connection to render. Microcosm CI parses a
generated document with the compatible range declared by its contract-test
package and a locked set of transitive dependencies.

## Fields and input bindings

Every field record identifies one visible value by:

- `population`, `entity`, and `column`: its coordinate;
- `provider`: the operation whose artifact supplies that value;
- `declared_in`: the operation containing the nearest applicable `Owned`
declaration;
- `dtype`, `rows`, `ownership`, and `rewrite`: the declaration details.

A structural operation carries all fields from its base population. For such a
carried field, `provider` is the structural operation because its output
artifact is what downstream operations read; `declared_in` still points to the
earlier `Owned` declaration. An operation that writes or rewrites a field is
the provider after that operation completes.

`input_bindings` are also derived entirely from `Graph` and `CompiledGraph`.
They contain no Frame values and do not observe kernel behavior. Each record
connects one declared input role to the exact pre-operation field provider:

| `kind` | Declared role | `rows` |
| --- | --- | --- |
| `slice` | A column named by a `Slice` | The slice's row selector |
| `slice_mask` | The boolean column used to select a slice's rows | `all` |
| `output_mask` | The boolean column limiting an `Owned` output | `all` |
| `rewrite_incumbent` | The prior value replaced by `Owned(rewrite=True)` | The output's row selector |
| `materialized_expand_output` | A new column physically installed by an `EXPAND` operation and read by its following ownership-claim operation | The output's row selector |

For a rewrite, both the explicit slice and the implicit incumbent role resolve
to the value present before that operation. The final rewritten field is a
different identity. Repeated declared roles remain repeated schema records,
even when they refer to the same coordinate.

For `materialized_expand_output`, the input field's provider is the `EXPAND`
operation and its declaration source is the following claim operation. The
claim's final output receives a separate field identity, preserving both the
physical materialization and the ownership declaration in the presentation.

These records describe values supplied to an operation. They do not claim that
a kernel read every supplied value at runtime, and they do not reproduce the
executor's complete causal-writer history, which may be refined by runtime
receipts.

## Orrery document structure

The adapter creates operation, source, and versioned-field nodes. Descriptions
are promoted to Orrery's visible `description` property while the complete
declaration remains in `data`. The entire compiler schema remains available at
`metadata.microcosm`.

| Relationship | Category | Meaning |
| --- | --- | --- |
| `compiled_predecessor` | dependency | An operation dependency derived by `compile_graph` |
| `declared_read` | dependency | A slice, mask, or rewrite-incumbent input binding |
| `artifact_input` | dependency | A typed producer-to-consumer artifact declaration |
| `provided_field` | provenance | The operation that supplies a versioned field |
| `structural_input` | provenance | A base population field carried into a structural operation |
| `declared_source` | provenance | A named external input declaration |

Structural carriage does not assert that values are unchanged. Declared reads
do not infer an individual mathematical formula for each output. Artifact and
source declarations do not establish that the referenced bytes exist or were
validated.

Pre-rewrite values receive auxiliary field nodes when necessary. Stable IDs
include the population, coordinate, provider, and declaration source, so the
pre-rewrite value and final rewritten value cannot collide.

## Evidence and limits

The export contains static declarations only. It does not contain entity IDs,
memberships, field values, source bytes, kernel code, observed runtime reads,
cache results, execution receipts, verifier assessments, or release decisions.
Content revisions identify metadata bytes; they are not execution keys or
authenticity claims.

Python integers outside JavaScript's safe range are represented as
`{"integer_literal": "..."}` before Orrery parses the document. Large integral
floats use `{"float_literal": "..."}`. Exports reject non-finite numbers,
excessive nesting, non-JSON objects, more than 20,000 presentation nodes or
100,000 relationships, input above 32 MiB, and output above 64 MiB. They fail
instead of silently omitting records.

Runtime receipts and precise per-output value lineage could be added by a
separate runtime-aware adapter in the future. They cannot be inferred from the
static compiler schema and are intentionally absent here.
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,63 @@ def test_registry_directories_are_unique() -> None:
assert len(directories) == len(set(directories))


def test_workflow_jobs_match_registered_test_and_infrastructure_jobs() -> None:
source = ci_test_plan.WORKFLOW.read_text(encoding="utf-8")

assert ci_test_plan.workflow_job_errors(source) == ()


def test_workflow_rejects_a_separate_behavioral_test_job() -> None:
source = """\
jobs:
engine-free:
steps:
- run: pytest
invented-compatibility-test:
steps:
- run: node verify.mjs
"""

assert (
"invented-compatibility-test: workflow job has no registered test "
"category or approved infrastructure role"
in ci_test_plan.workflow_job_errors(source)
)


def test_workflow_rejects_an_underscore_prefixed_job() -> None:
source = """\
jobs:
engine-free:
steps:
- run: pytest
_invented-compatibility-test:
steps:
- run: node verify.mjs
"""

assert (
"_invented-compatibility-test: workflow job has no registered test "
"category or approved infrastructure role"
in ci_test_plan.workflow_job_errors(source)
)


def test_workflow_job_parser_ignores_nested_yaml_keys() -> None:
source = """\
jobs:
engine-free:
strategy:
matrix:
python-version: ["3.13", "3.14"]
steps:
- name: Run tests
run: pytest
"""

assert ci_test_plan.workflow_job_names(source) == ("engine-free",)


def test_engine_guard_detection_ignores_strings_but_finds_executable_guards() -> None:
source = """
TEXT = '@pytest.mark.requires_uk'
Expand Down
Loading
Loading