Skip to content

Commit b2f4c14

Browse files
ArghDA M11: studio JSON freeze (reason/0.1) + Groove discovery manifest (#39)
## What & why Closes the **one remaining open v0.1 milestone** (Groove service manifest). Two deliverables: ### Studio JSON freeze (`reason/0.1`) A JSON-contract test in `src/reason/mod.rs` pins the schema arghda-studio / Groove consumers depend on — top-level (`version`/`dag`/`nodes`/`edges`/`crt_roots`), per-node (`id`/`self_verdict`/`effective`/`verdict_evidence`/`soundness`/`wired`), per-edge (`from`/`to`/`junct`/`kind`). A rename now breaks a test: bump the version, don't silently edit. ### Groove manifest (`src/groove.rs`, `arghda groove`) Groove is the estate's HTTP service-discovery protocol. `GrooveManifest` (`groove: "0.1"`; capabilities = live backend probes + CLI commands + the frozen `dag/0.1` + `reason/0.1` schema versions). `arghda groove [--output <path>]` emits it to stdout, or writes a `/.well-known/groove` file (creating parent dirs). arghda-core is a CLI, so it **emits** the manifest; serving it over HTTP at `/.well-known/groove` is the host's (PanLL's) job — honest, no embedded HTTP server. Backends are probed *live*, so the manifest reflects what this machine can actually run. ## Verified (ran, not inferred) - `cargo test` → **121 passing, 0 failed** (+1 groove unit, +1 reason-contract freeze). - `cargo clippy -D warnings` clean · `cargo fmt --check` clean · `check-spdx.sh` OK. - **Dogfooded:** `arghda groove` → all 6 backends runnable, schemas `0.1`, commands listed; `arghda groove --output /tmp/site/.well-known/groove` wrote the file. ## Milestone status **The v0.1 spec is now closed** (Groove was the last open milestone), and STATE records M11 = 100%. The engine-spine deliverable (M0–M7 + M11) is complete bar **M7** (Echidna dispatch seam), which is the natural next step. Beyond that: the M3/M4/M6 follow-ons and the heavy-prover tail M8–M10. --- _Generated by [Claude Code](https://claude.ai/code/session_012MpYSh6Wy8YMBH2E3qVyT7)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent d966f2e commit b2f4c14

5 files changed

Lines changed: 228 additions & 16 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ milestones = [
3030
{ name = "Content-hash invalidation of proven (file-level + transitive-import closure via [proven] include_root)", completion = 100 },
3131
{ name = "Operator config (.arghda/config.toml + --headline-pattern + --config)", completion = 100 },
3232
{ name = "RSR / standards alignment (.machine_readable, README.adoc, EXPLAINME, LICENSE, .well-known)", completion = 100 },
33-
{ name = "Groove service manifest (/.well-known/groove) for PanLL discovery", completion = 0 },
33+
{ name = "Groove service manifest (/.well-known/groove) for PanLL discovery", completion = 100 },
3434
]
3535

3636
# "Flying Logic for provers/solvers" epic (v0.2+): generalise the Agda-only
@@ -51,7 +51,7 @@ milestones = [
5151
{ name = "M8: Coq/Rocq adapter (--heavy provisioning; Section-aware postulate classifier)", completion = 0 },
5252
{ name = "M9: Isabelle adapter (--heavy; dogfood tropical-resource-typing .thy)", completion = 0 },
5353
{ name = "M10: Mizar adapter (detect-only first; highest uncertainty)", completion = 0 },
54-
{ name = "M11: studio JSON freeze (reason/0.1) + Groove manifest", completion = 0 },
54+
{ name = "M11: studio JSON freeze (reason/0.1) + Groove manifest", completion = 100 },
5555
]
5656
# M0 provisioning verified in-container 2026-07-01: agda 2.6.3 (+stdlib v2.3
5757
# +cubical lib), zig 0.15.2, z3 4.8.12, cvc5 1.2.0, lean 4.13.0 all report
@@ -60,6 +60,22 @@ milestones = [
6060
# Remaining M0: `arghda doctor` Rust subcommand (interim: `just doctor` shells
6161
# the script's --verify-only table).
6262
#
63+
# M11 landed 2026-07-01: studio JSON freeze (reason/0.1) + Groove manifest —
64+
# closes the ONE open v0.1 milestone (Groove service manifest, was 0%). Two
65+
# deliverables. (1) Studio JSON freeze: a reason/0.1 JSON CONTRACT test in
66+
# src/reason/mod.rs pins the top-level (version/dag/nodes/edges/crt_roots) +
67+
# per-node (id/self_verdict/effective/verdict_evidence/soundness/wired) +
68+
# per-edge (from/to/junct/kind) keys — a rename now breaks a test (bump the
69+
# version, don't silently edit). (2) Groove: src/groove.rs `GrooveManifest`
70+
# (groove 0.1; service/description; capabilities = live backend probes +
71+
# commands + frozen schemas dag/0.1+reason/0.1). `arghda groove [--output
72+
# <path>]` emits it (stdout, or writes a `/.well-known/groove` file, creating
73+
# parent dirs). arghda-core is a CLI so it EMITS the manifest; serving it over
74+
# HTTP is the host's (PanLL's) job — honest. Backends probed live so the
75+
# manifest reflects this machine. 121 tests (88 lib + 33 integration; +1 groove
76+
# unit +1 reason-contract-freeze unit). Dogfooded: `arghda groove` → all 6
77+
# backends runnable, schemas 0.1; `--output` wrote /.well-known/groove.
78+
#
6379
# M6 landed 2026-07-01 (85%): the Lean4 backend — sixth backend. src/prover/
6480
# lean.rs: `lean <file>` elaboration. The honesty subtlety Lean forces: a green
6581
# exit does NOT mean proven (sorry is only a warning; exit 0). So the verdict is
@@ -184,10 +200,9 @@ milestones = [
184200

185201
[critical-next-actions]
186202
actions = [
187-
"M0-M6 DONE. SIX backends (agda, agda-cubical, idris2, lean4 Assistant; z3, cvc5 Solver) + reasoning graph + doctor. Engine spine essentially complete. Next:",
188-
"Flying-Logic epic M11: studio JSON freeze (reason/0.1) + Groove /.well-known/groove manifest (the one open v0.1 milestone; `arghda doctor` output feeds the manifest).",
189-
"Flying-Logic epic M7: Echidna dispatch seam (feature-gated client, same Outcome).",
190-
"M6 follow-on (15%): lake-env/LEAN_PATH multi-file resolution; per-decl #print axioms audit to promote Unknown→Proven.",
203+
"M0-M6 + M11 DONE. The v0.1 spec is now CLOSED (Groove manifest was the last open milestone). Engine-spine deliverable (M0-M7 + M11) is complete bar M7. Six backends + reasoning graph + doctor + groove. Remaining:",
204+
"Flying-Logic epic M7: Echidna dispatch seam (feature-gated client, same Outcome) — completes the engine-spine deliverable.",
205+
"M6 follow-on: lake-env/LEAN_PATH multi-file resolution; per-decl #print axioms audit to promote Unknown→Proven.",
191206
"M4 follow-on: .ipkg-declared roots; totality-hole (?name) + per-def `partial` lint.",
192207
"M3 follow-on: feed real verdicts into `reason` from a workspace `proven/` state + wire staleness from the content-hash closure (proven.rs).",
193208
"M8-M10 heavy tail (Coq/Rocq, Isabelle, Mizar) — gated on --heavy provisioning.",
@@ -199,10 +214,11 @@ last-result = "pass" # unknown | pass | warn | fail
199214
open-warnings = 0
200215
open-failures = 0
201216
# `just check` (fmt-check + clippy -D warnings + build + test + SPDX) green
202-
# post-M6: 86 lib + 33 integration = 119 tests pass. `arghda doctor` dogfooded:
203-
# 6/6 backends runnable. All six dogfooded via real CLI: agda + agda-cubical +
204-
# idris2 + lean4 (Assistant), z3 + cvc5 (Solver). Dogfooded historically against
205-
# echo-types + agda-unused.
217+
# post-M11: 88 lib + 33 integration = 121 tests pass. `arghda doctor`/`groove`
218+
# dogfooded: 6/6 backends runnable; manifest emits + writes /.well-known/groove.
219+
# All six backends dogfooded via real CLI: agda + agda-cubical + idris2 + lean4
220+
# (Assistant), z3 + cvc5 (Solver). Dogfooded historically vs echo-types +
221+
# agda-unused.
206222

207223
[ecosystem]
208224
part-of = ["arghda"]

src/groove.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
4+
//! Groove service-discovery manifest.
5+
//!
6+
//! Groove is the estate's HTTP-based service-discovery protocol: a capability
7+
//! provider publishes a manifest at `/.well-known/groove` so consumers (e.g.
8+
//! PanLL) can auto-detect it and wire it in without manual configuration.
9+
//!
10+
//! arghda-core is a CLI, not a server, so it *emits* the manifest (`arghda
11+
//! groove`) as JSON; serving it over HTTP at `/.well-known/groove` is the
12+
//! host's job (a web server, or the PanLL shell). Backend availability is
13+
//! probed live, so the manifest reflects what this machine can actually run —
14+
//! honest discovery, not a static advertisement.
15+
//!
16+
//! This is arghda's v0.1 manifest shape (`groove: "0.1"`); it announces the
17+
//! two frozen JSON schemas the visual layer consumes (`dag/0.1`,
18+
//! `reason/0.1`), the CLI commands, and the probed backends.
19+
20+
use crate::prover::Probe;
21+
use serde::Serialize;
22+
23+
/// The JSON-contract schema versions the visual layer (arghda-studio) and
24+
/// PanLL consume. Frozen — bump only on a breaking change.
25+
#[derive(Clone, Debug, Serialize)]
26+
pub struct Schemas {
27+
pub dag: &'static str,
28+
pub reason: &'static str,
29+
}
30+
31+
/// What arghda announces it can do.
32+
#[derive(Clone, Debug, Serialize)]
33+
pub struct Capabilities {
34+
/// Live probe of each known backend (id, kind, runnable, detail).
35+
pub backends: Vec<Probe>,
36+
/// The CLI verbs a consumer can drive.
37+
pub commands: Vec<&'static str>,
38+
/// The frozen output-schema versions.
39+
pub schemas: Schemas,
40+
}
41+
42+
/// The `/.well-known/groove` manifest for arghda.
43+
#[derive(Clone, Debug, Serialize)]
44+
pub struct GrooveManifest {
45+
/// Groove manifest-schema version.
46+
pub groove: &'static str,
47+
pub service: &'static str,
48+
pub description: &'static str,
49+
pub capabilities: Capabilities,
50+
}
51+
52+
/// Build the manifest from live backend probes.
53+
pub fn manifest(backends: Vec<Probe>) -> GrooveManifest {
54+
GrooveManifest {
55+
groove: "0.1",
56+
service: "arghda",
57+
description: "proof-workspace manager for provers and solvers",
58+
capabilities: Capabilities {
59+
backends,
60+
commands: vec!["scan", "check", "dag", "reason", "doctor", "groove"],
61+
schemas: Schemas {
62+
dag: "0.1",
63+
reason: "0.1",
64+
},
65+
},
66+
}
67+
}
68+
69+
#[cfg(test)]
70+
mod tests {
71+
use super::*;
72+
use crate::prover::BackendKind;
73+
74+
fn probe(name: &str) -> Probe {
75+
Probe {
76+
backend: name.to_string(),
77+
kind: BackendKind::Assistant,
78+
runnable: true,
79+
detail: "v1".to_string(),
80+
}
81+
}
82+
83+
#[test]
84+
fn manifest_announces_frozen_schemas_and_backends() {
85+
let m = manifest(vec![probe("agda"), probe("lean4")]);
86+
assert_eq!(m.groove, "0.1");
87+
assert_eq!(m.service, "arghda");
88+
assert_eq!(m.capabilities.schemas.dag, "0.1");
89+
assert_eq!(m.capabilities.schemas.reason, "0.1");
90+
assert_eq!(m.capabilities.backends.len(), 2);
91+
assert!(m.capabilities.commands.contains(&"reason"));
92+
// It serialises to the expected discovery shape.
93+
let v = serde_json::to_value(&m).unwrap();
94+
assert_eq!(v["groove"], "0.1");
95+
assert_eq!(v["capabilities"]["schemas"]["reason"], "0.1");
96+
assert_eq!(v["capabilities"]["backends"][0]["backend"], "agda");
97+
}
98+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod dag;
1515
pub mod diagnostic;
1616
pub mod event;
1717
pub mod graph;
18+
pub mod groove;
1819
pub mod hash;
1920
pub mod lint;
2021
pub mod proven;
@@ -29,6 +30,7 @@ pub use dag::{build as build_dag, DagDocument};
2930
pub use diagnostic::{Diagnostic, LintReport, Severity};
3031
pub use event::{Event, EventKind};
3132
pub use graph::{build as build_graph, ImportGraph};
33+
pub use groove::{manifest as groove_manifest, GrooveManifest};
3234
pub use lint::{default_rules, rules_with_config, run_lints, LintRule, RuleConfig};
3335
pub use prover::{
3436
default_backend, Agda, AgdaCubical, Backend, BackendKind, Idris2, Lean, Outcome, Probe, Smt,

src/main.rs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
use anyhow::{Context, Result};
55
use arghda_core::lint::LintContext;
66
use arghda_core::{
7-
build_dag, build_reason, event, run_lints, unused, watcher, Agda, AgdaCubical, Backend,
8-
BackendKind, Idris2, Lean, LintRule, RuleConfig, Smt, State, Verdict, Workspace,
7+
build_dag, build_reason, event, groove_manifest, run_lints, unused, watcher, Agda, AgdaCubical,
8+
Backend, BackendKind, Idris2, Lean, LintRule, Probe, RuleConfig, Smt, State, Verdict,
9+
Workspace,
910
};
1011
use clap::{Parser, Subcommand};
1112
use std::path::{Path, PathBuf};
@@ -170,6 +171,15 @@ enum Cmd {
170171
#[arg(long)]
171172
json: bool,
172173
},
174+
/// Emit the Groove service-discovery manifest (`/.well-known/groove`).
175+
/// Announces arghda's capabilities (probed backends, CLI commands, and
176+
/// the frozen `dag/0.1` + `reason/0.1` schemas) for PanLL discovery.
177+
Groove {
178+
/// Write the manifest to a file instead of stdout (e.g.
179+
/// `<site>/.well-known/groove`).
180+
#[arg(long)]
181+
output: Option<PathBuf>,
182+
},
173183
}
174184

175185
fn main() -> Result<()> {
@@ -252,16 +262,41 @@ fn main() -> Result<()> {
252262
} => stale(&workspace, invalidate)?,
253263
Cmd::Watch { workspace } => watch(&workspace)?,
254264
Cmd::Doctor { json } => doctor(json)?,
265+
Cmd::Groove { output } => groove(output.as_deref())?,
255266
}
256267
Ok(())
257268
}
258269

259-
/// Probe every known backend and report which are actually runnable.
260-
fn doctor(json: bool) -> Result<()> {
261-
let probes = KNOWN_BACKENDS
270+
/// Probe every known backend for runnability (shared by `doctor`/`groove`).
271+
fn probe_all() -> Result<Vec<Probe>> {
272+
KNOWN_BACKENDS
262273
.iter()
263274
.map(|name| backend_for(name).map(|b| b.probe()))
264-
.collect::<Result<Vec<_>>>()?;
275+
.collect()
276+
}
277+
278+
/// Emit the Groove service-discovery manifest (stdout, or `--output` file).
279+
fn groove(output: Option<&Path>) -> Result<()> {
280+
let manifest = groove_manifest(probe_all()?);
281+
let json = serde_json::to_string_pretty(&manifest)?;
282+
match output {
283+
Some(path) => {
284+
if let Some(parent) = path.parent() {
285+
std::fs::create_dir_all(parent)
286+
.with_context(|| format!("creating {}", parent.display()))?;
287+
}
288+
std::fs::write(path, format!("{json}\n"))
289+
.with_context(|| format!("writing {}", path.display()))?;
290+
println!("wrote Groove manifest to {}", path.display());
291+
}
292+
None => println!("{json}"),
293+
}
294+
Ok(())
295+
}
296+
297+
/// Probe every known backend and report which are actually runnable.
298+
fn doctor(json: bool) -> Result<()> {
299+
let probes = probe_all()?;
265300

266301
if json {
267302
let payload = serde_json::json!({ "version": "0.1", "backends": probes });

src/reason/mod.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,4 +531,65 @@ mod tests {
531531
assert!(wired.contains("Deep"));
532532
assert!(!wired.contains("Orphan"));
533533
}
534+
535+
// ── reason/0.1 JSON contract freeze (M11) ────────────────────────────
536+
// The visual layer (arghda-studio) + Groove consumers depend on this
537+
// shape. A rename here is a BREAKING change — bump the version, don't
538+
// silently edit. This test pins the schema so that can't happen by
539+
// accident.
540+
#[test]
541+
fn reason_json_contract_is_frozen_at_0_1() {
542+
use crate::dag::{DagDocument, DagNode, LintSummary};
543+
use crate::graph::Edge;
544+
use crate::prover::Agda;
545+
use std::path::PathBuf;
546+
547+
let dag = DagDocument {
548+
version: "0.1",
549+
include_root: PathBuf::from("/r"),
550+
entry_modules: vec![PathBuf::from("/r/All.agda")],
551+
generated_at: "t".to_string(),
552+
nodes: vec![
553+
DagNode {
554+
id: "All".to_string(),
555+
file: PathBuf::from("All.agda"),
556+
status: "clean",
557+
lint: LintSummary::default(),
558+
headlines: vec![],
559+
},
560+
DagNode {
561+
id: "Util".to_string(),
562+
file: PathBuf::from("Util.agda"),
563+
status: "clean",
564+
lint: LintSummary::default(),
565+
headlines: vec![],
566+
},
567+
],
568+
edges: vec![Edge {
569+
from: "All".to_string(),
570+
to: "Util".to_string(),
571+
kind: "imports",
572+
}],
573+
blocked: vec![],
574+
};
575+
let doc = build(dag, &Agda, &BTreeMap::new(), &BTreeSet::new());
576+
let v = serde_json::to_value(&doc).unwrap();
577+
578+
assert_eq!(v["version"], "0.1");
579+
assert!(v["dag"].is_object(), "the DAG is embedded verbatim");
580+
assert!(v["crt_roots"].is_array());
581+
for k in [
582+
"id",
583+
"self_verdict",
584+
"effective",
585+
"verdict_evidence",
586+
"soundness",
587+
"wired",
588+
] {
589+
assert!(v["nodes"][0].get(k).is_some(), "reason node missing `{k}`");
590+
}
591+
for k in ["from", "to", "junct", "kind"] {
592+
assert!(v["edges"][0].get(k).is_some(), "reason edge missing `{k}`");
593+
}
594+
}
534595
}

0 commit comments

Comments
 (0)