Skip to content

grund 0.9.1

Choose a tag to compare

@github-actions github-actions released this 17 Aug 09:58
· 2635 commits to main since this release

Added

  • §FS-lsp.1.2: the declaration-side title hover now answers "who leans on this?" — hovering a Markdown declaration heading, the same declaration written inline in a doc-comment, a numbered section heading, or an inline-spec stub title returns `FS-check: …` — cited at 12 sites across 5 files where it used to return the bare title token. That hover existed as an editor affordance and little else: a title hover has to return something for Codium and its relatives to give the whole title a hover range, and what it returned was the text the cursor was already resting on — a popup whose entire content was legible behind it. The fact that is genuinely missing from that screen is the usage, and reading it meant leaving the editor for grund refs <ID> (§FS-refs), which is the lookup an agent or a contributor runs before changing or deleting a declaration — precisely when the editor is open on the declaration. The numbers are that query's, not a second tally kept beside it: on a whole-ID title the counted set is by definition the set grund refs reports from the root the server was started at, citations inside scanned source comments included and [reference] strict honoured — in a workspace that is grund refs <alias>/<ID> run from the workspace root, counting a member's own §<ID> and a sibling's §<alias>/<ID> alike (§FS-workspace.8.2) — held by a test that runs the query over the same tree and compares, rather than by the claim. They are counted off the session snapshot the server already maintains, never a fresh query: refs loads its own workspace context, so calling it per hover would re-walk the tree on a request the editor fires as the cursor moves (§GOAL-fast-feedback, §AR-lsp.5). On a numbered section heading the count is the section subtree — §<ID>.<section> and deeper — which is the set that heading's own references and definition already return (§FS-lsp.1.3.1), and deliberately wider than grund refs <ID> --section <s>, whose filter keeps only citations whose coordinate is exactly <s> (§FS-refs.1). Where the two readings conflict, the one that keeps a title's hover and its reference list in agreement wins: those are one gesture apart in the same editor, while the terminal comparison is one a reader has to go looking for. The wording is fixed rather than clever — only the two nouns inflect, across is the preposition at every count, and zero reads not cited instead of cited at 0 sites across 0 files, a sentence pretending to be a count. The token inside the code span is the title as the document writes it, section headings included (1. Detail, not a composed FS-….1: Detail), and a title that carries backticks of its own — plenty of headings name a flag — is fenced with a run one longer than the longest run inside it rather than backslash-escaped, which does nothing inside a code span. Zero is not suppressed in favour of the unused-declaration warning (§FS-check.4.1) either: that warning never fires on E2E declarations or on section headings, so a hover that fell silent at zero would go quiet exactly where nothing else speaks — and not cited stays a count in the hover's own words, so an editor that renders diagnostics inside the hover popup still shows the warning once and only once (§FS-lsp.1.1). One matcher in grund-core now decides which citations belong to a declaration-side title, so the hover count, textDocument/references, definition-as-usages, and occurrence highlight cannot drift apart, and the body bytes are formed in the engine rather than in the transport, so a second frontend cannot re-word them (§FS-lsp.4). The LSP suite's stdio harness moved to shared test support on the way, which is what let the hover cases land as a suite named for the request they drive. Resolves issue #51. PR #63

  • §FS-check.1.3 / §FS-check.3.14 / §DF-check-full-scope: add grund check --full, which walks the whole config root past [scan] include and reports the references that resolve to nothing out there. include names the roots the walk starts from, so a citation outside it was not merely unchecked — it was invisible: it did not resolve, it did not dangle, it appeared in no report, and the declaration it pointed at was reported declared but never cited as if nothing referred to it. check printed success, which means "clean within include" and reads as "clean". That is the false negative §GOAL-no-dangling-refs calls a bug, and the worst-shaped one available: include is written once, early; code then moves into sim/, render/, lib/, and nothing ever says that half the repository's citations stopped being read — the citations that most need checking are exactly the ones somebody forgot to bring into scope. Widening include is the durable fix, but nobody widens a config toward a problem they cannot see, and pointing check at each candidate directory by hand is a search, not a check. The reported case found three genuine defects behind that gap: a citation a formatter had wrapped across a line break, a cited section that did not exist, and a citation missing its workspace namespace, so it resolved against the wrong project. The flag cancels include and nothing else. [scan] exclude, .gitignore and every other ignore file, hidden directories, workspace member boundaries, and [scan] extensions apply to the wider walk unchanged; an explicit PATH argument still narrows, because --full cancels a config key, never a scope the caller typed — a path that resolves to the config root (grund check ., grund check ./, grund check <abs-root>) is the root scope and does widen, while any other path leaves the flag nothing to cancel and earns one CLI-level warning: on stderr saying so, rather than being accepted in silence. Which file types carry citations is a project fact that belongs in the config; which directories were forgotten is the accident this flag exists to expose, and one flag that widened both would leave "what did this run read?" underivable from the config. Out there, only reference resolution is judged: unknown ID, missing section, unknown namespace alias, and a shorthand matching zero or several declarations — never style, grounding, placement, direction, duplicate, or unused. That line is not cheap-versus-expensive, it is what the repository agreed to: "this citation points at nothing" is true of any tree under any convention, while "this inline note is over budget" is true only of a tree that adopted that convention, and [scan] include is where a project says which files it adopted it for. The reported case is the argument in numbers — scanning one unconfigured directory produced 160 findings, of which 3 were real, the other 157 being inline-note budget violations on module docstrings that had never been written to that convention. A mode that fails CI on conventions a directory never adopted is a mode nobody runs twice. The one finding withheld on its own terms is the mechanical shorthand rewrite (§FS-check.3.13): its named fix is grund fmt --write, and fmt scopes by include too, so out there it would name a fix the formatter declines to apply. Out-of-scope findings are errors and move the exit code to 1. A warning would leave the mode exit-code-neutral, and a finding no CI run can fail on is one a repository accumulates behind forever — the argument §FS-check.3.13 already makes. Nothing turns red without being asked: the flag is opt-in and no existing invocation changes behaviour (§GOAL-no-silent-breakage). It is also purely additive — the findings inside the configured scope are exactly the ones grund check reports on the same tree, so --full can only turn a green run red, never the reverse, and a team can add it beside the ordinary check without re-auditing the verdict they already trusted. That is why the wider walk does not feed the in-scope rules: an inline declaration living outside include would otherwise make an in-scope citation resolve under --full and dangle without it, reported green by the mode meant to find more. The additive property has a mechanical precondition that is easy to get wrong: .gitignore, [scan] exclude, and the hidden-directory rule prune descendants and never the directory a walk starts at, so the wider walk starts at the config root and at every include root and reads each file once — starting at the root alone would lose a gitignored, excluded, or hidden include root that the ordinary run reads as a root of its own, and --full would read fewer files than grund check. One thing the flag deliberately does not change: an in-scope declaration cited only from outside include keeps its declared but never cited warning (§FS-check.4.1), because counting the wider walk's citations toward it would retire an in-scope finding — the one direction additivity forbids — and the remedy the tier already names is to widen include. The wider walk can also fail wider: a file it reaches that cannot be read or decoded is the ordinary scan failure, so a tree whose plain check exits 0 can exit 2 under --full. In a workspace the flag applies per project — root and every member widens past its own include, because include is a per-project statement; with [workspace] include_root = false there is no root project to widen, so files at the workspace root outside every member stay unread either way. It is a flag and never a grund.toml key (§FS-config.3.5): a project that wants its whole tree governed widens include and gets the whole rule set, and a key that made --full the default would be a second, weaker include — two knobs describing one scope, which is how two correctly-configured installs come to disagree (§FS-non-goals.13). No grund_config_version bump and no AGENTS.md block bump. Four new diagnostic codes carry the tier — out-of-scope-dangling, out-of-scope-missing-section, out-of-scope-unknown-project, out-of-scope-shorthand-citation, each the in-scope rule's code under an out-of-scope- prefix — so --format=json filters the tier by prefix and the rule by exact match on the code field the shape already has, and no consumer learns a new field (§FS-errors.5). In text the tier leads the message (sim/world.py:1: outside [scan] include: unknown reference FS-999-missing) rather than trailing it, because out there the fix is usually to widen the key and a rule's own did you mean …? hint is the part most likely to be wrong. CheckOpts gains full: bool, and the Config the crate hands an embedder gains scan_full: bool (the same flag, after the CLI has resolved it, so a caller building a Config directly can widen the walk too) — two documented, additive breaks in the library surface, like include_suggestions before it (§FS-distribution.3.1). The second half of the report — grepping ID-shaped tokens out of string literals and unscanned file types as an advisory tier — is declined, with the reasons recorded in §DF-check-full-scope.2.6. Both real cases already have a deterministic answer: a marker-prefixed citation is recognized everywhere, string literal or not, so writing the marker on the token in the data table makes it checked today; and a file type is scanned by adding its extension to [scan] extensions. Reporting unmarked tokens instead would re-admit the false-positive class strict = true exists to end, contradict a rule the specs state outright — a bare ID-shaped token inside a string literal is not a citation — and need exactly the "is this a data table or a coincidence?" judgment §FS-non-goals.3 refuses to make. The implementation lives in crates/grund-core/src/checker_references.rs (§AR-checker.2.13) — the reference-resolution family and the scope layer in one file, because --full is precisely that family run a second time over a wider tree. This repository now runs the flag on itself: the CI self-check step and the grund check pre-commit hook are both check --full (§AR-ci.1), which found ten unresolvable illustrative citations in README.md and examples/workspace/README.md — two files [scan] include has never covered — now written with the <§> escape. Resolves issue #55. PR #64

  • §FS-check.1.2 / §FS-check.3.13 / §DF-number-only-citation-shorthand: the number-only citation shorthand — §FS-042 for §FS-042-user-login — is recognized, and a persisted one is now an error naming the canonical form to write. Under the {kind}-{number}-{slug} format grund init generates, the number alone already identifies a declaration within its kind, so people write the short form; grund had no position on it at all and the failure mode was silent. A §-marked token that every reader takes for a live citation was dropped on the floor — not resolved, not flagged, absent from refs and cover — while the declaration it cited was simultaneously reported declared but never cited, which is the opposite of the truth and invites deleting a live declaration. A dangling full ID errored correctly; only this shape vanished. §GOAL-no-dangling-refs names that class in one line — false negatives are bugs — and the fix is one rule applied identically on every surface, because a shorthand that resolves in the editor and not in CI is worse than one that resolves nowhere. The shorthand is authoring sugar, not a second stored grammar (§DISC-declaration-local-shorthand reached the same verdict in advance): it expands where you type it, resolves where you pass it, and is corrected where it is stored — so one greppable string per edge survives, and a citation still tells a reader what it points at without a lookup. Typing $$FS-042 lands §FS-042-user-login in one step, in the editor (§FS-lsp.1.4) and under grund fmt --write (§FS-fmt.2.4, a new shorthand → canonical rewrite that is also the bulk fix-it for the error). grund FS-042 and grund refs FS-042 resolve, which is what makes a clicked §FS-042 open in a terminal or editor — the shared client matcher already claimed that shape, so before this the clients linked it and the resolver then reported the ID unknown (§FS-integrations.3.1); putting the rule in the engine rather than in six hand-written regexes is what keeps them from drifting from it. Three properties bound the recognition. The marker is required even under strict = false, where a bare full ID does count: KIND-NNN carries no slug to make an accidental match unlikely and occurs constantly as issue keys, part numbers, and standards references, so the marker is what supplies the intent (§DF-reference-marker.2.1). The full ID always wins, and only a whole token is a shorthand — the full pattern claims its tokens first, and the shorthand pass then requires that nothing which could continue an ID follows its match. Both halves are load-bearing: the shorthand is a prefix of every longer ID-shaped token, so without the trailing boundary a typo'd <§>FS-042-User-Login would be reported as <§>FS-042 and rewritten with -User-Login left glued to the canonical slug. And a resolved shorthand is a real edge: refs lists it, cover groups it, it grounds its file under require_grounding, it counts for citation directions, and the declaration stops being reported unused — the half that is easy to skip and expensive to omit, since the original report's most damaging symptom was not the missing error but the lie about an uncited declaration. Ambiguity is named, never guessed: a shorthand matching several declarations lists every candidate and resolves nothing, and one matching none says so — three message shapes, at most one shorthand finding per site, with §3.1 suppressed there so unknown reference FS-042 never names a token that is not a full ID (a bad section or a forbidden direction at the same site is a different fact and still reported). It is an error rather than a warning because a warning leaves the exit code alone, so a repository could accumulate shorthand citations forever while CI stayed green, which is the state this ends. That does mean a repository already holding shorthand citations gains errors on upgrade, and it needs no deprecation window (§GOAL-no-silent-breakage): the shorthand was never a working feature to deprecate — it resolved nowhere and appeared in no report — so nothing that used to pass and mean something now means something else; a token that meant nothing now means fix me, and says so with the exact replacement text and a grund fmt --write that applies it. The shape is derived from [id] format by dropping {slug} and one adjacent separator, so it exists only where the format carries both {number} and {slug} (§FS-id.4.1): a {kind}-{slug} repository like grund itself compiles no extra pattern and pays nothing, which is why this tree gains no findings. Cross-namespace <§>api/FS-042 resolves too (escaped here — this repository has no api member), against the aliased project's declarations, and grund fmt --write rewrites it there as well, under that project's [id] format. Where §FS-fmt.2.3 forbids the rewrite — inline code, a link destination, a runtime string — the error does not fire either, so everything check reports is something fmt can clear. No grund_config_version bump and no [id] key — a second knob would let two installs disagree about what a citation is (§FS-non-goals.13) — and no AGENTS.md block bump: the block tells an agent to write canonical citations, which is still exactly right, and bumping it would hand every repository on v7 an agents-init error for a rule that changes nothing about what they should write. The rule itself lives in one crates/grund-core/src/shorthand.rs (§AR-scanner.2.6, §AR-checker.2.12) rather than split across the four stages it plugs into, because the four halves of one invariant in four files with nothing naming the invariant is the drift this rule exists to prevent. What is not the rule sits beside it: the [id] format template parser and the ID renderer are grammar rather than shorthand (id_format.rs), and the keystroke-time transform is the LSP's (on_type.rs). Three shapes of the pass were rejected on the instruction-count meter before this one (§AR-benchmarks, §GOAL-fast-feedback), and each failure is worth recording because the obvious implementation of every one of them is the slow one. Sweeping the line with an unanchored shorthand pattern cost +46% on the 10k-file fixture, because FS-042 is a prefix of every FS-042-user-login, so the sweep yields one candidate per citation and each needs a second search to reject — the pass is therefore driven from marker positions and skips the markers the full-ID pass already claimed. Scanning the tree in grund fmt whenever the repo could have a shorthand cost +89% on fmt --check, which otherwise never scans at all — so fmt now starts with no declarations and scans on the first candidate it actually meets, redoing that one file, which means a repo that never writes a shorthand pays nothing. And compiling the two shorthand patterns at config load cost +3–5% on every read command, because on a small tree regex construction is a visible share of the whole run — they are built on first use instead. What remains is between 0.6% and 3.8% across the suite, all of it under the 5% regression gate (§AR-ci.5). Resolves issue #50. PR #62

Changed

  • §FS-lsp.1.4 / §FS-distribution.3.1: the grund-core on-type helper is now on_type_line_edits(path, text, line_index, cursor, declarations) -> Vec<LineEdit>, replacing on_type_trigger_replacement's Option<TriggerReplacement>. An embedder applies the returned edits in order instead of reading a trigger span plus an optional expansion; TriggerReplacement and TokenExpansion are gone and LineEdit and DeclaredId take their place. Three things forced the shape. The two rewrites fire on different keystrokes rather than together, so a struct with one mandatory trigger field could not say "this keystroke only expanded a shorthand". The expansion can rewrite a citation the author did not just type, so it has to honour the whole-line skips grund fmt applies — a fenced block, a declaration heading — which a single line cannot reveal: hence the document plus a line index rather than one line. And declarations carries each declaration's path so the expansion is scoped to the edited file's own project. Passing an empty slice still gives trigger conversion alone. PR #62
  • §FS-fmt.2.1: grund fmt now consumes a trigger followed by the number-only shorthand, so $$FS-042 becomes a citation where it used to stay literal text. A repository on a numbered format that contains a typo'd trigger — $$FS-999 naming nothing — therefore gains a §FS-check.3.13 error after grund fmt --write where the tree was previously green and silent. That is the intended direction: the author typed a citation trigger, so a token that resolves nowhere is a dangling reference and §GOAL-no-dangling-refs wants it named rather than dropped. PR #62
  • §FS-config.3.2: [id] number_pattern and [id] slug_pattern must each be a valid regex on their own. Two that balanced only against each other — number_pattern = "(" with slug_pattern = "a)" — used to load successfully; they are now rejected on load with the underlying regex error. PR #62

Fixed

  • §FS-config.3.5 / §AR-scanner.1: two [scan] include entries that name one subtree no longer report every declaration under it as a duplicate of itself. include = ["docs", "docs/functional-spec"] — or one directory written twice as docs and ./docs/ — started the walk twice inside the overlap, so grund check read each file there twice and every declaration came back as duplicate declaration of <ID> (also declared at <the same path>:<the same line>), exiting 1 with nothing the author could act on: the two sites the message named were one site, and the only fix was to guess that the config was the problem. A redundant include list should be a no-op, not an error accusing a file of duplicating itself. Each root is now folded through its path components, so docs, ./docs, and docs/ are one root, and the sorted file list is collapsed before scanning. Under §FS-check.1.3 the same collapse carries the wider walk, where the config root is walked beside every include root by construction; there it is keyed on file identity rather than on the path, because an include root may be a symlink to a directory inside the config root and the two walks would otherwise reach one file under two names. The include roots are walked first so the surviving spelling is the one the plain run prints, which is what makes the in-scope half of a --full report byte-identical to grund check on the same tree. PR #64