diff --git a/.github/fep-lean-pair.json b/.github/fep-lean-pair.json index de94408c5..a6a921eb4 100644 --- a/.github/fep-lean-pair.json +++ b/.github/fep-lean-pair.json @@ -1,4 +1,4 @@ { "repository": "ActiveInferenceInstitute/fep_formal", - "revision": "184ee53f55b526fdc858e2d962eba16e6066fb19" + "revision": "0d980814b8a4cbf167cefbf262143b6498feeb86" } diff --git a/docs/development/backend_scientific_contracts.md b/docs/development/backend_scientific_contracts.md index f61ac1a3f..e9fe82e60 100644 --- a/docs/development/backend_scientific_contracts.md +++ b/docs/development/backend_scientific_contracts.md @@ -171,3 +171,49 @@ estimates tensor allocation before composition and sampling; process RSS include JAX runtime/compilation and is recorded separately. Estimated allocation is not measured peak RSS. Unsupported model families or structural-zero mappings are reported explicitly, without clipping or silently normalizing their values. + + +## Authored planning horizons + +The PyMDP, ordinary JAX, RxInfer, ActiveInference.jl, PyTorch and NumPyro +planning adapters admit one-step scoring. Source-authored +`planning_horizon`, `policy_len` and `policy_horizon` declarations must agree +and equal one. A declared `num_policies` must equal an unambiguous declared +`num_actions`. Positive integer values are required; Boolean, string and +fractional values are refused. Explicit `policies`/`policy_sequences` are +refused because these adapters do not consume their authored identity/order. +The alternative JAX POMDP solver and combined demonstration refuse all explicit +planning declarations because they use their own solver/demo defaults. +Declarations in the model-parameter, initial-parameter and parser parameter-list +surfaces are checked without hiding conflicts through precedence. + +Unsupported declarations produce `unsupported-planning:` before script writing +through the direct renderer, single-spec interface and Step 11 compatibility +gate. These checks do not alter sources, choose a pruned subset, reduce a horizon +or claim that one-step action scores implement cumulative multi-step EFE. +`discrete/deep_planning_horizon.md` therefore remains an authored research model +whose five-step/64-policy planning question is unsupported by those adapters. +Its source does not specify the retained 64 sequences or a pruning algorithm. + +Separately named JAX execution contracts and the finite continuous cpomdp +contract retain their own horizon rules. Stan and bnlearn filtering/smoothing, +THRML fixed-action smoothing and DisCoPy diagrams do not gain planning semantics +from this admission rule. Independent source-to-numerics witnesses and broader +planning contracts remain E4 work; successful backend execution alone is not a +proof of the model's complete prose semantics. + + +## Source text and path identity + +`gnn.validate_gnn_source(source, is_content=True)` and +`gnn.validate_gnn_syntax(source, is_content=True)` preserve literal source text, +including text that happens to name a real file. The public dictionary facade +delegates mode selection once to the formal validator; it does not probe a path, +read a document, then classify the document's contents as another filename. +Default mode retains the existing-file heuristic. Long Unicode text, embedded +NULs and empty text reach the validator without an unguarded path probe. + +Both modes use the same private, lossless markdown staging and normative schema +validation. Required-section failures remain failures; source-mode selection is +separate from backend capability or numerical fidelity. Explicit content mode +also treats a `Path` object as its literal string rather than reading that path. diff --git a/docs/development/remaining_roadmap_contracts.md b/docs/development/remaining_roadmap_contracts.md index 1670ce055..7cadc4c01 100644 --- a/docs/development/remaining_roadmap_contracts.md +++ b/docs/development/remaining_roadmap_contracts.md @@ -101,6 +101,17 @@ change EFE sign, omit cross covariance, substitute a source digest and change precision. Each must fail or refuse with a quantity-specific diagnosis. Add a new witness version before extending its scientific meaning. +### E4-P1: authored planning admission + +The shared `render/planning_admission.py` guard refuses unsupported source +horizons and policy identities on the six ordinary one-step planning adapters, +including direct and Step 11 entry points. It checks all supported parameter +containers before canonicalization can hide conflicts. This closes silent +one-step fallback for these explicit declarations only. The tracked five-step, +64-policy example retains its original source and remains unsupported for +planning until it supplies an executable policy-set/pruning contract. Broader +policy search and independent numerical qualification remain open. + ### E4-BN1: finite bnlearn source filtering The new `render/bnlearn/source_program.py` installs declared finite A/B/D CPTs diff --git a/input/gnn_files/multiagent/multi_agent_coordination_acceptance.md b/input/gnn_files/multiagent/multi_agent_coordination_acceptance.md index 0c8fd2638..d64efc9e9 100644 --- a/input/gnn_files/multiagent/multi_agent_coordination_acceptance.md +++ b/input/gnn_files/multiagent/multi_agent_coordination_acceptance.md @@ -43,3 +43,6 @@ Dynamic s=HiddenState o=Observation u=Action + +## Footer +Multi-Agent Coordination Acceptance Fixture diff --git a/output/data/manuscript_render_manifest.json b/output/data/manuscript_render_manifest.json index 6d9074292..bcb20c2b4 100644 --- a/output/data/manuscript_render_manifest.json +++ b/output/data/manuscript_render_manifest.json @@ -1,47 +1,47 @@ { - "counts_describe_commit": "4ed8b33", + "counts_describe_commit": "48db291", "manifest_version": "gnn_render_manifest_v1", - "receipt_counts_describe_commit": "4ed8b33", + "receipt_counts_describe_commit": "48db291", "recorded_by": "uv run python scripts/z_record_manuscript_render_manifest.py", "render_artifacts_normalized_sha256": { - "output/pdf/_combined_manuscript.log": "1b46412330c82f3d1a22a5654fa24d3a8926c8b8f2906118c142360d7f6389f9", - "output/pdf/_combined_manuscript.md": "f8d7d333a127274c8068e4dd0640ecb2b88544053d69e3930d3dcc0fd1291e93", - "output/pdf/_combined_manuscript.tex": "bbc7b51b5606eb5558518d56e02ab55ddb5377d146993ee68ba810c246778beb" + "output/pdf/_combined_manuscript.log": "3f8fc9495762bc1d2b66f668c470a4d6fee6792c6bff8da2d1d55e416725d5c2", + "output/pdf/_combined_manuscript.md": "055874270cc69740c3a5549ed48d695b74ee444aa8c933b4def85c99b196f138", + "output/pdf/_combined_manuscript.tex": "b03354e0b47272f3a611f600f46843aa96132dbea6811dff2a246599b1ea9b0b" }, "render_artifacts_sha256": { - "output/pdf/_combined_manuscript.log": "be663fe9a5fdb54b885ca1f2aba03bc8325a95c267377af82c2e3e7e49071803", - "output/pdf/_combined_manuscript.md": "27acaca71d11d3a887658df1ae0eebb1683e752b2fda1f72862c36d230c7b13b", - "output/pdf/_combined_manuscript.tex": "ee7f039e06d573825093dae08f580b3c0582401287c5378ddc2a8ca0f725bfdc" + "output/pdf/_combined_manuscript.log": "82794bc13a0d48150cd6390c4ce8f63ffccf297bd1ce04a2a30db0758cf4b2c7", + "output/pdf/_combined_manuscript.md": "3ac83f8cae6e585a4d080d3f14516990599cea58dcc5ed46c108976dc5cc716c", + "output/pdf/_combined_manuscript.tex": "e5d83a4477da3286a89dcb7fb844ba198637ccfdcbe2301e2574c8c6f8b271c3" }, "render_inputs_normalized_sha256": { "output/manuscript/00_abstract.md": "a66f146435f8300a5bfa2b3fbbc60373dec0e2e3c3fa4de9856f71e8b7eef27e", "output/manuscript/01_introduction.md": "5d0d444605725927d88c415bbd7b2b6d27dfe62d33130cddc3cdbc4669ccb771", "output/manuscript/02_system_context.md": "8da04dfdff5ba163f4c9e944150cff92de19d9ab76e3bf00fda1ee4c279a2e5b", "output/manuscript/03_methods.md": "b23b0a2717f6e4006c6016672ab8ff9f319494a29584e838970e3ffcaa3fe082", - "output/manuscript/04_artifacts_and_evidence.md": "53a557380bfe681f84830018b99f51ae4e36f2445e4a78993ace1a5bc53b7e45", - "output/manuscript/05_reproducibility.md": "ef7da6b1d821b4129053458c46bca80048a50a26cad59348aaa5aa21475a45bb", + "output/manuscript/04_artifacts_and_evidence.md": "8ed3e1144e9cb4e449acc09ed36c1e1999e7594bd3bbe4223cf0b45812947e21", + "output/manuscript/05_reproducibility.md": "91852ba88dac3ec4b708809a4d85222001cbacd4f350ded2386ee0513d57fc5c", "output/manuscript/06_limitations_and_next_steps.md": "86fdf003b8f486ce262699aca59d5b6bf2cea5b4ff9916acb87399d225a12d6e", "output/manuscript/98_symbols_glossary.md": "4342b4bac17e063cd34f0e0805afe4769048efb22beced4887080ff198577c60", "output/manuscript/99_references.md": "5e80feaa0ff39228501260f8e18f34debc071e1431de9c81a48016cb428aeef2", - "output/manuscript/S01_source_surface.md": "5d707901f2045b7d39c925f51551558c1a44928257b7025411b80045b2fd2eea", + "output/manuscript/S01_source_surface.md": "a098d8e41abc250a4586e9aadda7e8e61bcdd0d58084ac7ef835ba33f4327761", "output/manuscript/config.yaml": "94d56ef0b383c482d2807b49b9ed5df26705009203746aa4e06a2819313eebfa", "output/manuscript/preamble.md": "ebdb657038ccf007dffefe5e3d5834145b956e6580628356975d741bcd6fb01e", "output/manuscript/references.bib": "ed44f4594867a193123aaf03334e12c2f12e167584d42ac5bc6074c4bfec9f0d" }, "render_inputs_sha256": { - "output/manuscript/00_abstract.md": "9f41eac73a95505cccade998c67fc0a2245a1b079f9b6b378ff8307c6f9de50e", + "output/manuscript/00_abstract.md": "40e3726fbb3d6d491f517ae30e56c1b455315e00047bd3d43beb4543d718cbb0", "output/manuscript/01_introduction.md": "5d0d444605725927d88c415bbd7b2b6d27dfe62d33130cddc3cdbc4669ccb771", - "output/manuscript/02_system_context.md": "3e6e912afca37b0c441b78f8c0059679b9f3698823ebc022cdb1014cda777249", - "output/manuscript/03_methods.md": "d8e8176ceaf484f5081ae251273f5e90ee46ea26cca7e5c1ab7357dd8545b6e6", - "output/manuscript/04_artifacts_and_evidence.md": "58a5d4ddb764086e2acd7cd46f701cda41290cffa8dabfdb9777fc0a4a1cd403", - "output/manuscript/05_reproducibility.md": "c454fc494d108f76b99b4963ce567d7d04b63825336079c8f0d1955d96dbec6b", + "output/manuscript/02_system_context.md": "d86338576cdd98823e43bfa47080b6fbf7db0bf3f846687c12a0750785e0104c", + "output/manuscript/03_methods.md": "d08ed236de295a5cb848b3d02e7877a0c631299e62cf2a50aa777def131c32f3", + "output/manuscript/04_artifacts_and_evidence.md": "85bced0b1d8b13d73cc1aafffbc96c0592ca262f468467d34d733d5ed4ba78da", + "output/manuscript/05_reproducibility.md": "66e147c56206ea5e09557b6e0acd18fe8227e9b55627e7f87d9e5581b447be52", "output/manuscript/06_limitations_and_next_steps.md": "86fdf003b8f486ce262699aca59d5b6bf2cea5b4ff9916acb87399d225a12d6e", "output/manuscript/98_symbols_glossary.md": "4342b4bac17e063cd34f0e0805afe4769048efb22beced4887080ff198577c60", "output/manuscript/99_references.md": "5e80feaa0ff39228501260f8e18f34debc071e1431de9c81a48016cb428aeef2", - "output/manuscript/S01_source_surface.md": "5d707901f2045b7d39c925f51551558c1a44928257b7025411b80045b2fd2eea", + "output/manuscript/S01_source_surface.md": "a098d8e41abc250a4586e9aadda7e8e61bcdd0d58084ac7ef835ba33f4327761", "output/manuscript/config.yaml": "94d56ef0b383c482d2807b49b9ed5df26705009203746aa4e06a2819313eebfa", "output/manuscript/preamble.md": "ebdb657038ccf007dffefe5e3d5834145b956e6580628356975d741bcd6fb01e", "output/manuscript/references.bib": "ed44f4594867a193123aaf03334e12c2f12e167584d42ac5bc6074c4bfec9f0d" }, - "variables_sha256": "a8751fd723486c28d7a7975e8126013211ab177f3c2a6a48108936a72762799a" + "variables_sha256": "abfea8c9b090da4eb05047d80315e8f178baa391f1178301aa590d83180fa21c" } diff --git a/output/data/manuscript_variables.json b/output/data/manuscript_variables.json index efe99eeb1..e6da3dc5f 100644 --- a/output/data/manuscript_variables.json +++ b/output/data/manuscript_variables.json @@ -23,7 +23,7 @@ "GNN_FAMILY_TARGET_DIR_COUNT": "9", "GNN_FIRST_AUTHOR": "Daniel Ari Friedman", "GNN_FRAMEWORK_TABLE": "| Framework | Discrete render | Continuous render | Executor status |\n|---|---|---|---|\n| PyMDP | yes | unsupported | executor |\n| RxInfer.jl | yes | yes | executor |\n| ActiveInference.jl | yes | unsupported | executor |\n| JAX | yes | yes | executor |\n| DisCoPy | yes | unsupported | executor |\n| PyTorch | yes | yes | executor |\n| NumPyro | yes | yes | executor |\n| Stan | yes | yes | executor |\n| bnlearn | yes | unsupported | executor |\n| cpomdp | no | yes | executor |\n| THRML | yes | unsupported | executor |\n| ngc-learn | no | yes | executor |\n: Model kinds each render framework supports, read entirely from the flags in `src/gnn/render/framework_registry.py` — the same source as [@tbl:backend_registry]. A continuous cell of `unsupported` is the renderer's own status for continuous-state models; continuous-only targets do not render categorical models. An executor entry declares dispatch support, not acceptance of every model or inference mode; a render-only entry has no Step-12 executor. Read with [@tbl:model_kinds], which groups the same flags by model kind. {#tbl:framework_capability}", - "GNN_GIT_COMMIT": "4ed8b33", + "GNN_GIT_COMMIT": "48db291", "GNN_INPUT_FAMILY_DIR_COUNT": "12", "GNN_KEYWORDS": "GNN, active inference, generative models, model notation, pipeline automation", "GNN_LICENSE": "CC-BY-NC-SA-4.0", @@ -45,9 +45,9 @@ "GNN_RELEASE_CODENAME": "Public contracts and native boundaries", "GNN_RELEASE_DATE": "2026-10-08", "GNN_REPO_URL": "https://github.com/ActiveInferenceInstitute/Generalized_Notation_Notation", - "GNN_SRC_LOC": "225800", + "GNN_SRC_LOC": "225920", "GNN_SRC_PACKAGE_COUNT": "44", - "GNN_SRC_PY_FILE_COUNT": "766", + "GNN_SRC_PY_FILE_COUNT": "767", "GNN_STEP_ADVANCED_VIZ": "9", "GNN_STEP_ANALYSIS": "16", "GNN_STEP_AUDIO": "15", @@ -80,9 +80,9 @@ "GNN_STEP_WEBSITE": "20", "GNN_SUBTITLE": "A text language and modular processing pipeline for Active Inference generative models", "GNN_TARGET_DIR_COVERAGE_NOTE": "All 9 registered family target directories lie inside that tree, so a single invocation reaches every registered family.", - "GNN_TEST_COUNT": "6790", - "GNN_TEST_FILE_COUNT": "658", - "GNN_TEST_FUNCTION_COUNT": "6790", + "GNN_TEST_COUNT": "6809", + "GNN_TEST_FILE_COUNT": "660", + "GNN_TEST_FUNCTION_COUNT": "6809", "GNN_TITLE": "GeneralizedNotationNotation: A Text Language for Active Inference Models", "GNN_TOKEN_COUNT": "93", "GNN_TOOL_COUNT": "164", diff --git a/output/data/manuscript_variables_receipt.json b/output/data/manuscript_variables_receipt.json index 96f54456d..949a9e5b8 100644 --- a/output/data/manuscript_variables_receipt.json +++ b/output/data/manuscript_variables_receipt.json @@ -1,6 +1,6 @@ { - "counts_describe_commit": "4ed8b33", - "dirty_path_count": 11, + "counts_describe_commit": "48db291", + "dirty_path_count": 10, "dirty_paths": [ "output/.pipeline/", "output/data/manuscript_variables.json", @@ -11,8 +11,7 @@ "output/manuscript/02_system_context.md", "output/manuscript/03_methods.md", "output/manuscript/04_artifacts_and_evidence.md", - "output/manuscript/05_reproducibility.md", - "output/manuscript/S01_source_surface.md" + "output/manuscript/05_reproducibility.md" ], "generator": "scripts/z_generate_manuscript_variables.py", "git_available": true, diff --git a/output/figures/figure_registry.json b/output/figures/figure_registry.json index 9c0892cad..c412f64e6 100644 --- a/output/figures/figure_registry.json +++ b/output/figures/figure_registry.json @@ -71,13 +71,13 @@ "GNN_MCP_TOOL_COUNT": "164", "GNN_SRC_PACKAGE_COUNT": "44", "GNN_STEP_COUNT": "25", - "GNN_TEST_FILE_COUNT": "658", + "GNN_TEST_FILE_COUNT": "660", "GNN_VERSION": "4.1.0" }, "filename": "gnn_repo_metrics.png", "generated_by": "scripts/manuscript_fig_repo_metrics.py", "label": "fig:repo_metrics", - "png_sha256": "ed52c9dc28d588cef5fb1aead3fb526601bfe01d595f813d61e23732c16a33be", + "png_sha256": "f34f344caf64fc88f67f16ebc767110dd305df4d2c566a5a3399e4418d947893", "source_sha256": {} }, { diff --git a/output/figures/gnn_repo_metrics.png b/output/figures/gnn_repo_metrics.png index ec8a4b455..41571f81e 100644 Binary files a/output/figures/gnn_repo_metrics.png and b/output/figures/gnn_repo_metrics.png differ diff --git a/output/manuscript/00_abstract.md b/output/manuscript/00_abstract.md index 5d9e7f6a0..0d218b2df 100644 --- a/output/manuscript/00_abstract.md +++ b/output/manuscript/00_abstract.md @@ -1,5 +1,5 @@ # Abstract {#sec:abstract} -![The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 4ed8b33. A plain-text specification — the `StateSpaceBlock` declaring the model's tensors, `A`, `B`, `C`, `D`, and `E` for the discrete categorical kind of [@eq:generative_model] through [@eq:policy] and its own matrix vocabulary for the other model kinds — is parsed (Step 3), type-checked and validated (Steps 5–6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check — every discrete `B` literal is read as column-stochastic `B[s', s, a]`, and `--transpose-b` maps row-stochastic textbook literals onto that canonical order — and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools.](../output/figures/gnn_graphical_abstract.png){#fig:graphical_abstract width=100%} +![The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 48db291. A plain-text specification — the `StateSpaceBlock` declaring the model's tensors, `A`, `B`, `C`, `D`, and `E` for the discrete categorical kind of [@eq:generative_model] through [@eq:policy] and its own matrix vocabulary for the other model kinds — is parsed (Step 3), type-checked and validated (Steps 5–6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check — every discrete `B` literal is read as column-stochastic `B[s', s, a]`, and `--transpose-b` maps row-stochastic textbook literals onto that canonical order — and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools.](../output/figures/gnn_graphical_abstract.png){#fig:graphical_abstract width=100%} Active Inference offers a unifying account of perception, learning, and action under the free energy principle [@friston2010], yet the generative models at its core are still communicated ad hoc: scattered across prose descriptions, bespoke notebooks, and framework-specific code that rarely agree. This fragmentation makes published models hard to reproduce, compare, or port between tools, and it raises the barrier for newcomers learning the formalism [@parr2022]. GeneralizedNotationNotation (GNN) addresses this gap with a standardized, human- and machine-readable text language for specifying Active Inference generative models, paired with a 25-step processing pipeline that transforms a single specification into validation, visualization, simulation, and analysis artifacts [@gnn2023]. A GNN file denotes a generative model, and the language lets that model take the several shapes the field actually uses: the notation blocks a file declares — categorical `A`/`B`/`C`/`D`[/`E`] tensors, linear-Gaussian system matrices `F`/`H`/`Q`/`R` with a Gaussian prior, per-agent and per-level compositions — fix its model kind, which the pipeline classifies structurally and carries through rendering and execution, recording explicitly which registered backends can render and run a model of each kind and which report it unsupported. GNN's "Triple Play" treats each model as three coordinated views — a textual specification, graphical visualizations, and executable cognitive models — so that outputs share an authored source and can be checked for consistency through identity, parameter, semantic, and numerical receipts. The framework spans 38 exemplar specifications (29 discrete-state and 9 continuous linear-Gaussian) across 9 model families and 12 registered rendering backends, with explicit gates for cross-format semantic fidelity and cross-framework reliability. GNN 3.0.0 layered safe-by-design long-running orchestration on top of this language and pipeline — durable observation streams, resumable run sessions, and auditable container plans that generate, validate, and replay data only, with no live infrastructure mutation. We describe the language, the model kinds it denotes, the pipeline architecture, and the validation contracts and source-bound checks used to assess specification round trips and admitted executions across simulation backends — with coverage gaps recorded as explicit profiled-unsupported statuses rather than silently omitted — providing inspectable infrastructure for reproducing and comparing Active Inference models within the recorded capability and verification boundaries. diff --git a/output/manuscript/02_system_context.md b/output/manuscript/02_system_context.md index 96a28faac..856509518 100644 --- a/output/manuscript/02_system_context.md +++ b/output/manuscript/02_system_context.md @@ -128,7 +128,7 @@ The governance runs in the direction the taxonomy promises: classification is co The pipeline is a fixed sequence of 25 numbered steps, 0–24, each a self-contained stage that consumes the artifacts of its predecessors and writes typed outputs for those that follow. Early steps parse and type-check the GNN text and validate it against the language schema; middle steps render visualizations, export the model to executable backends, and run simulations; later steps perform analysis, reporting, and downstream integration. The declared orchestration prerequisites among the steps form the directed acyclic graph shown in [@fig:pipeline]. The graph records which steps must precede others; artifact receipts separately identify source inputs and produced files, including optional enrichments. -![The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under `src/gnn/` named in [@tbl:pipeline_steps]; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of `src/gnn/STEP_INDEX.md`, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from `src/gnn/STEP_INDEX.md` at commit 4ed8b33; its footer restates the step count, the number of required prerequisites, and the layout rule.](../output/figures/gnn_pipeline_dag.png){#fig:pipeline width=90%} +![The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under `src/gnn/` named in [@tbl:pipeline_steps]; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of `src/gnn/STEP_INDEX.md`, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from `src/gnn/STEP_INDEX.md` at commit 48db291; its footer restates the step count, the number of required prerequisites, and the layout rule.](../output/figures/gnn_pipeline_dag.png){#fig:pipeline width=90%} The per-step responsibilities are enumerated in [@tbl:pipeline_steps]; each row names a step and the transformation it owns within the 0–24 range. @@ -167,6 +167,6 @@ This staged design keeps the architecture modular. The implementation is organiz The reason for separating a single written language from a multi-stage pipeline is the design goal GNN calls the Triple Play: one model specification, three coordinated modes of existence. The same GNN text supplies a human-readable model description, the declared state-space and factor structure for graphical visualization, and input to supported executable renderers. Source identity and validation receipts connect these representations to the authored model; runtime readiness and model-kind admission govern which simulations can execute. @fig:triple_play depicts these three faces and the shared specification at their center. -![The Triple Play: one GNN text specification (left) and its three coordinated projections (right) — the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 4ed8b33, and the panel is generated by `scripts/manuscript_fig_triple_play.py`. Take-away: a model is authored once and then read, seen, and run from that single source.](../output/figures/gnn_triple_play.png){#fig:triple_play width=70%} +![The Triple Play: one GNN text specification (left) and its three coordinated projections (right) — the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 48db291, and the panel is generated by `scripts/manuscript_fig_triple_play.py`. Take-away: a model is authored once and then read, seen, and run from that single source.](../output/figures/gnn_triple_play.png){#fig:triple_play width=70%} Each face derives from a shared authored source. The text is the contract a researcher reads and reviews; the visualizations expose the declared factor-graph structure for inspection and communication; and the executable rendering targets inference frameworks, connecting the written specification to the discrete message-passing libraries for categorical Active Inference [@heins2022], to the reactive message-passing and probabilistic-programming engines that carry the linear-Gaussian kind, and to the broader tooling ecosystem for compositional model representation [@defelice2021]. Common source identity makes the projections traceable, while parameter values, tensor orientation, model-kind semantics, and source-bound numerical results still require validation to establish agreement. A supported model can therefore be read, seen, and run without authoring a separate specification for each purpose [@smith2022]. The Triple Play is governed per kind: renderers and executors apply the computed classification and their declared capability boundaries, and receipts distinguish successful, unsupported, skipped, partial, and failed outcomes for the selected model. diff --git a/output/manuscript/03_methods.md b/output/manuscript/03_methods.md index 4d1c10396..4e317d1ee 100644 --- a/output/manuscript/03_methods.md +++ b/output/manuscript/03_methods.md @@ -56,7 +56,7 @@ Because rendering is kind-aware, the registry is best read as a capability matri The registry surface of these backends — registry key, display name, implementation language, and which of them the cross-framework gate compares — is summarized in @fig:backend_matrix; the family-by-backend coverage itself is shown in @fig:family_matrix. -![The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under `src/gnn/render/`, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from `src/gnn/render/framework_registry.py` and the model-family manifest at commit 4ed8b33, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.](../output/figures/gnn_backend_capability_matrix.png){#fig:backend_matrix width=85%} +![The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under `src/gnn/render/`, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from `src/gnn/render/framework_registry.py` and the model-family manifest at commit 48db291, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.](../output/figures/gnn_backend_capability_matrix.png){#fig:backend_matrix width=85%} ### Rendering by Model Kind @@ -96,4 +96,4 @@ The final group of stages turns execution results into interpretable evidence. A ## Reproducibility and Auto-Injection -Every count reported in this manuscript is emitted by the producer `scripts/z_generate_manuscript_variables.py` from the repository state at commit 4ed8b33, not typed by hand. The producer reads that commit's source surfaces directly — the step modules, the source tree, the model-family manifest, and the framework registry — together with the project's maintained ledgers, notably the Model Context Protocol tool audit (`src/gnn/mcp/audit_report.json`, itself regenerated by the test suite). From these it emits a manifest of named tokens — counts of pipeline steps and their modules, source packages and files, model families, backends, example files partitioned by model kind, tests, and tools — plus the two capability tables this manuscript injects: the model-kind table ([@tbl:model_kinds]) and the framework capability matrix ([@tbl:framework_capability]). The renderer substitutes the tokens into the prose at build time. Filesystem-derived counts are recomputed on every run; ledger-derived counts (such as the backend registry) are re-read from a fixed repository state, so a fixed repository state yields the same values, and authors are prohibited from hard-coding any number that has a corresponding token. This makes the manuscript a faithful, regenerable description of the system it documents: when the 25-step pipeline, the 12 rendering backends, or the 38-file example corpus change — or when the discrete-to-continuous exemplar balance shifts — the reported numbers change with them on the next build, and `scripts/check_manuscript_tokens.py` fails the build if a section reintroduces a literal that a token already owns. +Every count reported in this manuscript is emitted by the producer `scripts/z_generate_manuscript_variables.py` from the repository state at commit 48db291, not typed by hand. The producer reads that commit's source surfaces directly — the step modules, the source tree, the model-family manifest, and the framework registry — together with the project's maintained ledgers, notably the Model Context Protocol tool audit (`src/gnn/mcp/audit_report.json`, itself regenerated by the test suite). From these it emits a manifest of named tokens — counts of pipeline steps and their modules, source packages and files, model families, backends, example files partitioned by model kind, tests, and tools — plus the two capability tables this manuscript injects: the model-kind table ([@tbl:model_kinds]) and the framework capability matrix ([@tbl:framework_capability]). The renderer substitutes the tokens into the prose at build time. Filesystem-derived counts are recomputed on every run; ledger-derived counts (such as the backend registry) are re-read from a fixed repository state, so a fixed repository state yields the same values, and authors are prohibited from hard-coding any number that has a corresponding token. This makes the manuscript a faithful, regenerable description of the system it documents: when the 25-step pipeline, the 12 rendering backends, or the 38-file example corpus change — or when the discrete-to-continuous exemplar balance shifts — the reported numbers change with them on the next build, and `scripts/check_manuscript_tokens.py` fails the build if a section reintroduces a literal that a token already owns. diff --git a/output/manuscript/04_artifacts_and_evidence.md b/output/manuscript/04_artifacts_and_evidence.md index d23ffc8a3..a7785053a 100644 --- a/output/manuscript/04_artifacts_and_evidence.md +++ b/output/manuscript/04_artifacts_and_evidence.md @@ -1,6 +1,6 @@ # Artifacts and Evidence {#sec:artifacts_evidence} -This section reports what the project has actually produced and how each quantitative claim is grounded. Every number below is substituted at render time from the deterministic producer, which reads the repository state at commit 4ed8b33, so each figure here is regenerated from the artifacts it describes rather than transcribed. +This section reports what the project has actually produced and how each quantitative claim is grounded. Every number below is substituted at render time from the deterministic producer, which reads the repository state at commit 48db291, so each figure here is regenerated from the artifacts it describes rather than transcribed. ## Model-Family Coverage @@ -25,7 +25,7 @@ Families that declare a capability axis (the continuous family declares the cont The family-by-framework structure is shown in @fig:family_matrix, which renders the coverage matrix directly from the family registry rather than from a hand-maintained table. -![Model-family coverage across the registered rendering backends: one row per family from `input/model_family_manifest.json`, one column per backend from `src/gnn/render/framework_registry.py`, and a green cell wherever the family declares that backend in its `frameworks` field. The right-hand count states how many backends each family declares; the grid is deliberately sparse — most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 4ed8b33.](../output/figures/gnn_family_framework_matrix.png){#fig:family_matrix width=85%} +![Model-family coverage across the registered rendering backends: one row per family from `input/model_family_manifest.json`, one column per backend from `src/gnn/render/framework_registry.py`, and a green cell wherever the family declares that backend in its `frameworks` field. The right-hand count states how many backends each family declares; the grid is deliberately sparse — most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 48db291.](../output/figures/gnn_family_framework_matrix.png){#fig:family_matrix width=85%} These families are not illustrative prose: they are the inputs over which the parser, the type checker, and the cross-framework code generators are exercised, and they are the substrate for the reliability gates described next. @@ -43,11 +43,11 @@ A third interchange check extends the same discipline across repositories: `scri ## Repository Scale -The repository's scale is itself evidence of the surface that the gates and pipeline cover, and it is reported in @fig:repo_metrics directly from the tracked files at commit 4ed8b33. +The repository's scale is itself evidence of the surface that the gates and pipeline cover, and it is reported in @fig:repo_metrics directly from the tracked files at commit 48db291. -![Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from `output/data/manuscript_variables.json` at commit 4ed8b33 — the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: *registered backends* (12) counts render targets, *render-target adapters* (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure.](../output/figures/gnn_repo_metrics.png){#fig:repo_metrics width=80%} +![Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from `output/data/manuscript_variables.json` at commit 48db291 — the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: *registered backends* (12) counts render targets, *render-target adapters* (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure.](../output/figures/gnn_repo_metrics.png){#fig:repo_metrics width=80%} -The test suite comprises 658 test files containing 6790 test functions, exercising a source base of 766 Python files across 44 packages (225800 lines of source). The pipeline's step modules — the thin orchestrators named in [@tbl:pipeline_steps] — number 25, one per numbered step. The Model Context Protocol surface — which exposes GNN's capabilities to external agents and tools — provides 164 tools across 36 modules. The pipeline itself runs as 25 steps (0–24), and 7 figure artifacts from the rendering of figures, models, and reports are committed under `output/`, of which 7 are the manuscript's own. +The test suite comprises 660 test files containing 6809 test functions, exercising a source base of 767 Python files across 44 packages (225920 lines of source). The pipeline's step modules — the thin orchestrators named in [@tbl:pipeline_steps] — number 25, one per numbered step. The Model Context Protocol surface — which exposes GNN's capabilities to external agents and tools — provides 164 tools across 36 modules. The pipeline itself runs as 25 steps (0–24), and 7 figure artifacts from the rendering of figures, models, and reports are committed under `output/`, of which 7 are the manuscript's own. ## Claim Discipline diff --git a/output/manuscript/05_reproducibility.md b/output/manuscript/05_reproducibility.md index 72bc4f4de..5fcf4dd3c 100644 --- a/output/manuscript/05_reproducibility.md +++ b/output/manuscript/05_reproducibility.md @@ -72,7 +72,7 @@ uv run --frozen python scripts/pipeline/stage_03_render.py \ The render needs a LaTeX installation providing the packages listed in `manuscript/preamble.md` plus `seqsplit`; the template guards `seqsplit` with `\IfFileExists`, so a missing copy degrades rather than failing the build. -Because the variables file is regenerated before rendering, the counts in the rendered PDF track the repository state at the commit recorded in `output/data/manuscript_variables.json` (4ed8b33): a code change that alters, for example, the test inventory (658 test files, 6790 test functions) propagates into the prose on the next regeneration without any manual editing. +Because the variables file is regenerated before rendering, the counts in the rendered PDF track the repository state at the commit recorded in `output/data/manuscript_variables.json` (48db291): a code change that alters, for example, the test inventory (660 test files, 6809 test functions) propagates into the prose on the next regeneration without any manual editing. ## Reproducibility Contract diff --git a/output/manuscript/S01_source_surface.md b/output/manuscript/S01_source_surface.md index 1b39f55e9..52113fbb6 100644 --- a/output/manuscript/S01_source_surface.md +++ b/output/manuscript/S01_source_surface.md @@ -2,7 +2,7 @@ This supplement records the top-level source surfaces a reader or reviewer should inspect before turning any prose in the main manuscript into a verifiable claim. Each surface below is authored material under version control; the generated artifacts it produces are described separately so that the boundary between hand-written source and reproducible output stays explicit. -The `src/` tree is the executable core of GNN. It is organized into 44 Python packages spanning 766 source files and roughly 225800 lines of code. Alongside these packages sit the 25 top-level step modules (0–24) that implement the numbered pipeline as thin orchestrators delegating into the packages, each module owning one stage of the progression from a parsed GNN text model through visualization, type checking, code export, and executable cognitive simulation. A claim about declared pipeline behavior should identify its source implementation; a claim about actual execution additionally requires a source-bound command and result receipt. +The `src/` tree is the executable core of GNN. It is organized into 44 Python packages spanning 767 source files and roughly 225920 lines of code. Alongside these packages sit the 25 top-level step modules (0–24) that implement the numbered pipeline as thin orchestrators delegating into the packages, each module owning one stage of the progression from a parsed GNN text model through visualization, type checking, code export, and executable cognitive simulation. A claim about declared pipeline behavior should identify its source implementation; a claim about actual execution additionally requires a source-bound command and result receipt. The `input/` tree holds the model corpora that exercise the pipeline. Its `gnn_files/` subtree contains 12 corpus directories organized by model family, together totaling 38 example files — 29 discrete-state specifications and 9 continuous linear-Gaussian specifications, the partition the manuscript's model-kind accounting uses ([@sec:model_kinds]). The cold-start index `input/gnn_files/INDEX.md` catalogues every exemplar with its kind and task folder, and the `recursive/` directory is reserved for bounded `--autonomous` proposal-loop runs, holding no committed models. Every model file under `input/` lives in that subtree, so the 38-file count covers the whole tree. A `model_family_manifest.json` enumerates the 9 registered families and their target directories. This manifest is the authoritative registry that downstream steps and the manuscript variable producer read when they report family counts and cross-framework coverage; it should be consulted directly rather than inferred from directory listings. diff --git a/output/pdf/GeneralizedNotationNotation_combined.pdf b/output/pdf/GeneralizedNotationNotation_combined.pdf index bf07f1143..0416c3996 100644 Binary files a/output/pdf/GeneralizedNotationNotation_combined.pdf and b/output/pdf/GeneralizedNotationNotation_combined.pdf differ diff --git a/output/pdf/_combined_manuscript.aux b/output/pdf/_combined_manuscript.aux index 0ac439cd6..618d13c95 100644 --- a/output/pdf/_combined_manuscript.aux +++ b/output/pdf/_combined_manuscript.aux @@ -12,9 +12,9 @@ \citation{gnn2023} \@writefile{toc}{\contentsline {section}{\numberline {1}Abstract}{2}{section.1}\protected@file@percent } \newlabel{sec:abstract}{{1}{2}{Abstract}{section.1}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 4ed8b33. A plain-text specification --- the \texttt {StateSpaceBlock} declaring the model's tensors, \texttt {A}, \texttt {B}, \texttt {C}, \texttt {D}, and \texttt {E} for the discrete categorical kind of eq.~\ref {eq:generative_model} through eq.~\ref {eq:policy} and its own matrix vocabulary for the other model kinds --- is parsed (Step 3), type-checked and validated (Steps 5--6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep\_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check --- every discrete \texttt {B} literal is read as column-stochastic \texttt {B{[}s\textquotesingle {},\ s,\ a{]}}, and \texttt {-\/-transpose-b} maps row-stochastic textbook literals onto that canonical order --- and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools.}}{2}{figure.caption.2}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 48db291. A plain-text specification --- the \texttt {StateSpaceBlock} declaring the model's tensors, \texttt {A}, \texttt {B}, \texttt {C}, \texttt {D}, and \texttt {E} for the discrete categorical kind of eq.~\ref {eq:generative_model} through eq.~\ref {eq:policy} and its own matrix vocabulary for the other model kinds --- is parsed (Step 3), type-checked and validated (Steps 5--6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep\_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check --- every discrete \texttt {B} literal is read as column-stochastic \texttt {B{[}s\textquotesingle {},\ s,\ a{]}}, and \texttt {-\/-transpose-b} maps row-stochastic textbook literals onto that canonical order --- and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools.}}{2}{figure.caption.2}\protected@file@percent } \providecommand*\caption@xref[2]{\@setref\relax\@undefined{#1}} -\newlabel{fig:graphical_abstract}{{1}{2}{The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 4ed8b33. A plain-text specification --- the \protect \texttt {StateSpaceBlock} declaring the model's tensors, \protect \texttt {A}, \protect \texttt {B}, \protect \texttt {C}, \protect \texttt {D}, and \protect \texttt {E} for the discrete categorical kind of eq.~\ref {eq:generative_model} through eq.~\ref {eq:policy} and its own matrix vocabulary for the other model kinds --- is parsed (Step 3), type-checked and validated (Steps 5--6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep\_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check --- every discrete \protect \texttt {B} literal is read as column-stochastic \protect \texttt {B{[}s\textquotesingle {},\ s,\ a{]}}, and \protect \texttt {-\/-transpose-b} maps row-stochastic textbook literals onto that canonical order --- and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools}{figure.caption.2}{}} +\newlabel{fig:graphical_abstract}{{1}{2}{The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 48db291. A plain-text specification --- the \protect \texttt {StateSpaceBlock} declaring the model's tensors, \protect \texttt {A}, \protect \texttt {B}, \protect \texttt {C}, \protect \texttt {D}, and \protect \texttt {E} for the discrete categorical kind of eq.~\ref {eq:generative_model} through eq.~\ref {eq:policy} and its own matrix vocabulary for the other model kinds --- is parsed (Step 3), type-checked and validated (Steps 5--6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep\_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check --- every discrete \protect \texttt {B} literal is read as column-stochastic \protect \texttt {B{[}s\textquotesingle {},\ s,\ a{]}}, and \protect \texttt {-\/-transpose-b} maps row-stochastic textbook literals onto that canonical order --- and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools}{figure.caption.2}{}} \citation{friston2010} \citation{dacosta2020} \citation{parr2022,smith2022} @@ -73,8 +73,8 @@ \newlabel{what-the-kind-classification-governs}{{3.2.5}{8}{What the Kind Classification Governs}{subsubsection.3.2.5}{}} \@writefile{toc}{\contentsline {subsection}{\numberline {3.3}The Processing Pipeline}{9}{subsection.3.3}\protected@file@percent } \newlabel{the-processing-pipeline}{{3.3}{9}{The Processing Pipeline}{subsection.3.3}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under \texttt {src/gnn/} named in tbl.~\ref {tbl:pipeline_steps}; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of \breaktt {src/gnn/STEP\_INDEX.md}, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from \breaktt {src/gnn/STEP\_INDEX.md} at commit 4ed8b33; its footer restates the step count, the number of required prerequisites, and the layout rule.}}{9}{figure.caption.14}\protected@file@percent } -\newlabel{fig:pipeline}{{2}{9}{The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under \protect \texttt {src/gnn/} named in tbl.~\ref {tbl:pipeline_steps}; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of \protect \breaktt {src/gnn/STEP\_INDEX.md}, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from \protect \breaktt {src/gnn/STEP\_INDEX.md} at commit 4ed8b33; its footer restates the step count, the number of required prerequisites, and the layout rule}{figure.caption.14}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under \texttt {src/gnn/} named in tbl.~\ref {tbl:pipeline_steps}; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of \breaktt {src/gnn/STEP\_INDEX.md}, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from \breaktt {src/gnn/STEP\_INDEX.md} at commit 48db291; its footer restates the step count, the number of required prerequisites, and the layout rule.}}{9}{figure.caption.14}\protected@file@percent } +\newlabel{fig:pipeline}{{2}{9}{The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under \protect \texttt {src/gnn/} named in tbl.~\ref {tbl:pipeline_steps}; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of \protect \breaktt {src/gnn/STEP\_INDEX.md}, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from \protect \breaktt {src/gnn/STEP\_INDEX.md} at commit 48db291; its footer restates the step count, the number of required prerequisites, and the layout rule}{figure.caption.14}{}} \gdef \LT@ii {\LT@entry {1}{196.59068pt}\LT@entry {1}{154.94664pt}\LT@entry @@ -86,8 +86,8 @@ \citation{smith2022} \@writefile{toc}{\contentsline {subsection}{\numberline {3.4}The Triple Play}{11}{subsection.3.4}\protected@file@percent } \newlabel{the-triple-play}{{3.4}{11}{The Triple Play}{subsection.3.4}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces The Triple Play: one GNN text specification (left) and its three coordinated projections (right) --- the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 4ed8b33, and the panel is generated by \breaktt {scripts/manuscript\_fig\_triple\_play.py}. Take-away: a model is authored once and then read, seen, and run from that single source.}}{11}{figure.caption.15}\protected@file@percent } -\newlabel{fig:triple_play}{{3}{11}{The Triple Play: one GNN text specification (left) and its three coordinated projections (right) --- the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 4ed8b33, and the panel is generated by \protect \breaktt {scripts/manuscript\_fig\_triple\_play.py}. Take-away: a model is authored once and then read, seen, and run from that single source}{figure.caption.15}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces The Triple Play: one GNN text specification (left) and its three coordinated projections (right) --- the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 48db291, and the panel is generated by \breaktt {scripts/manuscript\_fig\_triple\_play.py}. Take-away: a model is authored once and then read, seen, and run from that single source.}}{11}{figure.caption.15}\protected@file@percent } +\newlabel{fig:triple_play}{{3}{11}{The Triple Play: one GNN text specification (left) and its three coordinated projections (right) --- the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 48db291, and the panel is generated by \protect \breaktt {scripts/manuscript\_fig\_triple\_play.py}. Take-away: a model is authored once and then read, seen, and run from that single source}{figure.caption.15}{}} \citation{rangarajan2026hierarchicalSuccessor} \citation{heins2022} \@writefile{toc}{\contentsline {section}{\numberline {4}Methods}{12}{section.4}\protected@file@percent } @@ -113,8 +113,8 @@ \newlabel{tbl:framework_capability}{{4}{13}{Model kinds each render framework supports, read entirely from the flags in \protect \breaktt {src/gnn/render/framework\_registry.py} --- the same source as tbl.~\ref {tbl:backend_registry}. A continuous cell of \protect \texttt {unsupported} is the renderer's own status for continuous-state models; continuous-only targets do not render categorical models. An executor entry declares dispatch support, not acceptance of every model or inference mode; a render-only entry has no Step-12 executor. Read with tbl.~\ref {tbl:model_kinds}, which groups the same flags by model kind}{table.4}{}} \@writefile{toc}{\contentsline {subsubsection}{\numberline {4.3.1}Rendering by Model Kind}{13}{subsubsection.4.3.1}\protected@file@percent } \newlabel{rendering-by-model-kind}{{4.3.1}{13}{Rendering by Model Kind}{subsubsection.4.3.1}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {4}{\ignorespaces The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under \texttt {src/gnn/render/}, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from \breaktt {src/gnn/render/framework\_registry.py} and the model-family manifest at commit 4ed8b33, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.}}{14}{figure.caption.16}\protected@file@percent } -\newlabel{fig:backend_matrix}{{4}{14}{The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under \protect \texttt {src/gnn/render/}, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from \protect \breaktt {src/gnn/render/framework\_registry.py} and the model-family manifest at commit 4ed8b33, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart}{figure.caption.16}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {4}{\ignorespaces The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under \texttt {src/gnn/render/}, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from \breaktt {src/gnn/render/framework\_registry.py} and the model-family manifest at commit 48db291, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.}}{14}{figure.caption.16}\protected@file@percent } +\newlabel{fig:backend_matrix}{{4}{14}{The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under \protect \texttt {src/gnn/render/}, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from \protect \breaktt {src/gnn/render/framework\_registry.py} and the model-family manifest at commit 48db291, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart}{figure.caption.16}{}} \citation{heins2022} \citation{bagaev2021reactiveMessagePassing} \citation{defelice2021} @@ -144,14 +144,14 @@ \newlabel{tbl:model_families}{{5}{18}{Model families declared in \protect \breaktt {input/model\_family\_manifest.json} with the simulation frameworks each family targets and a generated description. The \emph {Frameworks} column is the family's declared targets, verbatim from the manifest; the capability splits appended to some descriptions are generated from \protect \breaktt {src/gnn/render/framework\_registry.py} flags, not authored in the manifest, so they cannot disagree with the registry. Read with tbl.~\ref {tbl:backend_registry} (what each backend is) and fig.~\ref {fig:family_matrix} (the same coverage as a matrix)}{table.5}{}} \citation{dacosta2020} \citation{heins2022} -\@writefile{lof}{\contentsline {figure}{\numberline {6}{\ignorespaces Model-family coverage across the registered rendering backends: one row per family from \breaktt {input/model\_family\_manifest.json}, one column per backend from \breaktt {src/gnn/render/framework\_registry.py}, and a green cell wherever the family declares that backend in its \texttt {frameworks} field. The right-hand count states how many backends each family declares; the grid is deliberately sparse --- most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 4ed8b33.}}{19}{figure.caption.18}\protected@file@percent } -\newlabel{fig:family_matrix}{{6}{19}{Model-family coverage across the registered rendering backends: one row per family from \protect \breaktt {input/model\_family\_manifest.json}, one column per backend from \protect \breaktt {src/gnn/render/framework\_registry.py}, and a green cell wherever the family declares that backend in its \protect \texttt {frameworks} field. The right-hand count states how many backends each family declares; the grid is deliberately sparse --- most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 4ed8b33}{figure.caption.18}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {6}{\ignorespaces Model-family coverage across the registered rendering backends: one row per family from \breaktt {input/model\_family\_manifest.json}, one column per backend from \breaktt {src/gnn/render/framework\_registry.py}, and a green cell wherever the family declares that backend in its \texttt {frameworks} field. The right-hand count states how many backends each family declares; the grid is deliberately sparse --- most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 48db291.}}{19}{figure.caption.18}\protected@file@percent } +\newlabel{fig:family_matrix}{{6}{19}{Model-family coverage across the registered rendering backends: one row per family from \protect \breaktt {input/model\_family\_manifest.json}, one column per backend from \protect \breaktt {src/gnn/render/framework\_registry.py}, and a green cell wherever the family declares that backend in its \protect \texttt {frameworks} field. The right-hand count states how many backends each family declares; the grid is deliberately sparse --- most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 48db291}{figure.caption.18}{}} \@writefile{toc}{\contentsline {subsection}{\numberline {5.2}Semantic-Fidelity and Cross-Framework Reliability Gates}{19}{subsection.5.2}\protected@file@percent } \newlabel{semantic-fidelity-and-cross-framework-reliability-gates}{{5.2}{19}{Semantic-Fidelity and Cross-Framework Reliability Gates}{subsection.5.2}{}} \@writefile{toc}{\contentsline {subsection}{\numberline {5.3}Repository Scale}{20}{subsection.5.3}\protected@file@percent } \newlabel{repository-scale}{{5.3}{20}{Repository Scale}{subsection.5.3}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {7}{\ignorespaces Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from \breaktt {output/data/manuscript\_variables.json} at commit 4ed8b33 --- the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: \emph {registered backends} (12) counts render targets, \emph {render-target adapters} (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure.}}{20}{figure.caption.19}\protected@file@percent } -\newlabel{fig:repo_metrics}{{7}{20}{Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from \protect \breaktt {output/data/manuscript\_variables.json} at commit 4ed8b33 --- the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: \emph {registered backends} (12) counts render targets, \emph {render-target adapters} (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure}{figure.caption.19}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {7}{\ignorespaces Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from \breaktt {output/data/manuscript\_variables.json} at commit 48db291 --- the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: \emph {registered backends} (12) counts render targets, \emph {render-target adapters} (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure.}}{20}{figure.caption.19}\protected@file@percent } +\newlabel{fig:repo_metrics}{{7}{20}{Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from \protect \breaktt {output/data/manuscript\_variables.json} at commit 48db291 --- the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: \emph {registered backends} (12) counts render targets, \emph {render-target adapters} (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure}{figure.caption.19}{}} \citation{friston2010,parr2022} \@writefile{toc}{\contentsline {subsection}{\numberline {5.4}Claim Discipline}{21}{subsection.5.4}\protected@file@percent } \newlabel{claim-discipline}{{5.4}{21}{Claim Discipline}{subsection.5.4}{}} diff --git a/output/pdf/_combined_manuscript.log b/output/pdf/_combined_manuscript.log index a1017968b..113444c49 100644 --- a/output/pdf/_combined_manuscript.log +++ b/output/pdf/_combined_manuscript.log @@ -1,4 +1,4 @@ -This is XeTeX, Version 3.141592653-2.6-0.999995 (TeX Live 2023/Debian) (preloaded format=xelatex 2026.10.18) 18 OCT 2026 21:59 +This is XeTeX, Version 3.141592653-2.6-0.999995 (TeX Live 2023/Debian) (preloaded format=xelatex 2026.10.19) 19 OCT 2026 03:19 entering extended mode %&-line parsing enabled. **_combined_manuscript.tex diff --git a/output/pdf/_combined_manuscript.md b/output/pdf/_combined_manuscript.md index 4db0fe837..6d9cd141e 100644 --- a/output/pdf/_combined_manuscript.md +++ b/output/pdf/_combined_manuscript.md @@ -1,6 +1,6 @@ # Abstract {#sec:abstract} -![The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 4ed8b33. A plain-text specification — the `StateSpaceBlock` declaring the model's tensors, `A`, `B`, `C`, `D`, and `E` for the discrete categorical kind of [@eq:generative_model] through [@eq:policy] and its own matrix vocabulary for the other model kinds — is parsed (Step 3), type-checked and validated (Steps 5–6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check — every discrete `B` literal is read as column-stochastic `B[s', s, a]`, and `--transpose-b` maps row-stochastic textbook literals onto that canonical order — and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools.](../figures/gnn_graphical_abstract.png){#fig:graphical_abstract width=100%} +![The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from the producer's token map at commit 48db291. A plain-text specification — the `StateSpaceBlock` declaring the model's tensors, `A`, `B`, `C`, `D`, and `E` for the discrete categorical kind of [@eq:generative_model] through [@eq:policy] and its own matrix vocabulary for the other model kinds — is parsed (Step 3), type-checked and validated (Steps 5–6), rendered as backend-specific code (Step 11), executed (Step 12), and analyzed (Step 16), before artifacts flow into the cross-repository interchange checks against the pinned GEO-INFER and fep_lean pairs. Read the cards left to right: each header names the stage, each pill names the pipeline step (or the source role), and the body states what the stage produces. The validation stage includes the B-tensor orientation check — every discrete `B` literal is read as column-stochastic `B[s', s, a]`, and `--transpose-b` maps row-stochastic textbook literals onto that canonical order — and the execution stage includes the bnlearn lane, which generates runnable Python from the same parsed model and skips with a recorded status when its runtime is absent. The footer states the scale the panel summarizes: 25 pipeline steps, 9 model families, 12 registered render backends with 12 render targets having Step 12 execution adapters, and 164 Model Context Protocol tools.](../figures/gnn_graphical_abstract.png){#fig:graphical_abstract width=100%} Active Inference offers a unifying account of perception, learning, and action under the free energy principle [@friston2010], yet the generative models at its core are still communicated ad hoc: scattered across prose descriptions, bespoke notebooks, and framework-specific code that rarely agree. This fragmentation makes published models hard to reproduce, compare, or port between tools, and it raises the barrier for newcomers learning the formalism [@parr2022]. GeneralizedNotationNotation (GNN) addresses this gap with a standardized, human- and machine-readable text language for specifying Active Inference generative models, paired with a 25-step processing pipeline that transforms a single specification into validation, visualization, simulation, and analysis artifacts [@gnn2023]. A GNN file denotes a generative model, and the language lets that model take the several shapes the field actually uses: the notation blocks a file declares — categorical `A`/`B`/`C`/`D`[/`E`] tensors, linear-Gaussian system matrices `F`/`H`/`Q`/`R` with a Gaussian prior, per-agent and per-level compositions — fix its model kind, which the pipeline classifies structurally and carries through rendering and execution, recording explicitly which registered backends can render and run a model of each kind and which report it unsupported. GNN's "Triple Play" treats each model as three coordinated views — a textual specification, graphical visualizations, and executable cognitive models — so that outputs share an authored source and can be checked for consistency through identity, parameter, semantic, and numerical receipts. The framework spans 38 exemplar specifications (29 discrete-state and 9 continuous linear-Gaussian) across 9 model families and 12 registered rendering backends, with explicit gates for cross-format semantic fidelity and cross-framework reliability. GNN 3.0.0 layered safe-by-design long-running orchestration on top of this language and pipeline — durable observation streams, resumable run sessions, and auditable container plans that generate, validate, and replay data only, with no live infrastructure mutation. We describe the language, the model kinds it denotes, the pipeline architecture, and the validation contracts and source-bound checks used to assess specification round trips and admitted executions across simulation backends — with coverage gaps recorded as explicit profiled-unsupported statuses rather than silently omitted — providing inspectable infrastructure for reproducing and comparing Active Inference models within the recorded capability and verification boundaries. @@ -177,7 +177,7 @@ The governance runs in the direction the taxonomy promises: classification is co The pipeline is a fixed sequence of 25 numbered steps, 0–24, each a self-contained stage that consumes the artifacts of its predecessors and writes typed outputs for those that follow. Early steps parse and type-check the GNN text and validate it against the language schema; middle steps render visualizations, export the model to executable backends, and run simulations; later steps perform analysis, reporting, and downstream integration. The declared orchestration prerequisites among the steps form the directed acyclic graph shown in [@fig:pipeline]. The graph records which steps must precede others; artifact receipts separately identify source inputs and produced files, including optional enrichments. -![The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under `src/gnn/` named in [@tbl:pipeline_steps]; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of `src/gnn/STEP_INDEX.md`, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from `src/gnn/STEP_INDEX.md` at commit 4ed8b33; its footer restates the step count, the number of required prerequisites, and the layout rule.](../figures/gnn_pipeline_dag.png){#fig:pipeline width=90%} +![The 25-step GNN processing pipeline as a directed acyclic graph, arranged in rowwise topological order and colored by execution phase (legend above the nodes). Each node is the thin step orchestrator under `src/gnn/` named in [@tbl:pipeline_steps]; each arrow is a required orchestration prerequisite parsed from the Data Dependency Graph block of `src/gnn/STEP_INDEX.md`, which mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from `src/gnn/STEP_INDEX.md` at commit 48db291; its footer restates the step count, the number of required prerequisites, and the layout rule.](../figures/gnn_pipeline_dag.png){#fig:pipeline width=90%} The per-step responsibilities are enumerated in [@tbl:pipeline_steps]; each row names a step and the transformation it owns within the 0–24 range. @@ -216,7 +216,7 @@ This staged design keeps the architecture modular. The implementation is organiz The reason for separating a single written language from a multi-stage pipeline is the design goal GNN calls the Triple Play: one model specification, three coordinated modes of existence. The same GNN text supplies a human-readable model description, the declared state-space and factor structure for graphical visualization, and input to supported executable renderers. Source identity and validation receipts connect these representations to the authored model; runtime readiness and model-kind admission govern which simulations can execute. @fig:triple_play depicts these three faces and the shared specification at their center. -![The Triple Play: one GNN text specification (left) and its three coordinated projections (right) — the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 4ed8b33, and the panel is generated by `scripts/manuscript_fig_triple_play.py`. Take-away: a model is authored once and then read, seen, and run from that single source.](../figures/gnn_triple_play.png){#fig:triple_play width=70%} +![The Triple Play: one GNN text specification (left) and its three coordinated projections (right) — the human-readable text form, graphical model visualizations, and executable simulation code. All three arrows originate at the same source box to show their shared parsed input. Source binding, parameter validation, and semantic-fidelity checks provide evidence of agreement among the projections. The executable node summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at commit 48db291, and the panel is generated by `scripts/manuscript_fig_triple_play.py`. Take-away: a model is authored once and then read, seen, and run from that single source.](../figures/gnn_triple_play.png){#fig:triple_play width=70%} Each face derives from a shared authored source. The text is the contract a researcher reads and reviews; the visualizations expose the declared factor-graph structure for inspection and communication; and the executable rendering targets inference frameworks, connecting the written specification to the discrete message-passing libraries for categorical Active Inference [@heins2022], to the reactive message-passing and probabilistic-programming engines that carry the linear-Gaussian kind, and to the broader tooling ecosystem for compositional model representation [@defelice2021]. Common source identity makes the projections traceable, while parameter values, tensor orientation, model-kind semantics, and source-bound numerical results still require validation to establish agreement. A supported model can therefore be read, seen, and run without authoring a separate specification for each purpose [@smith2022]. The Triple Play is governed per kind: renderers and executors apply the computed classification and their declared capability boundaries, and receipts distinguish successful, unsupported, skipped, partial, and failed outcomes for the selected model. @@ -285,7 +285,7 @@ Because rendering is kind-aware, the registry is best read as a capability matri The registry surface of these backends — registry key, display name, implementation language, and which of them the cross-framework gate compares — is summarized in @fig:backend_matrix; the family-by-backend coverage itself is shown in @fig:family_matrix. -![The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under `src/gnn/render/`, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from `src/gnn/render/framework_registry.py` and the model-family manifest at commit 4ed8b33, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.](../figures/gnn_backend_capability_matrix.png){#fig:backend_matrix width=85%} +![The GNN rendering backend registry as a table: one row per registered backend, with its registry key, display name, implementation language, whether a render-output subdirectory exists under `src/gnn/render/`, and its role in the cross-framework reference comparison. Amber rows are the backends the reliability gate profiles for the continuous family; the tan row (Stan, ngc-learn) is declared by that family but sits outside the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from `src/gnn/render/framework_registry.py` and the model-family manifest at commit 48db291, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.](../figures/gnn_backend_capability_matrix.png){#fig:backend_matrix width=85%} ### Rendering by Model Kind @@ -325,7 +325,7 @@ The final group of stages turns execution results into interpretable evidence. A ## Reproducibility and Auto-Injection -Every count reported in this manuscript is emitted by the producer `scripts/z_generate_manuscript_variables.py` from the repository state at commit 4ed8b33, not typed by hand. The producer reads that commit's source surfaces directly — the step modules, the source tree, the model-family manifest, and the framework registry — together with the project's maintained ledgers, notably the Model Context Protocol tool audit (`src/gnn/mcp/audit_report.json`, itself regenerated by the test suite). From these it emits a manifest of named tokens — counts of pipeline steps and their modules, source packages and files, model families, backends, example files partitioned by model kind, tests, and tools — plus the two capability tables this manuscript injects: the model-kind table ([@tbl:model_kinds]) and the framework capability matrix ([@tbl:framework_capability]). The renderer substitutes the tokens into the prose at build time. Filesystem-derived counts are recomputed on every run; ledger-derived counts (such as the backend registry) are re-read from a fixed repository state, so a fixed repository state yields the same values, and authors are prohibited from hard-coding any number that has a corresponding token. This makes the manuscript a faithful, regenerable description of the system it documents: when the 25-step pipeline, the 12 rendering backends, or the 38-file example corpus change — or when the discrete-to-continuous exemplar balance shifts — the reported numbers change with them on the next build, and `scripts/check_manuscript_tokens.py` fails the build if a section reintroduces a literal that a token already owns. +Every count reported in this manuscript is emitted by the producer `scripts/z_generate_manuscript_variables.py` from the repository state at commit 48db291, not typed by hand. The producer reads that commit's source surfaces directly — the step modules, the source tree, the model-family manifest, and the framework registry — together with the project's maintained ledgers, notably the Model Context Protocol tool audit (`src/gnn/mcp/audit_report.json`, itself regenerated by the test suite). From these it emits a manifest of named tokens — counts of pipeline steps and their modules, source packages and files, model families, backends, example files partitioned by model kind, tests, and tools — plus the two capability tables this manuscript injects: the model-kind table ([@tbl:model_kinds]) and the framework capability matrix ([@tbl:framework_capability]). The renderer substitutes the tokens into the prose at build time. Filesystem-derived counts are recomputed on every run; ledger-derived counts (such as the backend registry) are re-read from a fixed repository state, so a fixed repository state yields the same values, and authors are prohibited from hard-coding any number that has a corresponding token. This makes the manuscript a faithful, regenerable description of the system it documents: when the 25-step pipeline, the 12 rendering backends, or the 38-file example corpus change — or when the discrete-to-continuous exemplar balance shifts — the reported numbers change with them on the next build, and `scripts/check_manuscript_tokens.py` fails the build if a section reintroduces a literal that a token already owns. @@ -336,7 +336,7 @@ Every count reported in this manuscript is emitted by the producer `scripts/z_ge # Artifacts and Evidence {#sec:artifacts_evidence} -This section reports what the project has actually produced and how each quantitative claim is grounded. Every number below is substituted at render time from the deterministic producer, which reads the repository state at commit 4ed8b33, so each figure here is regenerated from the artifacts it describes rather than transcribed. +This section reports what the project has actually produced and how each quantitative claim is grounded. Every number below is substituted at render time from the deterministic producer, which reads the repository state at commit 48db291, so each figure here is regenerated from the artifacts it describes rather than transcribed. ## Model-Family Coverage @@ -361,7 +361,7 @@ Families that declare a capability axis (the continuous family declares the cont The family-by-framework structure is shown in @fig:family_matrix, which renders the coverage matrix directly from the family registry rather than from a hand-maintained table. -![Model-family coverage across the registered rendering backends: one row per family from `input/model_family_manifest.json`, one column per backend from `src/gnn/render/framework_registry.py`, and a green cell wherever the family declares that backend in its `frameworks` field. The right-hand count states how many backends each family declares; the grid is deliberately sparse — most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 4ed8b33.](../figures/gnn_family_framework_matrix.png){#fig:family_matrix width=85%} +![Model-family coverage across the registered rendering backends: one row per family from `input/model_family_manifest.json`, one column per backend from `src/gnn/render/framework_registry.py`, and a green cell wherever the family declares that backend in its `frameworks` field. The right-hand count states how many backends each family declares; the grid is deliberately sparse — most families declare a single backend, and only continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit 48db291.](../figures/gnn_family_framework_matrix.png){#fig:family_matrix width=85%} These families are not illustrative prose: they are the inputs over which the parser, the type checker, and the cross-framework code generators are exercised, and they are the substrate for the reliability gates described next. @@ -379,11 +379,11 @@ A third interchange check extends the same discipline across repositories: `scri ## Repository Scale -The repository's scale is itself evidence of the surface that the gates and pipeline cover, and it is reported in @fig:repo_metrics directly from the tracked files at commit 4ed8b33. +The repository's scale is itself evidence of the surface that the gates and pipeline cover, and it is reported in @fig:repo_metrics directly from the tracked files at commit 48db291. -![Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from `output/data/manuscript_variables.json` at commit 4ed8b33 — the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: *registered backends* (12) counts render targets, *render-target adapters* (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure.](../figures/gnn_repo_metrics.png){#fig:repo_metrics width=80%} +![Repository-scale counts on a logarithmic axis: pipeline steps, model families, registered backends, render-target execution adapters, Model Context Protocol tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from `output/data/manuscript_variables.json` at commit 48db291 — the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately distinct: *registered backends* (12) counts render targets, *render-target adapters* (12) counts the render-target subset with implemented Step 12 execution adapters. It excludes the separate execution-only Lean bridge and is not a count of observed native runs. Read the chart as the scale of the surface the pipeline maintains, not as a quality measure.](../figures/gnn_repo_metrics.png){#fig:repo_metrics width=80%} -The test suite comprises 658 test files containing 6790 test functions, exercising a source base of 766 Python files across 44 packages (225800 lines of source). The pipeline's step modules — the thin orchestrators named in [@tbl:pipeline_steps] — number 25, one per numbered step. The Model Context Protocol surface — which exposes GNN's capabilities to external agents and tools — provides 164 tools across 36 modules. The pipeline itself runs as 25 steps (0–24), and 7 figure artifacts from the rendering of figures, models, and reports are committed under `output/`, of which 7 are the manuscript's own. +The test suite comprises 660 test files containing 6809 test functions, exercising a source base of 767 Python files across 44 packages (225920 lines of source). The pipeline's step modules — the thin orchestrators named in [@tbl:pipeline_steps] — number 25, one per numbered step. The Model Context Protocol surface — which exposes GNN's capabilities to external agents and tools — provides 164 tools across 36 modules. The pipeline itself runs as 25 steps (0–24), and 7 figure artifacts from the rendering of figures, models, and reports are committed under `output/`, of which 7 are the manuscript's own. ## Claim Discipline @@ -477,7 +477,7 @@ uv run --frozen python scripts/pipeline/stage_03_render.py \ The render needs a LaTeX installation providing the packages listed in `manuscript/preamble.md` plus `seqsplit`; the template guards `seqsplit` with `\IfFileExists`, so a missing copy degrades rather than failing the build. -Because the variables file is regenerated before rendering, the counts in the rendered PDF track the repository state at the commit recorded in `output/data/manuscript_variables.json` (4ed8b33): a code change that alters, for example, the test inventory (658 test files, 6790 test functions) propagates into the prose on the next regeneration without any manual editing. +Because the variables file is regenerated before rendering, the counts in the rendered PDF track the repository state at the commit recorded in `output/data/manuscript_variables.json` (48db291): a code change that alters, for example, the test inventory (660 test files, 6809 test functions) propagates into the prose on the next regeneration without any manual editing. ## Reproducibility Contract @@ -525,7 +525,7 @@ The next major release pushes toward bounded autonomy, and it is gated behind th This supplement records the top-level source surfaces a reader or reviewer should inspect before turning any prose in the main manuscript into a verifiable claim. Each surface below is authored material under version control; the generated artifacts it produces are described separately so that the boundary between hand-written source and reproducible output stays explicit. -The `src/` tree is the executable core of GNN. It is organized into 44 Python packages spanning 766 source files and roughly 225800 lines of code. Alongside these packages sit the 25 top-level step modules (0–24) that implement the numbered pipeline as thin orchestrators delegating into the packages, each module owning one stage of the progression from a parsed GNN text model through visualization, type checking, code export, and executable cognitive simulation. A claim about declared pipeline behavior should identify its source implementation; a claim about actual execution additionally requires a source-bound command and result receipt. +The `src/` tree is the executable core of GNN. It is organized into 44 Python packages spanning 767 source files and roughly 225920 lines of code. Alongside these packages sit the 25 top-level step modules (0–24) that implement the numbered pipeline as thin orchestrators delegating into the packages, each module owning one stage of the progression from a parsed GNN text model through visualization, type checking, code export, and executable cognitive simulation. A claim about declared pipeline behavior should identify its source implementation; a claim about actual execution additionally requires a source-bound command and result receipt. The `input/` tree holds the model corpora that exercise the pipeline. Its `gnn_files/` subtree contains 12 corpus directories organized by model family, together totaling 38 example files — 29 discrete-state specifications and 9 continuous linear-Gaussian specifications, the partition the manuscript's model-kind accounting uses ([@sec:model_kinds]). The cold-start index `input/gnn_files/INDEX.md` catalogues every exemplar with its kind and task folder, and the `recursive/` directory is reserved for bounded `--autonomous` proposal-loop runs, holding no committed models. Every model file under `input/` lives in that subtree, so the 38-file count covers the whole tree. A `model_family_manifest.json` enumerates the 9 registered families and their target directories. This manifest is the authoritative registry that downstream steps and the manuscript variable producer read when they report family counts and cross-framework coverage; it should be consulted directly rather than inferred from directory listings. diff --git a/output/pdf/_combined_manuscript.tex b/output/pdf/_combined_manuscript.tex index 60d7b014f..cdee83794 100644 --- a/output/pdf/_combined_manuscript.tex +++ b/output/pdf/_combined_manuscript.tex @@ -300,7 +300,7 @@ \section{Abstract}\label{sec:abstract} \centering \includegraphics[width=1\linewidth,height=0.5\textheight,keepaspectratio,alt={The GeneralizedNotationNotation (GNN) pipeline at a glance, generated from -the producer's token map at commit 4ed8b33. A plain-text specification +the producer's token map at commit 48db291. A plain-text specification --- the StateSpaceBlock declaring the model's tensors, A, B, C, D, and E for the discrete categorical kind of eq.~ through eq.~ and its own matrix vocabulary for the other model kinds --- is parsed (Step 3), @@ -321,7 +321,7 @@ \section{Abstract}\label{sec:abstract} execution adapters, and 164 Model Context Protocol tools.}]{../figures/gnn_graphical_abstract.png} \caption{The GeneralizedNotationNotation (GNN) pipeline at a glance, -generated from the producer's token map at commit 4ed8b33. A plain-text +generated from the producer's token map at commit 48db291. A plain-text specification --- the \protect\texttt{StateSpaceBlock} declaring the model's tensors, \protect\texttt{A}, \protect\texttt{B}, \protect\texttt{C}, \protect\texttt{D}, and \protect\texttt{E} for the @@ -1026,7 +1026,7 @@ \subsection{The Processing Pipeline}\label{the-processing-pipeline} mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from -src/gnn/STEP\_INDEX.md at commit 4ed8b33; its footer restates the step +src/gnn/STEP\_INDEX.md at commit 48db291; its footer restates the step count, the number of required prerequisites, and the layout rule.}]{../figures/gnn_pipeline_dag.png} \caption{The 25-step GNN processing pipeline as a directed acyclic @@ -1038,7 +1038,7 @@ \subsection{The Processing Pipeline}\label{the-processing-pipeline} mirrors the live step registry. Parsing (Step 3) precedes many later steps. Optional artifact enrichments are documented separately and are not shown as required edges. The figure is generated from -\protect\breaktt{src/gnn/STEP\_INDEX.md} at commit 4ed8b33; its footer +\protect\breaktt{src/gnn/STEP\_INDEX.md} at commit 48db291; its footer restates the step count, the number of required prerequisites, and the layout rule.}\label{fig:pipeline} \end{figure} @@ -1165,7 +1165,7 @@ \subsection{The Triple Play}\label{the-triple-play} summarizes 12 of 12 registered render targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound -run receipt. Counts are producer tokens at commit 4ed8b33, and the panel +run receipt. Counts are producer tokens at commit 48db291, and the panel is generated by scripts/manuscript\_fig\_triple\_play.py. Take-away: a model is authored once and then read, seen, and run from that single source.}]{../figures/gnn_triple_play.png} @@ -1179,7 +1179,7 @@ \subsection{The Triple Play}\label{the-triple-play} targets with implemented Step 12 execution adapters and previews three by name; execution depends on runtime readiness, admitted model kind, and a successful source-bound run receipt. Counts are producer tokens at -commit 4ed8b33, and the panel is generated by +commit 48db291, and the panel is generated by \protect\breaktt{scripts/manuscript\_fig\_triple\_play.py}. Take-away: a model is authored once and then read, seen, and run from that single source.}\label{fig:triple_play} @@ -1402,7 +1402,7 @@ \subsection{Rendering to Multiple the maintained set, so it is never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from src/gnn/render/framework\_registry.py and the model-family -manifest at commit 4ed8b33, and its footer summarizes the row counts. +manifest at commit 48db291, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.}]{../figures/gnn_backend_capability_matrix.png} @@ -1416,7 +1416,7 @@ \subsection{Rendering to Multiple never profiled; a red Render-Subdir cell would mean a registry entry with no subdirectory. The table is read directly from \protect\breaktt{src/gnn/render/framework\_registry.py} and the -model-family manifest at commit 4ed8b33, and its footer summarizes the +model-family manifest at commit 48db291, and its footer summarizes the row counts. Take-away: registration, render presence, and profiling status are three different claims, and the table keeps them apart.}\label{fig:backend_matrix} @@ -1621,7 +1621,7 @@ \subsection{Reproducibility and Every count reported in this manuscript is emitted by the producer \breaktt{scripts/z\_generate\_manuscript\_variables.py} from the -repository state at commit 4ed8b33, not typed by hand. The producer +repository state at commit 48db291, not typed by hand. The producer reads that commit's source surfaces directly --- the step modules, the source tree, the model-family manifest, and the framework registry --- together with the project's maintained ledgers, notably the Model @@ -1652,7 +1652,7 @@ \section{Artifacts and Evidence}\label{sec:artifacts_evidence} This section reports what the project has actually produced and how each quantitative claim is grounded. Every number below is substituted at render time from the deterministic producer, which reads the repository -state at commit 4ed8b33, so each figure here is regenerated from the +state at commit 48db291, so each figure here is regenerated from the artifacts it describes rather than transcribed. \subsection{Model-Family Coverage}\label{model-family-coverage} @@ -1773,7 +1773,7 @@ \subsection{Model-Family Coverage}\label{model-family-coverage} continuous, discrete, and gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit -4ed8b33.}]{../figures/gnn_family_framework_matrix.png} +48db291.}]{../figures/gnn_family_framework_matrix.png} \caption{Model-family coverage across the registered rendering backends: one row per family from \protect\breaktt{input/model\_family\_manifest.json}, one column per @@ -1785,7 +1785,7 @@ \subsection{Model-Family Coverage}\label{model-family-coverage} gridworld declare several. Read it as declared intent, not as profiled outcomes: the gates described below supply the outcomes. The matrix is generated from the two registries at commit -4ed8b33.}\label{fig:family_matrix} +48db291.}\label{fig:family_matrix} \end{figure} These families are not illustrative prose: they are the inputs over @@ -1857,7 +1857,7 @@ \subsection{Repository Scale}\label{repository-scale} The repository's scale is itself evidence of the surface that the gates and pipeline cover, and it is reported in fig.~\ref{fig:repo_metrics} -directly from the tracked files at commit 4ed8b33. +directly from the tracked files at commit 48db291. \begin{figure} \centering @@ -1867,7 +1867,7 @@ \subsection{Repository Scale}\label{repository-scale} tools, source packages, test files, example models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from output/data/manuscript\_variables.json at -commit 4ed8b33 --- the same token map that substitutes the prose counts. +commit 48db291 --- the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are @@ -1883,7 +1883,7 @@ \subsection{Repository Scale}\label{repository-scale} models, and documentation files. Every bar is annotated with its exact value, and every value is a producer token read from \protect\breaktt{output/data/manuscript\_variables.json} at commit -4ed8b33 --- the same token map that substitutes the prose counts. The +48db291 --- the same token map that substitutes the prose counts. The figure-freshness suite binds the committed image digest and its recorded producer inputs and counts; semantic content and visual correctness require their own review. The two backend bars are deliberately @@ -1895,8 +1895,8 @@ \subsection{Repository Scale}\label{repository-scale} as a quality measure.}\label{fig:repo_metrics} \end{figure} -The test suite comprises 658 test files containing 6790 test functions, -exercising a source base of 766 Python files across 44 packages (225800 +The test suite comprises 660 test files containing 6809 test functions, +exercising a source base of 767 Python files across 44 packages (225920 lines of source). The pipeline's step modules --- the thin orchestrators named in tbl.~\ref{tbl:pipeline_steps} --- number 25, one per numbered step. The Model Context Protocol surface --- which exposes GNN's @@ -2103,8 +2103,8 @@ \subsection{Manuscript Because the variables file is regenerated before rendering, the counts in the rendered PDF track the repository state at the commit recorded in -\breaktt{output/data/manuscript\_variables.json} (4ed8b33): a code change -that alters, for example, the test inventory (658 test files, 6790 test +\breaktt{output/data/manuscript\_variables.json} (48db291): a code change +that alters, for example, the test inventory (660 test files, 6809 test functions) propagates into the prose on the next regeneration without any manual editing. @@ -2295,7 +2295,7 @@ \section{Supplemental Source Surface}\label{sec:source_surface} reproducible output stays explicit. The \texttt{src/} tree is the executable core of GNN. It is organized -into 44 Python packages spanning 766 source files and roughly 225800 +into 44 Python packages spanning 767 source files and roughly 225920 lines of code. Alongside these packages sit the 25 top-level step modules (0--24) that implement the numbered pipeline as thin orchestrators delegating into the packages, each module owning one stage diff --git a/src/gnn/__init__.py b/src/gnn/__init__.py index ec40853ef..2f6709f42 100644 --- a/src/gnn/__init__.py +++ b/src/gnn/__init__.py @@ -130,15 +130,9 @@ def validate_gnn_source(source: Any, *, is_content: bool = False) -> Any: Returns: Dict with keys ``is_valid`` (bool) and ``errors`` (list[str]). """ - from pathlib import Path as _Path - - if not is_content and isinstance(source, (str, _Path)) and _Path(source).exists(): - content = _Path(source).read_text(encoding="utf-8") - else: - content = str(source) from .parsers.basic import validate_gnn_syntax as _validate_gnn_syntax - is_valid, errors = _validate_gnn_syntax(content) + is_valid, errors = _validate_gnn_syntax(source, is_content=is_content) return {"is_valid": is_valid, "errors": errors} diff --git a/src/gnn/parsers/basic.py b/src/gnn/parsers/basic.py index a5964f446..575170bc5 100644 --- a/src/gnn/parsers/basic.py +++ b/src/gnn/parsers/basic.py @@ -272,6 +272,8 @@ def get_parse_tree_visualization(content: str) -> str: def validate_gnn_syntax( file_path_or_content: Union[str, Path], validation_level: Union[ValidationLevel, str, None] = ValidationLevel.STANDARD, + *, + is_content: bool = False, **kwargs: Any, ) -> Tuple[bool, List[str]]: """ @@ -307,8 +309,11 @@ def validate_gnn_syntax( or ``None`` to use the validator's default level (STANDARD). Unknown level strings raise ``ValueError`` instead of silently skipping validation. - **kwargs: Accepted for backward compatibility; no options are - defined and they are ignored. + is_content: Treat the input as literal text, including Path objects + and strings that happen to name an existing file. No source path + classification is performed in this mode. + **kwargs: Other options are accepted for backward compatibility and + ignored. Returns: Tuple of (is_valid, formal_validator_errors). The second element is @@ -332,7 +337,8 @@ def validate_gnn_syntax( source_path: Optional[Path] = None if ( - isinstance(file_path_or_content, (str, Path)) + not is_content + and isinstance(file_path_or_content, (str, Path)) and str(file_path_or_content) # "" resolves to the CWD; always content ): try: diff --git a/src/gnn/render/execution_contracts.py b/src/gnn/render/execution_contracts.py index 84244d050..e5db57c10 100644 --- a/src/gnn/render/execution_contracts.py +++ b/src/gnn/render/execution_contracts.py @@ -44,6 +44,10 @@ def unsupported_contract(spec: object, framework: str) -> str | None: contract not in CONTRACTS or contract not in supported ): return f"unsupported-execution-contract: {framework} cannot execute {contract}" + if contract is None: + from gnn.render.planning_admission import unsupported_planning + + return unsupported_planning(spec, framework) return None diff --git a/src/gnn/render/jax/jax_renderer.py b/src/gnn/render/jax/jax_renderer.py index 03e3675f0..4d2a4144b 100644 --- a/src/gnn/render/jax/jax_renderer.py +++ b/src/gnn/render/jax/jax_renderer.py @@ -168,6 +168,11 @@ def render_gnn_to_jax_factorized( if execution_contract(gnn_spec) is not None: return render_gnn_to_jax(gnn_spec, output_path, options) + from gnn.render.planning_admission import unsupported_planning + + refusal = unsupported_planning(gnn_spec, "jax") + if refusal: + return False, refusal, [] return _render_to_path( _generate_jax_factorized_code, "JAX Kronecker-factorized", @@ -192,6 +197,11 @@ def render_gnn_to_jax_pomdp( if execution_contract(gnn_spec) is not None: return render_gnn_to_jax(gnn_spec, output_path, options) + from gnn.render.planning_admission import unsupported_planning + + refusal = unsupported_planning(gnn_spec, "jax_pomdp") + if refusal: + return False, refusal, [] return _render_to_path( _generate_jax_pomdp_code, "JAX POMDP", gnn_spec, output_path, options ) @@ -211,6 +221,11 @@ def render_gnn_to_jax_combined( if execution_contract(gnn_spec) is not None: return render_gnn_to_jax(gnn_spec, output_path, options) + from gnn.render.planning_admission import unsupported_planning + + refusal = unsupported_planning(gnn_spec, "jax_combined") + if refusal: + return False, refusal, [] return _render_to_path( _generate_jax_combined_code, "JAX combined", gnn_spec, output_path, options ) diff --git a/src/gnn/render/planning_admission.py b/src/gnn/render/planning_admission.py new file mode 100644 index 000000000..121c4018c --- /dev/null +++ b/src/gnn/render/planning_admission.py @@ -0,0 +1,101 @@ +"""Admission for source-authored horizons on the one-step planning adapters. + +This checks declared intent before canonicalization can hide it. It neither +selects a pruned policy set nor certifies filtering/smoothing equivalence. +Versioned execution contracts and cpomdp own their separate planning semantics. +""" + +from typing import Any + +ONE_STEP_PLANNERS = frozenset( + {"pymdp", "jax", "rxinfer", "activeinference_jl", "pytorch", "numpyro"} +) +ALTERNATIVE_JAX_TARGETS = frozenset({"jax_pomdp", "jax_combined"}) +HORIZON_KEYS = ("planning_horizon", "policy_len", "policy_horizon") +PLANNING_KEYS = (*HORIZON_KEYS, "num_policies", "policies", "policy_sequences") + + +def unsupported_planning(spec: dict[str, Any], framework: str) -> str | None: + """Refuse declarations the selected one-step planner cannot preserve. + + Read all supported parameter containers independently: precedence must not + erase a conflicting authored declaration. Integers are never coerced. + Explicit policy lists are refused even when their length matches the action + count, since these adapters do not consume such lists or their ordering. + """ + if framework not in ONE_STEP_PLANNERS | ALTERNATIVE_JAX_TARGETS: + return None + containers = [("spec", spec)] + for name in ( + "model_parameters", + "initialparameterization", + "initial_parameterization", + ): + value = spec.get(name) + if isinstance(value, dict): + containers.append((name, value)) + parameters = spec.get("parameters") + if isinstance(parameters, list): + for index, parameter in enumerate(parameters): + if isinstance(parameter, dict) and parameter.get("name") in ( + *PLANNING_KEYS, + "num_actions", + ): + containers.append( + ( + f"parameters[{index}]", + {parameter["name"]: parameter.get("value")}, + ) + ) + declared = [ + (f"{name}.{key}", key, values[key]) + for name, values in containers + for key in PLANNING_KEYS + if key in values + ] + if not declared: + return None + prefix = f"unsupported-planning: {framework} " + if framework in ALTERNATIVE_JAX_TARGETS: + return ( + prefix + + "generator does not consume source-authored planning declarations; its solver/demo defaults cannot certify them" + ) + for path, key, value in declared: + if key in {"policies", "policy_sequences"}: + return ( + prefix + + f"does not consume authored {path}; policy identity/order cannot be discarded" + ) + if type(value) is not int or value < 1: + return prefix + f"requires {path} to be a positive integer, got {value!r}" + horizons = [(path, value) for path, key, value in declared if key in HORIZON_KEYS] + if len({value for _, value in horizons}) > 1: + return prefix + f"cannot resolve conflicting horizon declarations: {horizons!r}" + if horizons and horizons[0][1] != 1: + return ( + prefix + + f"implements one-step planning only; authored {horizons[0][0]}={horizons[0][1]} requires an explicit multi-step policy contract (a policy count does not define a pruned policy set)" + ) + counts = [(path, value) for path, key, value in declared if key == "num_policies"] + if counts: + actions = [ + (f"{name}.num_actions", values["num_actions"]) + for name, values in containers + if "num_actions" in values + ] + if ( + not actions + or any(type(value) is not int or value < 1 for _, value in actions) + or len({value for _, value in actions}) != 1 + ): + return ( + prefix + + "cannot validate num_policies without one unambiguous positive integer num_actions declaration" + ) + if any(value != actions[0][1] for _, value in counts): + return ( + prefix + + f"requires one policy per action; authored policy counts {counts!r} differ from num_actions={actions[0][1]}" + ) + return None diff --git a/tests/execute/test_discrete_models_pymdp.py b/tests/execute/test_discrete_models_pymdp.py index 3f602b513..9259cd853 100644 --- a/tests/execute/test_discrete_models_pymdp.py +++ b/tests/execute/test_discrete_models_pymdp.py @@ -6,8 +6,8 @@ ``input/gnn_files/discrete/``, ensuring that: 1. POMDP extraction succeeds for each file -2. PyMDP rendering produces a valid script -3. Execution via ``run_pymdp_simulation`` produces: +2. PyMDP rendering produces a valid script or a named unsupported receipt +3. Admitted-source execution via ``run_pymdp_simulation`` produces: - Valid beliefs (all values in [0, 1]) - Beliefs summing to 1 - Actions within the valid range @@ -29,7 +29,8 @@ REPO_ROOT = Path(__file__).resolve().parents[2] DISCRETE_DIR = REPO_ROOT / "input" / "gnn_files" / "discrete" -# PyMDP-supported sources; the episodic T-maze is tested as unsupported below. +# Selected sources; the authored deep planner is refused at render admission. +# The separately contracted episodic T-maze is tested as unsupported below. DISCRETE_MODELS: list[Any] = [ "actinf_pomdp_agent.md", "bnlearn_causal_model.md", @@ -261,7 +262,7 @@ def test_time_varying_b_tensor_passes_through_nonstationary() -> None: @pytest.mark.parametrize("model_file", DISCRETE_MODELS) def test_pymdp_render(model_file: str, tmp_path: Path) -> None: - """Every discrete model should successfully render a PyMDP script.""" + """Every source gets an executable script or its exact planning refusal.""" from gnn.extract.pomdp_extractor import extract_pomdp_from_file from gnn.render.pomdp_processor import POMDPRenderProcessor @@ -276,6 +277,13 @@ def test_pymdp_render(model_file: str, tmp_path: Path) -> None: result = proc.process_pomdp_for_all_frameworks(pomdp, path, frameworks=["pymdp"]) fr = result.get("framework_results", {}).get("pymdp", {}) + if model_file == "deep_planning_horizon.md": + assert fr.get("status") == "unsupported" and fr.get("unsupported") is True + assert fr.get("success") is False and not fr.get("output_files") + assert fr["message"].startswith("unsupported-planning:") + assert "planning_horizon=5" in fr["message"] + assert not list(tmp_path.rglob("*.py")) + return assert fr.get("success"), ( f"Render failed for {model_file}: {fr.get('message', 'unknown error')}" ) @@ -298,7 +306,7 @@ def test_pymdp_render(model_file: str, tmp_path: Path) -> None: @pytest.mark.integration @pytest.mark.parametrize("model_file", DISCRETE_MODELS) def test_pymdp_execute(model_file: str, tmp_path: Path) -> None: - """Every discrete model should execute and produce valid simulation results.""" + """Only admitted sources reach the low-level numerical executor.""" if not _pymdp_importable(): raise AssertionError("PyMDP (inferactively-pymdp) not installed") @@ -309,6 +317,14 @@ def test_pymdp_execute(model_file: str, tmp_path: Path) -> None: from gnn.execute.pymdp.simulation import run_pymdp_simulation gnn_spec = _extract_and_build_spec(model_file, timesteps=12) + if model_file == "deep_planning_horizon.md": + from gnn.render.execution_contracts import unsupported_contract + + reason = unsupported_contract(gnn_spec, "pymdp") + assert reason and reason.startswith("unsupported-planning:") + assert "planning_horizon=5" in reason + assert not list(tmp_path.rglob("*simulation_results.json")) + return np.random.seed(42) ok, results = run_pymdp_simulation( @@ -346,7 +362,7 @@ def test_pymdp_execute(model_file: str, tmp_path: Path) -> None: @pytest.mark.integration @pytest.mark.parametrize("model_file", DISCRETE_MODELS) def test_pymdp_analysis_extractor(model_file: str, tmp_path: Path) -> None: - """Analysis extractor should consume simulation results from each model.""" + """Only admitted sources supply native results to the analysis extractor.""" if not _pymdp_importable(): raise AssertionError("PyMDP (inferactively-pymdp) not installed") @@ -358,6 +374,14 @@ def test_pymdp_analysis_extractor(model_file: str, tmp_path: Path) -> None: from gnn.execute.pymdp.simulation import run_pymdp_simulation gnn_spec = _extract_and_build_spec(model_file, timesteps=12) + if model_file == "deep_planning_horizon.md": + from gnn.render.execution_contracts import unsupported_contract + + reason = unsupported_contract(gnn_spec, "pymdp") + assert reason and reason.startswith("unsupported-planning:") + assert "planning_horizon=5" in reason + assert not list(tmp_path.rglob("*simulation_results.json")) + return np.random.seed(42) ok, results = run_pymdp_simulation(gnn_spec, tmp_path / "run") @@ -437,9 +461,22 @@ def test_all_discrete_models_e2e(tmp_path: Path) -> None: ) assert render_ok, "process_render should succeed for all discrete models" + summary = json.loads((render_out / "render_processing_summary.json").read_text()) + assert summary["total_files"] == len(DISCRETE_MODELS) + assert summary["failed_framework_renderings"] == [] + refusals = summary["unsupported_framework_renderings"] + assert len(refusals) == 1 and refusals[0]["framework"] == "pymdp" + assert Path(refusals[0]["file"]).name == "deep_planning_horizon.md" + assert refusals[0]["message"].startswith("unsupported-planning:") + expected_sources = set(DISCRETE_MODELS) - {"deep_planning_horizon.md"} + assert { + Path(source).name + for source, record in summary["file_results"].items() + if record["framework_results"]["pymdp"]["success"] + } == expected_sources pymdp_scripts = list(render_out.rglob("*pymdp.py")) - assert len(pymdp_scripts) == len(DISCRETE_MODELS), ( - f"Expected {len(DISCRETE_MODELS)} pymdp scripts, got {len(pymdp_scripts)}: " + assert len(pymdp_scripts) == len(expected_sources), ( + f"Expected {len(expected_sources)} pymdp scripts, got {len(pymdp_scripts)}: " f"{[s.name for s in pymdp_scripts]}" ) @@ -458,11 +495,12 @@ def test_all_discrete_models_e2e(tmp_path: Path) -> None: sim_results = list( exec_out.glob("**/pymdp/simulation_data/*simulation_results.json") ) - assert len(sim_results) == len(DISCRETE_MODELS), ( - f"Expected {len(DISCRETE_MODELS)} simulation results under {exec_out}, " + assert len(sim_results) == len(expected_sources), ( + f"Expected {len(expected_sources)} simulation results under {exec_out}, " f"found {len(sim_results)}" ) + assert not any("deep_planning" in str(path).lower() for path in sim_results) for result_file in sim_results: payload = json.loads(result_file.read_text(encoding="utf-8")) assert payload.get("framework") == "PyMDP", ( diff --git a/tests/gnn/test_source_validation_modes.py b/tests/gnn/test_source_validation_modes.py new file mode 100644 index 000000000..bcb03a0ef --- /dev/null +++ b/tests/gnn/test_source_validation_modes.py @@ -0,0 +1,92 @@ +"""Public and formal validation must preserve explicit source identity.""" + +from pathlib import Path + +import pytest + +from gnn import validate_gnn_source +from gnn.parsers.basic import validate_gnn_syntax +from gnn.processing.discovery import is_model_source_path + +ROOT = Path(__file__).resolve().parents[2] +MODELS = sorted( + path + for path in (ROOT / "input/gnn_files").rglob("*.md") + if is_model_source_path(path) +) +SAMPLE = ROOT / "input/gnn_files/discrete/actinf_pomdp_agent.md" + + +@pytest.mark.parametrize( + "path", MODELS, ids=lambda path: str(path.relative_to(ROOT / "input/gnn_files")) +) +def test_all_tracked_models_preserve_file_and_content_verdicts(path): + before = path.read_bytes() + content = before.decode("utf-8") + file_result = validate_gnn_source(path) + assert file_result["is_valid"], file_result["errors"] + assert file_result == validate_gnn_source(content, is_content=True) + valid, errors = validate_gnn_syntax(content, is_content=True) + assert file_result == {"is_valid": valid, "errors": errors} + assert path.read_bytes() == before + + +@pytest.mark.parametrize("suffix", ("界" * 10000, "\x00", "\n")) +def test_implicit_long_or_nonpath_text_reaches_same_formal_gate(suffix): + content = SAMPLE.read_text() + suffix + explicit = validate_gnn_source(content, is_content=True) + assert validate_gnn_source(content) == explicit + valid, errors = validate_gnn_syntax(content, is_content=True) + assert explicit == {"is_valid": valid, "errors": errors} + + +@pytest.mark.parametrize("value", ("", None, 42, "not a GNN document")) +def test_empty_or_nonpath_source_returns_formal_invalidity(value): + result = validate_gnn_source(value) + assert result["is_valid"] is False and result["errors"] + + +@pytest.mark.parametrize("name", ("literal-name.md", "境界-source.md")) +def test_explicit_literal_filename_does_not_read_existing_file( + name, tmp_path, monkeypatch +): + monkeypatch.chdir(tmp_path) + source = tmp_path / name + source.write_bytes(SAMPLE.read_bytes()) + before = source.read_bytes() + assert validate_gnn_source(name)["is_valid"] is True + explicit = validate_gnn_source(name, is_content=True) + assert explicit["is_valid"] is False and explicit["errors"] + valid, errors = validate_gnn_syntax(name, is_content=True) + assert explicit == {"is_valid": valid, "errors": errors} + assert source.read_bytes() == before + + +def test_path_objects_can_be_forced_to_literal_text(tmp_path): + source = tmp_path / "valid.md" + source.write_bytes(SAMPLE.read_bytes()) + assert validate_gnn_source(source)["is_valid"] is True + explicit = validate_gnn_source(source, is_content=True) + assert explicit["is_valid"] is False + valid, errors = validate_gnn_syntax(source, is_content=True) + assert explicit == {"is_valid": valid, "errors": errors} + + +def test_file_contents_cannot_redirect_to_another_file(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + target = tmp_path / "target.md" + target.write_bytes(SAMPLE.read_bytes()) + indirection = tmp_path / "indirection.md" + indirection.write_text(target.name) + assert validate_gnn_source(target)["is_valid"] is True + result = validate_gnn_source(indirection) + assert result["is_valid"] is False and result["errors"] + assert indirection.read_text() == target.name + + +def test_directory_and_undecodable_file_report_invalidity(tmp_path): + assert validate_gnn_source(tmp_path)["is_valid"] is False + source = tmp_path / "invalid-utf8.md" + source.write_bytes(b"\xff\xfe") + assert validate_gnn_source(source)["is_valid"] is False + assert source.read_bytes() == b"\xff\xfe" diff --git a/tests/render/test_planning_admission.py b/tests/render/test_planning_admission.py new file mode 100644 index 000000000..93639953d --- /dev/null +++ b/tests/render/test_planning_admission.py @@ -0,0 +1,177 @@ +"""Source intent must survive parser, direct renderer and Step 11 admission.""" + +from copy import deepcopy +from importlib import import_module +from pathlib import Path + +import pytest + +from gnn.extract.pomdp_extractor import extract_pomdp_from_file +from gnn.parsers.markdown_parser import MarkdownGNNParser +from gnn.render.execution_contracts import unsupported_contract +from gnn.render.planning_admission import unsupported_planning +from gnn.render.pomdp_processor import POMDPRenderProcessor +from gnn.render.processor import render_gnn_spec + +ROOT = Path(__file__).resolve().parents[2] +DEEP = ROOT / "input/gnn_files/discrete/deep_planning_horizon.md" +PLANNERS = ("pymdp", "jax", "rxinfer", "activeinference_jl", "pytorch", "numpyro") +DIRECT = ( + ("pymdp.pymdp_renderer", "render_gnn_to_pymdp"), + ("jax.jax_renderer", "render_gnn_to_jax"), + ("jax.jax_renderer", "render_gnn_to_jax_factorized"), + ("jax.jax_renderer", "render_gnn_to_jax_pomdp"), + ("jax.jax_renderer", "render_gnn_to_jax_combined"), + ("rxinfer.rxinfer_renderer", "render_gnn_to_rxinfer"), + ("activeinference_jl.activeinference_renderer", "render_gnn_to_activeinference_jl"), + ("pytorch.pytorch_renderer", "render_gnn_to_pytorch"), + ("numpyro.numpyro_renderer", "render_gnn_to_numpyro"), +) + + +def parsed_deep(): + result = MarkdownGNNParser().parse_string(DEEP.read_text()) + assert result.success + spec = result.model.to_dict() + spec["model_name"] = "renamed_authored_planning_example" + assert spec["model_parameters"]["planning_horizon"] == 5 + assert spec["model_parameters"]["num_policies"] == 64 + return spec + + +@pytest.mark.parametrize("backend", PLANNERS) +def test_real_source_is_unsupported_at_both_pipeline_interfaces(backend, tmp_path): + original = DEEP.read_bytes() + spec = parsed_deep() + before = deepcopy(spec) + ok, reason, files = render_gnn_spec(spec, backend, tmp_path / "single") + assert not ok and not files and reason.startswith("unsupported-planning:") + assert "=5" in reason and "pruned policy set" in reason + space = extract_pomdp_from_file(DEEP, on_error="raise") + receipt = POMDPRenderProcessor(tmp_path / "pipeline")._process_single_framework( + space, backend + ) + assert receipt["status"] == "unsupported" and receipt["unsupported"] + assert receipt["message"].startswith("unsupported-planning:") + assert not receipt["output_files"] + assert not list(tmp_path.rglob("*.py")) and not list(tmp_path.rglob("*.jl")) + assert spec == before and DEEP.read_bytes() == original + + +@pytest.mark.parametrize("module,function", DIRECT) +def test_direct_renderers_preserve_existing_destination(module, function, tmp_path): + renderer = getattr(import_module("gnn.render." + module), function) + destination = tmp_path / "existing.py" + destination.write_bytes(b"preserve existing artifact exactly\n") + before = destination.read_bytes() + ok, reason, files = renderer(parsed_deep(), destination) + assert not ok and not files and "unsupported-planning:" in reason + assert destination.read_bytes() == before + + +@pytest.mark.parametrize("mode", ("pipeline", "standalone")) +def test_file_backed_pymdp_class_refuses_before_writing(mode, tmp_path): + from gnn.render.pymdp.pymdp_renderer import PyMDPRenderer + + destination = tmp_path / "absent" / "model.py" + ok, reason = PyMDPRenderer({"mode": mode}).render_file(DEEP, destination) + assert not ok and "unsupported-planning:" in reason + assert not destination.parent.exists() + + +@pytest.mark.parametrize("key", ("planning_horizon", "policy_len", "policy_horizon")) +@pytest.mark.parametrize("value", (True, 0, -1, 1.0, "1", None, 2)) +def test_invalid_or_unimplemented_horizons_never_coerce(key, value): + reason = unsupported_planning({"model_parameters": {key: value}}, "pymdp") + assert reason and "unsupported-planning:" in reason and key in reason + + +@pytest.mark.parametrize( + "container", + ( + "spec", + "model_parameters", + "initialparameterization", + "initial_parameterization", + "parameters", + ), +) +def test_every_parameter_surface_preserves_horizon(container): + parameters = {"planning_horizon": 5} + spec = parameters if container == "spec" else {container: parameters} + if container == "parameters": + spec = {container: [{"name": "planning_horizon", "value": 5}]} + assert "=5" in unsupported_planning(spec, "pytorch") + + +def test_precedence_cannot_erase_conflicting_authored_horizon(): + spec = { + "model_parameters": {"planning_horizon": 1}, + "initialparameterization": {"policy_len": 2}, + } + assert "conflicting" in unsupported_planning(spec, "numpyro") + + +@pytest.mark.parametrize( + "parameters", + ( + {"num_policies": 64, "num_actions": 4}, + {"num_policies": True, "num_actions": 4}, + {"num_policies": 4}, + {"num_policies": 4, "num_actions": "4"}, + {"num_policies": 4, "num_actions": False}, + {"policies": [[0], [1]]}, + {"policy_sequences": [[0], [1]]}, + ), +) +def test_policy_identity_cannot_be_invented_or_ignored(parameters): + assert unsupported_planning({"model_parameters": parameters}, "rxinfer") + + +def test_conflicting_action_counts_cannot_certify_policy_count(): + spec = { + "model_parameters": {"num_policies": 2, "num_actions": 2}, + "initialparameterization": {"num_actions": 3}, + } + assert "unambiguous" in unsupported_planning(spec, "pymdp") + + +@pytest.mark.parametrize("backend", PLANNERS) +def test_matching_one_step_declarations_are_admitted(backend): + assert ( + unsupported_contract( + { + "model_parameters": { + "planning_horizon": 1, + "policy_len": 1, + "policy_horizon": 1, + "num_policies": 4, + "num_actions": 4, + } + }, + backend, + ) + is None + ) + + +@pytest.mark.parametrize("backend", ("stan", "bnlearn", "discopy", "thrml", "cpomdp")) +def test_separate_inference_questions_are_not_relabelled_as_planning(backend): + assert unsupported_planning(parsed_deep(), backend) is None + + +def test_separately_versioned_execution_contract_keeps_its_horizon(): + spec = { + "model_parameters": { + "execution_contract": "episodic_contingent_v1", + "policy_horizon": 2, + } + } + assert unsupported_contract(spec, "jax") is None + assert "unsupported-execution-contract:" in unsupported_contract(spec, "pymdp") + + +@pytest.mark.parametrize("target", ("jax_pomdp", "jax_combined")) +def test_alternative_jax_defaults_cannot_certify_even_a_declared_one_step(target): + reason = unsupported_planning({"model_parameters": {"planning_horizon": 1}}, target) + assert "does not consume source-authored planning declarations" in reason diff --git a/tests/render/test_render_process_discovery.py b/tests/render/test_render_process_discovery.py index 6049b5b1b..2a3aeddb9 100644 --- a/tests/render/test_render_process_discovery.py +++ b/tests/render/test_render_process_discovery.py @@ -3,7 +3,7 @@ Verifies recursive discovery in ``gnn.render.processor``: 1. ``process_render(..., recursive=True)`` walks every nested model folder. - All 38 model sources receive current receipts: 30 render and eight unsupported + All 38 model sources receive current receipts: 29 render and nine unsupported model contracts receive explicit receipts. Private malformed fixtures retain strict scientific rejection coverage. Documentation is excluded. 2. Passing ``recursive=False`` via kwargs reverts to a top-level-only glob, so @@ -26,6 +26,7 @@ EXEMPLAR_DIR = REPO_ROOT / "input" / "gnn_files" EXPECTED_EXEMPLAR_COUNT = 38 EXPECTED_UNSUPPORTED_SOURCES = { + "discrete/deep_planning_horizon.md": "unsupported-planning", "hierarchical/hierarchical_pomdp.md": "unsupported-execution-contract", "hierarchical/temporal_hierarchy.md": "unsupported-execution-contract", "discrete/tmaze_epistemic.md": "unsupported-execution-contract", diff --git a/tests/render/test_rxinfer_model_strategies.py b/tests/render/test_rxinfer_model_strategies.py index 1af73ec7d..ea4ddb0ea 100644 --- a/tests/render/test_rxinfer_model_strategies.py +++ b/tests/render/test_rxinfer_model_strategies.py @@ -174,6 +174,7 @@ def test_all_exemplars_render(self, tmp_path: Path) -> None: rel = str(gnn_file.relative_to(GNN_FILES)) spec = pomdp_to_gnn_spec(pomdp, native_agents=True) receipted = { + "discrete/deep_planning_horizon.md": "unsupported-planning", "hierarchical/hierarchical_pomdp.md": "unsupported-execution-contract", "hierarchical/temporal_hierarchy.md": "unsupported-execution-contract", "discrete/tmaze_epistemic.md": "unsupported-execution-contract", @@ -197,8 +198,8 @@ def test_all_exemplars_render(self, tmp_path: Path) -> None: assert script.exists() and script.stat().st_size > 0 outcomes["rendered"].add(rel) assert not failures, "render failures:\n" + "\n".join(failures) - assert len(outcomes["rendered"]) == 30 - assert len(outcomes["unsupported"]) == 8 + assert len(outcomes["rendered"]) == 29 + assert len(outcomes["unsupported"]) == 9 assert set.union(*outcomes.values()) == { str(path.relative_to(GNN_FILES)) for path in _exemplar_files() }