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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions decisions/roadmaps/corpus-federation.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
schema_version: 1
id: RAC-KWJ8RTRK4JWM
Expand Down Expand Up @@ -150,12 +150,26 @@
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,
Expand Down
47 changes: 40 additions & 7 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
<parent-root> --corpus <parent-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 <parent-root> --corpus
<parent-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
Expand Down Expand Up @@ -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 <parent-root> --corpus <parent-corpus>"
"digest_command": "decided corpus digest --root <parent-root> --corpus <parent-corpus>",
"recommended_manifest_version": 2,
"parents_field": "parents",
"multiple_parents": true,
"digest_command_v2": "decided corpus digest --version 2 --root <parent-root> --corpus <parent-corpus>"
}
}
```
Expand Down
35 changes: 32 additions & 3 deletions rust/decided/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <parent-root> --corpus <parent-corpus>",
"decided corpus digest --root <parent-root> --corpus <parent-corpus>",
"one to 32 verified parents",
"version: 2 parents sequence",
".decided/corpus.md",
"## inherits",
"## overrides",
Expand Down Expand Up @@ -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 <parent-root> --corpus <parent-corpus>\""
));
assert!(root.join(".mcp.json").is_file());
assert!(root.join(".cursor/mcp.json").is_file());
assert!(!root.join(".decided/corpus.md").exists());
Expand All @@ -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
Expand All @@ -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");
Expand Down
105 changes: 105 additions & 0 deletions rust/decided/tests/eval_graph.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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);
}
23 changes: 18 additions & 5 deletions rust/fixtures/eval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions rust/fixtures/eval/federation/.decided/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
repository_key: GRAPH
corpus:
source: eval/graph-root
30 changes: 30 additions & 0 deletions rust/fixtures/eval/federation/.decided/corpus.md
Original file line number Diff line number Diff line change
@@ -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: []
```
3 changes: 3 additions & 0 deletions rust/fixtures/eval/federation/audit/.decided/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
repository_key: AUDIT
corpus:
source: eval/audit
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading