Skip to content

Commit 1023097

Browse files
committed
feat(config): operator config via .arghda/config.toml
Persists the operator-configurable headline pattern (and any future lint knobs) in `.arghda/config.toml`, the surface the spec's open question calls for — previously it was only a CLI flag. - New `src/config.rs`: a serde/toml `[lint] headline_pattern` schema overlaid on `RuleConfig::default()`. `deny_unknown_fields` so a typo errors rather than being silently ignored; a missing file yields defaults. - `scan` / `dag` gain `--config <file>`; discovery defaults to `<PATH>/.arghda/config.toml`. Precedence (low→high): built-in default < config.toml < CLI `--headline-pattern`. An explicit `--config` that does not exist is an error; the default location is silently optional. - Reuses the existing `RuleConfig` / `rules_with_config` (from #6); no change to the rule itself. Adds the `toml` dependency. 6 config unit tests (parse/precedence/unknown-key/ malformed/discovery); lib suite 41 → 47. Live-verified end to end: empty config → default pattern flags all headlines; `^thm-` config narrows to one; `--headline-pattern` overrides config; missing `--config` errors cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GiSiEfgZCte35dyykgBHs
1 parent bf05c52 commit 1023097

7 files changed

Lines changed: 263 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ All notable changes to arghda-core are documented here. The format follows
3737
and re-emits each finding as an `unused-import` warning attributed to the
3838
file. Degrades gracefully (with a note) when `agda-unused` is not on `PATH`,
3939
mirroring how `check` tolerates a missing `agda`.
40+
- `.arghda/config.toml` operator configuration (the spec's open-question
41+
surface): `scan` / `dag` read a `[lint] headline_pattern` override from
42+
`<PATH>/.arghda/config.toml` (or an explicit `--config <file>`). Precedence
43+
is built-in default < `config.toml` < CLI `--headline-pattern`. Missing file
44+
⇒ defaults; unknown keys are rejected so typos surface.
4045
- RSR scaffolding: `.machine_readable/6a2/` artefacts, `0-AI-MANIFEST.a2ml`,
4146
`Justfile`, `.well-known/`, and community-health files.
4247
- Content-hash invalidation of `proven`: promotion records a SHA-256 of the

Cargo.lock

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ notify = "6"
1717
regex = "1"
1818
serde = { version = "1", features = ["derive"] }
1919
serde_json = "1"
20+
toml = "0.8"
2021
walkdir = "2"
2122

2223
[dev-dependencies]

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ record.
2626
- `unpinned-headline` (warn) — a top-level theorem whose name matches the
2727
headline pattern is not pinned in any `Smoke.agda` via a `using ( … )`
2828
clause (the estate "every headline pinned in Smoke" discipline). The
29-
pattern is operator-configurable (`--headline-pattern <regex>`);
30-
its default `^[a-z][A-Za-z0-9-]*$` is deliberately broad, so operators
31-
narrow it to their own headline-naming convention. Self-skips when no
32-
`Smoke.agda` is in scope (e.g. a single-file `check`)
29+
pattern is operator-configurable (via `.arghda/config.toml` or
30+
`--headline-pattern <regex>`); its default `^[a-z][A-Za-z0-9-]*$` is
31+
deliberately broad, so operators narrow it to their own headline-naming
32+
convention. Self-skips when no `Smoke.agda` is in scope (e.g. a
33+
single-file `check`)
3334
- `unused-import` (warn) — re-emits the findings of the external
3435
[`agda-unused`](https://github.com/msuperdock/agda-unused) tool. Opt-in
3536
behind `scan --unused` (it runs `agda-unused` per file in local mode and
@@ -56,10 +57,18 @@ plus standalone scratch files. `scan` also flags the files deliberately
5657
outside the `--safe --without-K` kernel cone (`Fidelity.agda`, the cubical
5758
island, the postulated shadow).
5859

60+
Configuration: `scan` and `dag` read `.arghda/config.toml` (from
61+
`<PATH>/.arghda/config.toml`, or an explicit `--config <file>`). Precedence is
62+
built-in default < `config.toml` < CLI flag. Current schema:
63+
64+
```toml
65+
[lint]
66+
headline_pattern = "^[a-z][A-Za-z0-9-]*$"
67+
```
68+
5969
Not yet: the DAG `headlines` field (the extractor now exists for
6070
`unpinned-headline`, but the per-node `headlines` array in the DAG schema is
61-
still unpopulated); persisting the headline pattern in `.arghda/config.toml`
62-
(currently a CLI flag); the Groove service manifest. (`missing-without-k` is
71+
still unpopulated); the Groove service manifest. (`missing-without-k` is
6372
subsumed by `missing-safe-pragma`, which already reports a missing
6473
`--without-K`.)
6574

src/config.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! Operator configuration from `.arghda/config.toml`.
2+
//!
3+
//! The spec makes the `unpinned-headline` pattern operator-overridable
4+
//! per-workspace (`docs/arghda-spec.adoc` §Open questions). This module loads
5+
//! that override (and any future knobs) from a source tree's or workspace's
6+
//! `.arghda/config.toml`:
7+
//!
8+
//! ```toml
9+
//! [lint]
10+
//! headline_pattern = "^[a-z][A-Za-z0-9-]*$"
11+
//! ```
12+
//!
13+
//! Precedence (low → high): built-in [`RuleConfig::default`] < `config.toml`
14+
//! < CLI flag (e.g. `--headline-pattern`). A missing file is not an error —
15+
//! defaults apply. Unknown keys are rejected so a typo surfaces rather than
16+
//! being silently ignored.
17+
18+
use crate::lint::RuleConfig;
19+
use anyhow::{Context, Result};
20+
use serde::Deserialize;
21+
use std::path::Path;
22+
23+
/// Conventional config location, relative to a source tree or workspace root.
24+
pub const CONFIG_REL_PATH: &str = ".arghda/config.toml";
25+
26+
/// The on-disk `.arghda/config.toml` shape. Mirror of the knobs in
27+
/// [`RuleConfig`], all optional so a partial file overlays defaults.
28+
#[derive(Debug, Default, Deserialize)]
29+
#[serde(deny_unknown_fields)]
30+
struct ConfigFile {
31+
#[serde(default)]
32+
lint: LintTable,
33+
}
34+
35+
#[derive(Debug, Default, Deserialize)]
36+
#[serde(deny_unknown_fields)]
37+
struct LintTable {
38+
/// Override for the `unpinned-headline` detection regex.
39+
headline_pattern: Option<String>,
40+
}
41+
42+
/// Parse config TOML text into a [`RuleConfig`], overlaying built-in defaults.
43+
fn parse(text: &str) -> Result<RuleConfig> {
44+
let file: ConfigFile = toml::from_str(text).context("parsing .arghda/config.toml")?;
45+
let mut cfg = RuleConfig::default();
46+
if let Some(p) = file.lint.headline_pattern {
47+
cfg.headline_pattern = p;
48+
}
49+
Ok(cfg)
50+
}
51+
52+
/// Load a config file expected to exist, overlaying defaults.
53+
pub fn load_file(path: &Path) -> Result<RuleConfig> {
54+
let text = std::fs::read_to_string(path)
55+
.with_context(|| format!("reading config {}", path.display()))?;
56+
parse(&text).with_context(|| format!("in config {}", path.display()))
57+
}
58+
59+
/// Load `<base>/.arghda/config.toml` if it exists, else built-in defaults.
60+
pub fn load_from_dir(base: &Path) -> Result<RuleConfig> {
61+
let candidate = base.join(CONFIG_REL_PATH);
62+
if candidate.is_file() {
63+
load_file(&candidate)
64+
} else {
65+
Ok(RuleConfig::default())
66+
}
67+
}
68+
69+
#[cfg(test)]
70+
mod tests {
71+
use super::*;
72+
use crate::lint::unpinned_headline::DEFAULT_HEADLINE_PATTERN;
73+
74+
#[test]
75+
fn lint_table_overrides_headline_pattern() {
76+
let cfg = parse("[lint]\nheadline_pattern = \"^thm-.*$\"\n").unwrap();
77+
assert_eq!(cfg.headline_pattern, "^thm-.*$");
78+
}
79+
80+
#[test]
81+
fn empty_or_partial_file_keeps_defaults() {
82+
assert_eq!(
83+
parse("").unwrap().headline_pattern,
84+
DEFAULT_HEADLINE_PATTERN
85+
);
86+
assert_eq!(
87+
parse("[lint]\n").unwrap().headline_pattern,
88+
DEFAULT_HEADLINE_PATTERN
89+
);
90+
}
91+
92+
#[test]
93+
fn unknown_keys_are_rejected() {
94+
assert!(parse("[lint]\nheadline_patten = \"x\"\n").is_err()); // typo
95+
assert!(parse("[bogus]\nx = 1\n").is_err()); // unknown table
96+
}
97+
98+
#[test]
99+
fn malformed_toml_errors() {
100+
assert!(parse("[lint").is_err());
101+
}
102+
103+
#[test]
104+
fn load_from_dir_without_config_is_default() {
105+
let dir = tempfile::tempdir().unwrap();
106+
let cfg = load_from_dir(dir.path()).unwrap();
107+
assert_eq!(cfg.headline_pattern, DEFAULT_HEADLINE_PATTERN);
108+
}
109+
110+
#[test]
111+
fn load_from_dir_reads_present_config() {
112+
let dir = tempfile::tempdir().unwrap();
113+
std::fs::create_dir_all(dir.path().join(".arghda")).unwrap();
114+
std::fs::write(
115+
dir.path().join(CONFIG_REL_PATH),
116+
"[lint]\nheadline_pattern = \"^[A-Z].*$\"\n",
117+
)
118+
.unwrap();
119+
let cfg = load_from_dir(dir.path()).unwrap();
120+
assert_eq!(cfg.headline_pattern, "^[A-Z].*$");
121+
}
122+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! and the diagnostic types. The CLI in `main.rs` is a thin consumer.
55
66
pub mod agda;
7+
pub mod config;
78
pub mod dag;
89
pub mod diagnostic;
910
pub mod event;

0 commit comments

Comments
 (0)