I1966 — The composed graph is whole or its generation refuses
Stop docs/generated/graph.json from silently dropping nodes and edges at the MAX_NODES cap. Either the cap is raised or removed under a measured size budget, or generation refuses a truncated graph and names the count and the cap. The committed graph must never be a prefix of the repository that still passes as whole.
DONE wave 0 · p1 · implementation profile · parallel safe
Objective
Stop docs/generated/graph.json from silently dropping nodes and edges at the MAX_NODES cap. Either the cap is raised or removed under a measured size budget, or generation refuses a truncated graph and names the count and the cap. The committed graph must never be a prefix of the repository that still passes as whole.
Why
graph::Builder::node refuses every node past MAX_NODES = 2000 (apps/majordomus-cli/src/graph.rs). It sets metadata.truncated, and compose() then drops each edge whose end could not be added. generate::graph_document refuses an unresolved relation (ADR 0020, case 390) but writes a truncated graph without a word. derive-check stays green, and every newly indexed object takes the place of a real node and its edges. This is the defect case 390 was written against, 'the file looks whole and is quietly short of one edge', reached by another route. It also breaks the capability-graph milestone's criterion that a relation that does not resolve fails generation rather than going missing.
Current state
The committed docs/generated/graph.json is composed without a node bound (graph::composed_complete, 9f98133aa2, PR #788) and holds 2434 nodes and 5341 edges with metadata.truncated false in 2,766,504 bytes; the implemented_by edge from majordomus://claim/mcp-uri-resolution to apps/majordomus-cli/src/capability/builtin/objects.rs is present. graph::whole refuses a truncated graph committed as data, and the site's why and product graphs go through it, proved by unit tests in graph.rs. Still open: no case drives generation over a fixture past a bound, because the composed graph has none, and no measured size budget is written beside MAX_NODES (2000, the bound of a graph a person is shown). Measured on master 4090b2b8ae, 2026-10-08.
Desired state
A committed graph.json is either complete, with metadata.truncated false, or it is not written: generation fails, naming the node count it needed and the cap it hit, as graph_document already does for an unresolved relation. If the cap is raised or removed for the committed artifact, the new bound is a size budget measured on this repository and written beside the constant with its measurement. The Cockpit and the interactive views may keep a display bound of their own, and they keep saying when they apply it.
Scope
- apps/majordomus-cli/src/graph.rs
- apps/majordomus-cli/src/generate.rs
- test/cases/390_unresolved_relation_refuses_generation.sh
- docs/generated/graph.json
- docs/generated/graph.schema.json
Out of scope
- The mesh registry's own MAX_NODES (apps/majordomus-cli/src/mesh/registry.rs), which is an unrelated bound on peers
- The Cockpit's graph pages and the site's why-graph and product-graph, apart from keeping their existing truncation notice true
- Changing which objects, claims or relations are composed: the inputs stay as they are, and only the completeness of the output is at stake
- Pruning claims or index classes to get under the cap
Dependencies
None. This issue is a root of the graph.
Acceptance criteria
- Generating the composed graph over this repository yields metadata.truncated false, or generation exits non-zero naming the node count and the cap
- The implemented_by edge from majordomus://claim/mcp-uri-resolution to apps/majordomus-cli/src/capability/builtin/objects.rs is present in docs/generated/graph.json, and so is every claim edge whose target file exists
- A fixture whose composition exceeds the bound makes generation refuse; it does not write a truncated graph.json, and the case fails if the refusal is removed
- Any new bound on the committed artifact has its measured size (bytes, nodes, edges) written beside the constant
Validation
- bash test/run.sh 390_unresolved_relation_refuses_generation
- cargo test --lib graph::
- scripts/derive-check
Evidence required
- composed_graph_complete_or_refused
- truncation_refusal_case
- size_budget_measured
Evidence
| covers | type | command | result | at commit |
|---|---|---|---|---|
| composed_graph_complete_or_refused | test | jq '.graph.metadata.truncated, (.graph.nodes|length), (.graph.edges|length)' docs/generated/graph.json; cargo test --lib -- graph:: generate::tests::a_composed_graph | ok at e8505a431d plus this change, on lundra: 89 passed, 0 failed. The committed docs/generated/graph.json holds 2795 nodes and 6128 edges with metadata.truncated false, past MAX_NODES 2000, because graph::composed_complete composes without a bound; generation now passes it through graph::whole, so a truncated composition exits 10 naming the count it held and the bound instead of being written. The implemented_by edge from majordomus://claim/mcp-uri-resolution to apps/majordomus-cli/src/capability/builtin/objects.rs is present | e8505a4 |
| truncation_refusal_case | test | cargo test --lib generate::tests::a_composed_graph_that_stopped_at_a_bound_is_refused_and_not_committed, then again with the refusal in graph::whole removed | ok as written: a composition bounded at one node is refused with exit 10 and a message naming 1 node and the bound 2000, and a whole one is accepted. With the refusal removed the same test FAILED, panicked at generate.rs:3248, so the test fails if the refusal is removed; the file was restored afterwards | e8505a4 |
| size_budget_measured | artifact | wc -c docs/generated/graph.json; jq '(.graph.nodes|length), (.graph.edges|length)' docs/generated/graph.json | measured at master 97c4521f91 and written beside graph::composed_complete: 2795 nodes, 6128 edges, 3,339,236 bytes. The committed artifact carries no bound, so no new constant was introduced; MAX_NODES stays the bound of a graph a person is shown | e8505a4 |
Risk
Raising the cap without a budget brings the problem back one size later, and graph.json is already 2.3 MB. Refusing outright makes every derive fail until the bound is settled, so the refusal and the new bound belong in one change.
Timeline
- started
- 2026-10-14T20:13:37Z
- verified
- 2026-10-14T20:14:49Z
- completed
- 2026-10-14T20:15:20Z
Those three fields, the evidence above and the state of the dependencies are all the status is made of. There is no status field to disagree with them.
Canonical record: .ai/repo/project/issues/I1966.yaml. Read it back with majordomus plan show I1966.