From dfd08aad3dbfa88f4cd0695446bbdab1aa6c8025 Mon Sep 17 00:00:00 2001 From: Tom Ballard Date: Sun, 30 Aug 2026 08:47:12 +0100 Subject: [PATCH] feat(federation): certify graph profile and evaluation Signed-off-by: Tom Ballard --- decisions/roadmaps/corpus-federation.md | 14 +++ docs/cli.md | 47 ++++++-- rust/decided/tests/cli.rs | 35 +++++- rust/decided/tests/eval_graph.rs | 105 ++++++++++++++++++ rust/fixtures/eval/README.md | 23 +++- .../eval/federation/.decided/config.yaml | 3 + .../eval/federation/.decided/corpus.md | 30 +++++ .../federation/audit/.decided/config.yaml | 3 + .../audit/decisions/quorum-audit-archive.md | 26 +++++ .../graph-decisions/root-quantum-notes.md | 26 +++++ .../eval/federation/graph-track/baseline.json | 31 ++++++ .../federation/graph-track/eval-config.json | 15 +++ .../eval/federation/graph-track/queries.json | 37 ++++++ .../policy-tree/.decided/config.yaml | 3 + .../federation/policy-tree/.decided/corpus.md | 20 ++++ .../decisions/transitive-policy-index.md | 30 +++++ .../vendor/deep/.decided/config.yaml | 3 + .../cerulean-quorum-reconciliation.md | 28 +++++ rust/rac-engine/src/commands.rs | 2 +- rust/rac-engine/src/output.rs | 29 +++-- 20 files changed, 484 insertions(+), 26 deletions(-) create mode 100644 rust/decided/tests/eval_graph.rs create mode 100644 rust/fixtures/eval/federation/.decided/config.yaml create mode 100644 rust/fixtures/eval/federation/.decided/corpus.md create mode 100644 rust/fixtures/eval/federation/audit/.decided/config.yaml create mode 100644 rust/fixtures/eval/federation/audit/decisions/quorum-audit-archive.md create mode 100644 rust/fixtures/eval/federation/graph-decisions/root-quantum-notes.md create mode 100644 rust/fixtures/eval/federation/graph-track/baseline.json create mode 100644 rust/fixtures/eval/federation/graph-track/eval-config.json create mode 100644 rust/fixtures/eval/federation/graph-track/queries.json create mode 100644 rust/fixtures/eval/federation/policy-tree/.decided/config.yaml create mode 100644 rust/fixtures/eval/federation/policy-tree/.decided/corpus.md create mode 100644 rust/fixtures/eval/federation/policy-tree/decisions/transitive-policy-index.md create mode 100644 rust/fixtures/eval/federation/policy-tree/vendor/deep/.decided/config.yaml create mode 100644 rust/fixtures/eval/federation/policy-tree/vendor/deep/decisions/cerulean-quorum-reconciliation.md diff --git a/decisions/roadmaps/corpus-federation.md b/decisions/roadmaps/corpus-federation.md index 4eb8e2b1..a321d633 100644 --- a/decisions/roadmaps/corpus-federation.md +++ b/decisions/roadmaps/corpus-federation.md @@ -150,12 +150,26 @@ isolation, YAML structural bombs, overlong paths, hard links, mount/reparse boundaries, unsupported filesystem identity, cold/warm/no-cache parity, MCP budgets/audit, exports/Portal, and Linux/macOS/Windows containment. +Implementation evidence now includes a committed track using the existing +DecisionGrounding scorer and metric gate: three direct parents, nested v1 and +v2 ancestry, a 40-artifact transitive standards source, a 32-inbound weak hard +negative, combined source-neutral ranking, and transitive search/relationship +retrieval. The broader certification matrix above remains open; this evidence +does not change the Proposed implementation requirements or this roadmap's +Planned status. + ### Profile guidance Update explicit `decided init --parent-corpus` guidance to show the v2 manifest and `decided corpus digest --version 2` without creating a manifest, fetching a source, or changing ordinary init/profile bytes. +The opt-in guidance now recommends manifest version 2, the bounded `parents` +sequence, and the explicit version-2 digest command while retaining the +version-1 command for existing single-parent manifests. Focused CLI contracts +prove fresh, idempotent, profiled, and unrequested flows; no manifest or parent +bytes are written. This narrow evidence does not complete the programme. + ## Constraints - Materialised, reviewed local bytes only. No clone, fetch, refresh, registry, diff --git a/docs/cli.md b/docs/cli.md index 4be94ffa..8332677e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -42,6 +42,7 @@ repin the parent. ```bash decided corpus digest --root vendor/standards --corpus decisions +decided corpus digest --version 2 --root vendor/standards --corpus decisions ``` `--root` is the parent repository root and bounds configuration discovery to @@ -70,6 +71,14 @@ escape, and traversed symlinks are rejected with stable `parent-corpus-*` errors. Exit `0` means the digest was calculated; exit `1` means the bounded materialisation could not be safely snapshotted. +Manifest version 2 uses the explicit `--version 2` command. Its +`sha256-v2:<64 lowercase hex>` digest also commits to whether +`.decided/corpus.md` is present and, when present, its exact bytes. Those bytes +bind the parent's own outgoing edges and pins, so graph updates are repinned +bottom-up. The owned Markdown snapshot excludes every direct materialisation +subtree declared by that manifest. The default command, version-1 digest, +`sha256:` prefix, and single-parent workflow remain unchanged. + --- ## validate @@ -1159,12 +1168,32 @@ for fallback and aggregation behaviour. [Org Grounding](org-grounding.md). - **`--parent-corpus`** prints deterministic setup guidance for the operational `.decided/corpus.md` manifest, including the exact `## inherits` and - `## overrides` headings. It tells you to materialise the parent inside the - repository first, then calculate its pin with `decided corpus digest --root - --corpus `. The flag works on fresh and - already-initialized repositories, with or without a profile. It is guidance - only: it never creates the manifest, fetches a parent, or writes parent bytes. - Without the flag, init files and human/JSON output are unchanged. + `## overrides` headings. Its recommended version-2 flow tells you to + materialise every parent inside the repository first, calculate each pin with + `decided corpus digest --version 2 --root --corpus + `, and declare one to 32 records in the `parents` sequence. + It also retains the original version-1 digest command for existing + single-parent manifests. The flag works on fresh and already-initialized + repositories, with or without a profile. It is guidance only: it never + creates the manifest, fetches a parent, or writes parent bytes. Without the + flag, init files and human/JSON output are unchanged. + + The version-2 manifest it describes has this shape (repeat the parent record + as needed; list order grants no precedence): + + ````markdown + ## inherits + + ```yaml + version: 2 + parents: + - alias: standards + source: acme/standards + root: vendor/standards + corpus: decisions + digest: sha256-v2:<64-lowercase-hex> + ``` + ```` - **Exit codes:** `0` initialized, or already initialized with the same key (idempotent) · `1` a different key is already established (never silently rewritten), or a client config exists but cannot be merged into (malformed @@ -1207,7 +1236,11 @@ With `--parent-corpus --json`, the response additionally includes: "manifest": ".decided/corpus.md", "inherits_heading": "## inherits", "overrides_heading": "## overrides", - "digest_command": "decided corpus digest --root --corpus " + "digest_command": "decided corpus digest --root --corpus ", + "recommended_manifest_version": 2, + "parents_field": "parents", + "multiple_parents": true, + "digest_command_v2": "decided corpus digest --version 2 --root --corpus " } } ``` diff --git a/rust/decided/tests/cli.rs b/rust/decided/tests/cli.rs index 468ea050..de684b05 100644 --- a/rust/decided/tests/cli.rs +++ b/rust/decided/tests/cli.rs @@ -95,9 +95,12 @@ fn init_parent_corpus_emits_guidance_without_creating_a_manifest() { ); let stdout = String::from_utf8_lossy(&output.stdout); for expected in [ - "Parent corpus setup:", - "Materialise the parent inside this repository", + "Parent corpus setup (manifest version 2):", + "Materialise every parent inside this repository", + "decided corpus digest --version 2 --root --corpus ", "decided corpus digest --root --corpus ", + "one to 32 verified parents", + "version: 2 parents sequence", ".decided/corpus.md", "## inherits", "## overrides", @@ -138,6 +141,12 @@ fn init_parent_corpus_is_profile_composable_and_idempotent() { let fresh_stdout = String::from_utf8_lossy(&fresh.stdout); assert!(fresh_stdout.contains("\"profile\": \"default\"")); assert!(fresh_stdout.contains("\"parent_corpus_guidance\": {")); + assert!(fresh_stdout.contains("\"recommended_manifest_version\": 2")); + assert!(fresh_stdout.contains("\"parents_field\": \"parents\"")); + assert!(fresh_stdout.contains("\"multiple_parents\": true")); + assert!(fresh_stdout.contains( + "\"digest_command_v2\": \"decided corpus digest --version 2 --root --corpus \"" + )); assert!(root.join(".mcp.json").is_file()); assert!(root.join(".cursor/mcp.json").is_file()); assert!(!root.join(".decided/corpus.md").exists()); @@ -152,7 +161,7 @@ fn init_parent_corpus_is_profile_composable_and_idempotent() { ); let idempotent_stdout = String::from_utf8_lossy(&idempotent.stdout); assert!(idempotent_stdout.starts_with("Already initialized: repository key RAC\n")); - assert!(idempotent_stdout.contains("Parent corpus setup:")); + assert!(idempotent_stdout.contains("Parent corpus setup (manifest version 2):")); assert_eq!( fs::read(root.join(".decided/config.yaml")).expect("read config after re-init"), before @@ -162,6 +171,26 @@ fn init_parent_corpus_is_profile_composable_and_idempotent() { fs::remove_dir_all(root).expect("remove parent-profile scratch repository"); } +#[test] +fn init_profile_without_parent_request_keeps_guidance_absent() { + let root = empty_scratch_root("profile-without-parent-guidance"); + let root_text = root.to_string_lossy().into_owned(); + let output = run(&["init", &root_text, "--profile", "enterprise", "--json"]); + assert!( + output.status.success(), + "stdout={}, stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let payload: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("profile init JSON"); + assert_eq!(payload["profile"], "enterprise"); + assert!(payload.get("parent_corpus_guidance").is_none()); + assert!(!root.join(".decided/corpus.md").exists()); + + fs::remove_dir_all(root).expect("remove profile-without-guidance scratch repository"); +} + #[test] fn init_parent_corpus_preserves_an_existing_non_default_key() { let root = empty_scratch_root("parent-existing-key"); diff --git a/rust/decided/tests/eval_graph.rs b/rust/decided/tests/eval_graph.rs new file mode 100644 index 00000000..7f34ed1d --- /dev/null +++ b/rust/decided/tests/eval_graph.rs @@ -0,0 +1,105 @@ +//! DecisionGrounding certification for the activated version-2 corpus graph. + +use serde_json::Value; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +fn fixtures() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../fixtures/eval") +} + +fn run(args: &[String]) -> Output { + Command::new(env!("CARGO_BIN_EXE_decided")) + .args(args) + .output() + .expect("run decided eval") +} + +fn graph_args(mode: &str) -> Vec { + let root = fixtures().join("federation"); + vec![ + "eval".to_string(), + mode.to_string(), + "--root".to_string(), + root.join("graph-decisions").to_string_lossy().into_owned(), + "--queries".to_string(), + root.join("graph-track/queries.json") + .to_string_lossy() + .into_owned(), + "--baseline".to_string(), + root.join("graph-track/baseline.json") + .to_string_lossy() + .into_owned(), + "--config".to_string(), + root.join("graph-track/eval-config.json") + .to_string_lossy() + .into_owned(), + ] +} + +#[test] +fn graph_track_gates_combined_ranking_floor_and_transitive_decisions() { + let checked = run(&graph_args("--check")); + assert!( + checked.status.success(), + "stdout={}, stderr={}", + String::from_utf8_lossy(&checked.stdout), + String::from_utf8_lossy(&checked.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&checked.stdout), + "decided eval: gate PASS\n" + ); + + let first = run(&graph_args("--json")); + let second = run(&graph_args("--json")); + assert!(first.status.success()); + assert!(second.status.success()); + let first: Value = serde_json::from_slice(&first.stdout).expect("first scorecard JSON"); + let second: Value = serde_json::from_slice(&second.stdout).expect("second scorecard JSON"); + assert_eq!(first["metrics"], second["metrics"]); + assert_eq!(first["per_query"], second["per_query"]); + assert_eq!(first["metadata"]["n_queries"], 4); + assert_eq!(first["metrics"]["overall"]["negative_violations"], 0); + + let queries = first["per_query"].as_array().expect("per-query rows"); + let gq01 = queries.iter().find(|row| row["id"] == "GQ01").unwrap(); + let large_parent = gq01["returned"].as_array().unwrap(); + assert_eq!(large_parent[0], "FEDEVAL-000000000001"); + assert!( + large_parent + .iter() + .position(|id| id == "FEDEVAL-000000000002") + .is_some_and(|position| position >= 5), + "the high-inbound lexical hard negative entered the top-five window" + ); + + let gq03 = queries.iter().find(|row| row["id"] == "GQ03").unwrap(); + assert_eq!(gq03["returned"][0], "FEDEVAL-000000000004"); + let gq04 = queries.iter().find(|row| row["id"] == "GQ04").unwrap(); + assert_eq!(gq04["returned"][0], "FEDEVAL-000000000005"); +} + +#[test] +fn no_manifest_track_retains_its_committed_metrics() { + let root = fixtures(); + let output = run(&[ + "eval".to_string(), + "--json".to_string(), + "--root".to_string(), + root.join("corpus").to_string_lossy().into_owned(), + "--queries".to_string(), + root.join("queries.json").to_string_lossy().into_owned(), + ]); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let scorecard: Value = serde_json::from_slice(&output.stdout).expect("scorecard JSON"); + let baseline: Value = serde_json::from_slice( + &std::fs::read(root.join("baseline.json")).expect("read committed baseline"), + ) + .expect("baseline JSON"); + assert_eq!(scorecard["metrics"], baseline); +} diff --git a/rust/fixtures/eval/README.md b/rust/fixtures/eval/README.md index f27559f8..442e5ad8 100644 --- a/rust/fixtures/eval/README.md +++ b/rust/fixtures/eval/README.md @@ -27,11 +27,17 @@ is part of the repository's product-knowledge corpus. category's `p_at_1` / `r_at_5`. Per-tool figures are diagnostic. - `baseline.json` — the committed `metrics` baseline, written by `decided eval --update-baseline` (human-only; CI never rebaselines). -- `federation/` — the ADR-139 DecisionGrounding track. Its child inherits a - 40-artifact standards parent containing a precise inherited match, six - lexical near-matches, and a 32-inbound-edge hard negative. The hard negative - has graph rank 1 but remains outside the top-five window because the v0.28 - lexical floor clamps its graph contribution. +- `federation/child/` — the version-1 ADR-139 DecisionGrounding track. Its + child inherits a 40-artifact standards parent containing a precise inherited + match, six lexical near-matches, and a 32-inbound-edge hard negative. The hard + negative has graph rank 1 but remains outside the top-five window because the + v0.28 lexical floor clamps its graph contribution. +- `federation/graph-decisions/` plus `federation/graph-track/` — the version-2 + extension of that same benchmark family. The root has three direct parents, + reaches the 40-artifact standards corpus through a nested version-1 node, and + reaches another Decision through a nested version-2 node. It measures the + combined source-neutral BM25 index, the same lexical graph-floor hard + negative, and transitive inherited search and relationship retrieval. ## Running @@ -47,6 +53,13 @@ decided eval --check \ --queries rust/fixtures/eval/federation/queries.json \ --baseline rust/fixtures/eval/federation/baseline.json \ --config rust/fixtures/eval/federation/eval-config.json + +# ADR-139 version-2 graph track (same scorer and metric family) +decided eval --check \ + --root rust/fixtures/eval/federation/graph-decisions \ + --queries rust/fixtures/eval/federation/graph-track/queries.json \ + --baseline rust/fixtures/eval/federation/graph-track/baseline.json \ + --config rust/fixtures/eval/federation/graph-track/eval-config.json ``` ## Calibration diff --git a/rust/fixtures/eval/federation/.decided/config.yaml b/rust/fixtures/eval/federation/.decided/config.yaml new file mode 100644 index 00000000..993ad91e --- /dev/null +++ b/rust/fixtures/eval/federation/.decided/config.yaml @@ -0,0 +1,3 @@ +repository_key: GRAPH +corpus: + source: eval/graph-root diff --git a/rust/fixtures/eval/federation/.decided/corpus.md b/rust/fixtures/eval/federation/.decided/corpus.md new file mode 100644 index 00000000..5e936167 --- /dev/null +++ b/rust/fixtures/eval/federation/.decided/corpus.md @@ -0,0 +1,30 @@ +# DecisionGrounding graph-federation fixture + +## inherits + +```yaml +version: 2 +parents: + - alias: application + source: eval/child + root: child + corpus: decisions + digest: sha256-v2:ac0003002cde387382ca5228bdf0f346ba3abbbd735c9349e54b587c0f319d5e + - alias: policies + source: eval/policy-tree + root: policy-tree + corpus: decisions + digest: sha256-v2:de108483b57c703c901e0095f133036bf8c8622d18f228b428dd23a7d6bf10b8 + - alias: audit + source: eval/audit + root: audit + corpus: decisions + digest: sha256-v2:b6c3e3b33d73d0c281498cb55b6996a80080e4c6e2da3c16d547c481f3b0c1e4 +``` + +## overrides + +```yaml +version: 2 +items: [] +``` diff --git a/rust/fixtures/eval/federation/audit/.decided/config.yaml b/rust/fixtures/eval/federation/audit/.decided/config.yaml new file mode 100644 index 00000000..3c12ebfc --- /dev/null +++ b/rust/fixtures/eval/federation/audit/.decided/config.yaml @@ -0,0 +1,3 @@ +repository_key: AUDIT +corpus: + source: eval/audit diff --git a/rust/fixtures/eval/federation/audit/decisions/quorum-audit-archive.md b/rust/fixtures/eval/federation/audit/decisions/quorum-audit-archive.md new file mode 100644 index 00000000..6e7da12a --- /dev/null +++ b/rust/fixtures/eval/federation/audit/decisions/quorum-audit-archive.md @@ -0,0 +1,26 @@ +--- +schema_version: 1 +id: FEDEVAL-000000000007 +type: decision +--- +# Quorum Audit Archive + +## Status + +Accepted + +## Context + +Audit records mention quorum operations without defining rollback recovery. + +## Decision + +Retain the annual quorum archive for compliance sampling. + +## Consequences + +This direct-parent record is a lexical distractor, not the transitive standard. + +## Category + +Technical diff --git a/rust/fixtures/eval/federation/graph-decisions/root-quantum-notes.md b/rust/fixtures/eval/federation/graph-decisions/root-quantum-notes.md new file mode 100644 index 00000000..2ae21a1f --- /dev/null +++ b/rust/fixtures/eval/federation/graph-decisions/root-quantum-notes.md @@ -0,0 +1,26 @@ +--- +schema_version: 1 +id: FEDEVAL-000000000006 +type: decision +--- +# Root Quantum Operations Notes + +## Status + +Accepted + +## Context + +The root service records local operational notes about quantum ledger clients. + +## Decision + +Follow the inherited standard for ledger compaction and signed checkpoints. + +## Consequences + +Locality does not make this note outrank the precise inherited policy. + +## Category + +Technical diff --git a/rust/fixtures/eval/federation/graph-track/baseline.json b/rust/fixtures/eval/federation/graph-track/baseline.json new file mode 100644 index 00000000..beb217b2 --- /dev/null +++ b/rust/fixtures/eval/federation/graph-track/baseline.json @@ -0,0 +1,31 @@ +{ + "overall": { + "p_at_1": 1.0, + "p_at_3": 0.333333, + "p_at_5": 0.2, + "r_at_1": 1.0, + "r_at_3": 1.0, + "r_at_5": 1.0, + "negative_violations": 0 + }, + "by_category": { + "graph_large_parent": { + "p_at_1": 1.0, + "r_at_5": 1.0 + }, + "graph_transitive_inheritance": { + "p_at_1": 1.0, + "r_at_5": 1.0 + } + }, + "by_tool": { + "get_related": { + "p_at_1": 1.0, + "r_at_5": 1.0 + }, + "search_artifacts": { + "p_at_1": 1.0, + "r_at_5": 1.0 + } + } +} diff --git a/rust/fixtures/eval/federation/graph-track/eval-config.json b/rust/fixtures/eval/federation/graph-track/eval-config.json new file mode 100644 index 00000000..f501cea3 --- /dev/null +++ b/rust/fixtures/eval/federation/graph-track/eval-config.json @@ -0,0 +1,15 @@ +{ + "description": "Graph DecisionGrounding gate: retain precise transitive matches, source-neutral combined ranking, and zero hard negatives in the top-five window.", + "tolerance": 0.02, + "floors": { + "negative_violations": 0, + "overall": { + "p_at_1": 0.9, + "r_at_5": 0.95 + }, + "by_category": { + "graph_large_parent": {"p_at_1": 0.9, "r_at_5": 0.95}, + "graph_transitive_inheritance": {"p_at_1": 0.9, "r_at_5": 0.95} + } + } +} diff --git a/rust/fixtures/eval/federation/graph-track/queries.json b/rust/fixtures/eval/federation/graph-track/queries.json new file mode 100644 index 00000000..f8a7d91b --- /dev/null +++ b/rust/fixtures/eval/federation/graph-track/queries.json @@ -0,0 +1,37 @@ +{ + "description": "DecisionGrounding graph track: three direct parents, nested v1 and v2 ancestry, a large transitive standards corpus, and lexical hard negatives without source preference.", + "cases": [ + { + "id": "GQ01", + "tool": "search_artifacts", + "category": "graph_large_parent", + "query": "quantum ledger compaction anchor", + "relevant": ["FEDEVAL-000000000001"], + "must_not_return": ["FEDEVAL-000000000002"] + }, + { + "id": "GQ02", + "tool": "search_artifacts", + "category": "graph_large_parent", + "query": "signed anchor checkpoint pruning", + "relevant": ["FEDEVAL-000000000001"], + "must_not_return": ["FEDEVAL-000000000002", "FEDEVAL-000000000003"] + }, + { + "id": "GQ03", + "tool": "search_artifacts", + "category": "graph_transitive_inheritance", + "query": "cerulean quorum rollback reconciliation signed epoch ledger", + "relevant": ["FEDEVAL-000000000004"], + "must_not_return": ["FEDEVAL-000000000006", "FEDEVAL-000000000007"] + }, + { + "id": "GQ04", + "tool": "get_related", + "category": "graph_transitive_inheritance", + "query": "eval/deep-standards::FEDEVAL-000000000004", + "relevant": ["FEDEVAL-000000000005"], + "must_not_return": [] + } + ] +} diff --git a/rust/fixtures/eval/federation/policy-tree/.decided/config.yaml b/rust/fixtures/eval/federation/policy-tree/.decided/config.yaml new file mode 100644 index 00000000..2d3e95f6 --- /dev/null +++ b/rust/fixtures/eval/federation/policy-tree/.decided/config.yaml @@ -0,0 +1,3 @@ +repository_key: POLICY +corpus: + source: eval/policy-tree diff --git a/rust/fixtures/eval/federation/policy-tree/.decided/corpus.md b/rust/fixtures/eval/federation/policy-tree/.decided/corpus.md new file mode 100644 index 00000000..a96ec1b7 --- /dev/null +++ b/rust/fixtures/eval/federation/policy-tree/.decided/corpus.md @@ -0,0 +1,20 @@ +# DecisionGrounding transitive policy fixture + +## inherits + +```yaml +version: 2 +parents: + - alias: deep + source: eval/deep-standards + root: vendor/deep + corpus: decisions + digest: sha256-v2:4aa3fd01907cde53014c9db467109eb1f053652ccfad428f2a70e9e46e2005fc +``` + +## overrides + +```yaml +version: 2 +items: [] +``` diff --git a/rust/fixtures/eval/federation/policy-tree/decisions/transitive-policy-index.md b/rust/fixtures/eval/federation/policy-tree/decisions/transitive-policy-index.md new file mode 100644 index 00000000..9eb328dc --- /dev/null +++ b/rust/fixtures/eval/federation/policy-tree/decisions/transitive-policy-index.md @@ -0,0 +1,30 @@ +--- +schema_version: 1 +id: FEDEVAL-000000000005 +type: decision +--- +# Transitive Policy Index + +## Status + +Accepted + +## Context + +The policy tree owns the route to its deeper operational standard. + +## Decision + +Use the deep standard for cerulean quorum rollback reconciliation. + +## Consequences + +The relationship proves that graph evaluation uses transitive inherited edges. + +## Category + +Technical + +## Related Decisions + +- FEDEVAL-000000000004 diff --git a/rust/fixtures/eval/federation/policy-tree/vendor/deep/.decided/config.yaml b/rust/fixtures/eval/federation/policy-tree/vendor/deep/.decided/config.yaml new file mode 100644 index 00000000..28f5a16b --- /dev/null +++ b/rust/fixtures/eval/federation/policy-tree/vendor/deep/.decided/config.yaml @@ -0,0 +1,3 @@ +repository_key: DEEP +corpus: + source: eval/deep-standards diff --git a/rust/fixtures/eval/federation/policy-tree/vendor/deep/decisions/cerulean-quorum-reconciliation.md b/rust/fixtures/eval/federation/policy-tree/vendor/deep/decisions/cerulean-quorum-reconciliation.md new file mode 100644 index 00000000..0167015b --- /dev/null +++ b/rust/fixtures/eval/federation/policy-tree/vendor/deep/decisions/cerulean-quorum-reconciliation.md @@ -0,0 +1,28 @@ +--- +schema_version: 1 +id: FEDEVAL-000000000004 +type: decision +--- +# Cerulean Quorum Rollback Reconciliation + +## Status + +Accepted + +## Context + +Services need one exact standard for reconciling a cerulean quorum after a +rollback. + +## Decision + +Cerulean quorum rollback reconciliation MUST compare the signed epoch ledger +before accepting a recovered replica. + +## Consequences + +The decision remains discoverable through two inheritance edges. + +## Category + +Technical diff --git a/rust/rac-engine/src/commands.rs b/rust/rac-engine/src/commands.rs index f0e530bd..d97df9d1 100644 --- a/rust/rac-engine/src/commands.rs +++ b/rust/rac-engine/src/commands.rs @@ -2906,7 +2906,7 @@ pub struct InitArgs { pub profile: Option, /// Org endpoint URL (ADR-117); http(s)-validated in the service layer. pub org_endpoint: Option, - /// Emit deterministic setup guidance for one materialised parent corpus. + /// Emit deterministic v2-first setup guidance for materialised parents. pub parent_corpus: bool, pub json: bool, } diff --git a/rust/rac-engine/src/output.rs b/rust/rac-engine/src/output.rs index 1421bc4e..abfba884 100644 --- a/rust/rac-engine/src/output.rs +++ b/rust/rac-engine/src/output.rs @@ -4116,10 +4116,12 @@ pub fn render_init_human(result: &crate::scaffold::InitResult) -> String { const PARENT_CORPUS_MANIFEST: &str = ".decided/corpus.md"; const PARENT_CORPUS_INHERITS_HEADING: &str = "## inherits"; const PARENT_CORPUS_OVERRIDES_HEADING: &str = "## overrides"; -const PARENT_CORPUS_DIGEST_COMMAND: &str = +const PARENT_CORPUS_DIGEST_COMMAND_V1: &str = "decided corpus digest --root --corpus "; +const PARENT_CORPUS_DIGEST_COMMAND_V2: &str = + "decided corpus digest --version 2 --root --corpus "; -/// Human init with the explicit ADR-088/134 setup guidance request. The +/// Human init with the explicit ADR-088/145 setup guidance request. The /// ordinary renderer above stays byte-identical and the guidance never writes /// a manifest or parent bytes. pub(crate) fn render_init_human_with_parent_corpus( @@ -4144,18 +4146,21 @@ pub(crate) fn render_init_human_with_parent_corpus( lines.extend(result.files_written.iter().map(|p| format!("Wrote: {p}"))); if parent_corpus { lines.extend([ - "Parent corpus setup:".to_string(), - "1. Materialise the parent inside this repository before calculating its digest; AsDecided does not fetch or refresh it." + "Parent corpus setup (manifest version 2):".to_string(), + "1. Materialise every parent inside this repository before calculating its digest; AsDecided does not fetch or refresh it." .to_string(), - format!("2. Calculate its digest: {PARENT_CORPUS_DIGEST_COMMAND}"), + format!("2. Calculate each topology-binding digest: {PARENT_CORPUS_DIGEST_COMMAND_V2}"), format!( - "3. Declare the verified parent in {} under {}.", + "3. Declare one to 32 verified parents in {} under {} using a version: 2 parents sequence.", PARENT_CORPUS_MANIFEST, PARENT_CORPUS_INHERITS_HEADING ), format!( - "4. Record explicit replacements under {}.", + "4. Record explicit replacements under {} using version: 2.", PARENT_CORPUS_OVERRIDES_HEADING ), + format!( + "Version 1 compatibility: existing single-parent manifests keep using {PARENT_CORPUS_DIGEST_COMMAND_V1}" + ), ]); } lines.join("\n") @@ -4192,7 +4197,11 @@ pub(crate) fn render_init_json_with_parent_corpus( "manifest": PARENT_CORPUS_MANIFEST, "inherits_heading": PARENT_CORPUS_INHERITS_HEADING, "overrides_heading": PARENT_CORPUS_OVERRIDES_HEADING, - "digest_command": PARENT_CORPUS_DIGEST_COMMAND, + "digest_command": PARENT_CORPUS_DIGEST_COMMAND_V1, + "recommended_manifest_version": 2, + "parents_field": "parents", + "multiple_parents": true, + "digest_command_v2": PARENT_CORPUS_DIGEST_COMMAND_V2, }), ); } @@ -5032,11 +5041,11 @@ mod init_output_tests { let result = init_result(); assert_eq!( render_init_human_with_parent_corpus(&result, true), - "Initialized repository key RAC\nConfig: repo/.decided/config.yaml\nParent corpus setup:\n1. Materialise the parent inside this repository before calculating its digest; AsDecided does not fetch or refresh it.\n2. Calculate its digest: decided corpus digest --root --corpus \n3. Declare the verified parent in .decided/corpus.md under ## inherits.\n4. Record explicit replacements under ## overrides." + "Initialized repository key RAC\nConfig: repo/.decided/config.yaml\nParent corpus setup (manifest version 2):\n1. Materialise every parent inside this repository before calculating its digest; AsDecided does not fetch or refresh it.\n2. Calculate each topology-binding digest: decided corpus digest --version 2 --root --corpus \n3. Declare one to 32 verified parents in .decided/corpus.md under ## inherits using a version: 2 parents sequence.\n4. Record explicit replacements under ## overrides using version: 2.\nVersion 1 compatibility: existing single-parent manifests keep using decided corpus digest --root --corpus " ); assert_eq!( render_init_json_with_parent_corpus(&result, true), - "{\n \"schema_version\": \"1\",\n \"repository_key\": \"RAC\",\n \"config_path\": \"repo/.decided/config.yaml\",\n \"created\": true,\n \"profile\": null,\n \"files_written\": [],\n \"org_endpoint\": null,\n \"parent_corpus_guidance\": {\n \"materialise_first\": true,\n \"manifest\": \".decided/corpus.md\",\n \"inherits_heading\": \"## inherits\",\n \"overrides_heading\": \"## overrides\",\n \"digest_command\": \"decided corpus digest --root --corpus \"\n }\n}" + "{\n \"schema_version\": \"1\",\n \"repository_key\": \"RAC\",\n \"config_path\": \"repo/.decided/config.yaml\",\n \"created\": true,\n \"profile\": null,\n \"files_written\": [],\n \"org_endpoint\": null,\n \"parent_corpus_guidance\": {\n \"materialise_first\": true,\n \"manifest\": \".decided/corpus.md\",\n \"inherits_heading\": \"## inherits\",\n \"overrides_heading\": \"## overrides\",\n \"digest_command\": \"decided corpus digest --root --corpus \",\n \"recommended_manifest_version\": 2,\n \"parents_field\": \"parents\",\n \"multiple_parents\": true,\n \"digest_command_v2\": \"decided corpus digest --version 2 --root --corpus \"\n }\n}" ); } }