Skip to content

Commit a443d2e

Browse files
committed
feat(lint): unpinned-headline rule (Smoke-pin coverage)
Implements the spec's `unpinned-headline` lint (arghda-spec.adoc §Linter rules): a module declares a top-level theorem whose name matches the headline pattern, but the name is not pinned in any `Smoke.agda` via a `using ( … )` clause. This enforces the estate "every headline pinned in Smoke" discipline — a renamed or silently-dropped headline surfaces as a missing pin rather than a quiet regression. - Warn severity (per spec): not every matching definition is a headline the operator wants pinned, and the default pattern is intentionally broad. - Headline detection keys on top-level (column-0) type signatures `name : T` (and `a b c : T`), which yields the "export-only" filter for free since `private`-block definitions are indented. - Pins are the union of every `using ( … )` list across the `Smoke.agda` files among the entry modules; tolerant of multi-line `using` lists. - Self-skips when no `Smoke.agda` is in scope (e.g. single-file `check`), and never lints a `Smoke.agda` for its own headlines. - Operator-configurable regex via `--headline-pattern <regex>` on `scan`/`dag`; default `^[a-z][A-Za-z0-9-]*$` per the spec open question. The pattern lives on the rule instance, so `LintContext` and the other rules' tests are untouched. Persisting it in `.arghda/config.toml` remains a follow-up. Adds the `regex` dependency. 12 unit tests; dogfooded against the echo-types corpus (fires as a non-blocking warn, pins read correctly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019GiSiEfgZCte35dyykgBHs
1 parent 4e47f3a commit a443d2e

8 files changed

Lines changed: 550 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ All notable changes to arghda-core are documented here. The format follows
2424
- Lint rules: `unjustified-postulate` (hard-block), `escape-hatch` (warn:
2525
`TERMINATING`-family pragmas + `believe_me` / `primTrustMe`), `tab-mix`
2626
(warn).
27+
- `unpinned-headline` (warn): flags a top-level theorem whose name matches
28+
the headline pattern but is not pinned in any `Smoke.agda` via a
29+
`using ( … )` clause. The pattern is operator-configurable via
30+
`--headline-pattern <regex>` on `scan` / `dag` (default
31+
`^[a-z][A-Za-z0-9-]*$`, per the spec). Detects top-level (column-0)
32+
signatures only, which gives the export-only filter for free; tolerant of
33+
multi-line `using` lists; self-skips when no `Smoke.agda` is in scope.
2734
- RSR scaffolding: `.machine_readable/6a2/` artefacts, `0-AI-MANIFEST.a2ml`,
2835
`Justfile`, `.well-known/`, and community-health files.
2936
- Content-hash invalidation of `proven`: promotion records a SHA-256 of the

Cargo.lock

Lines changed: 39 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
@@ -14,6 +14,7 @@ path = "src/main.rs"
1414
anyhow = "1"
1515
clap = { version = "4", features = ["derive"] }
1616
notify = "6"
17+
regex = "1"
1718
serde = { version = "1", features = ["derive"] }
1819
serde_json = "1"
1920
walkdir = "2"

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ record.
2323
`NON_TERMINATING`, `NO_TERMINATION_CHECK`) and trust primitives
2424
(`believe_me`/`primTrustMe`)
2525
- `tab-mix` (warn) — a tab in leading whitespace
26+
- `unpinned-headline` (warn) — a top-level theorem whose name matches the
27+
headline pattern is not pinned in any `Smoke.agda` via a `using ( … )`
28+
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`)
2633
- Workspace state machine — transitions are file moves, each logged to
2734
`.arghda/events.jsonl` (`claim`, `promote`, `reject`, `requeue`,
2835
`invalidate`)
@@ -44,11 +51,12 @@ plus standalone scratch files. `scan` also flags the files deliberately
4451
outside the `--safe --without-K` kernel cone (`Fidelity.agda`, the cubical
4552
island, the postulated shadow).
4653

47-
Not yet: `unpinned-headline` (needs `Smoke.agda` parsing + a configurable
48-
regex) and `unused-import` (shells out to `agda-unused`); content-hash
49-
invalidation of `proven`; the Groove service manifest; and the
50-
`.machine_readable/` RSR retrofit. (`missing-without-k` is subsumed by
51-
`missing-safe-pragma`, which already reports a missing `--without-K`.)
54+
Not yet: `unused-import` (shells out to `agda-unused`); the DAG `headlines`
55+
field (the extractor now exists for `unpinned-headline`, but the per-node
56+
`headlines` array in the DAG schema is still unpopulated); persisting the
57+
headline pattern in `.arghda/config.toml` (currently a CLI flag); the Groove
58+
service manifest. (`missing-without-k` is subsumed by `missing-safe-pragma`,
59+
which already reports a missing `--without-K`.)
5260

5361
## Build
5462

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ pub use dag::{build as build_dag, DagDocument};
2020
pub use diagnostic::{Diagnostic, LintReport, Severity};
2121
pub use event::{Event, EventKind};
2222
pub use graph::{build as build_graph, ImportGraph};
23-
pub use lint::{default_rules, run_lints, LintRule};
23+
pub use lint::{default_rules, rules_with_config, run_lints, LintRule, RuleConfig};
2424
pub use workspace::{StaleEntry, State, Workspace};

src/lint/mod.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod orphan_module;
77
pub mod postulate;
88
pub mod safe_pragma;
99
pub mod tab_mix;
10+
pub mod unpinned_headline;
1011

1112
/// Context handed to every rule.
1213
#[derive(Clone, Debug)]
@@ -25,14 +26,42 @@ pub trait LintRule: Send + Sync {
2526
fn run(&self, file: &Path, ctx: &LintContext<'_>, report: &mut LintReport) -> Result<()>;
2627
}
2728

28-
pub fn default_rules() -> Vec<Box<dyn LintRule>> {
29-
vec![
29+
/// Operator-configurable lint settings (the `.arghda/config.toml` surface
30+
/// the spec calls for; currently set via the CLI `--headline-pattern` flag).
31+
#[derive(Clone, Debug)]
32+
pub struct RuleConfig {
33+
/// Regex a top-level definition name must match to be treated as a
34+
/// pinnable headline by the `unpinned-headline` rule.
35+
pub headline_pattern: String,
36+
}
37+
38+
impl Default for RuleConfig {
39+
fn default() -> Self {
40+
Self {
41+
headline_pattern: unpinned_headline::DEFAULT_HEADLINE_PATTERN.to_string(),
42+
}
43+
}
44+
}
45+
46+
/// The standard lint pack, parameterised by operator config. Fails only if a
47+
/// supplied pattern (e.g. `headline_pattern`) is not a valid regex.
48+
pub fn rules_with_config(cfg: &RuleConfig) -> Result<Vec<Box<dyn LintRule>>> {
49+
Ok(vec![
3050
Box::new(safe_pragma::SafePragma),
3151
Box::new(orphan_module::OrphanModule),
3252
Box::new(postulate::UnjustifiedPostulate),
3353
Box::new(escape_hatch::EscapeHatch),
3454
Box::new(tab_mix::TabMix),
35-
]
55+
Box::new(unpinned_headline::UnpinnedHeadline::new(
56+
&cfg.headline_pattern,
57+
)?),
58+
])
59+
}
60+
61+
/// The standard lint pack with default config. The default pattern is a
62+
/// known-good literal, so this is infallible.
63+
pub fn default_rules() -> Vec<Box<dyn LintRule>> {
64+
rules_with_config(&RuleConfig::default()).expect("default rule config is valid")
3665
}
3766

3867
pub fn run_lints(

0 commit comments

Comments
 (0)