Repository navigation
grund 0.9.1
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 fileswhere 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 forgrund 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 setgrund refsreports from the root the server was started at, citations inside scanned source comments included and[reference] stricthonoured — in a workspace that isgrund 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:refsloads 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 thangrund 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,acrossis the preposition at every count, and zero readsnot citedinstead ofcited 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 composedFS-….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 onE2Edeclarations or on section headings, so a hover that fell silent at zero would go quiet exactly where nothing else speaks — andnot citedstays 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 ingrund-corenow 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] includeand reports the references that resolve to nothing out there.includenames 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 reporteddeclared but never citedas if nothing referred to it.checkprintedsuccess, which means "clean withininclude" and reads as "clean". That is the false negative §GOAL-no-dangling-refs calls a bug, and the worst-shaped one available:includeis written once, early; code then moves intosim/,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. Wideningincludeis the durable fix, but nobody widens a config toward a problem they cannot see, and pointingcheckat 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 cancelsincludeand nothing else.[scan] exclude,.gitignoreand every other ignore file, hidden directories, workspace member boundaries, and[scan] extensionsapply to the wider walk unchanged; an explicitPATHargument still narrows, because--fullcancels 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-levelwarning: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] includeis 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 isgrund fmt --write, andfmtscopes byincludetoo, so out there it would name a fix the formatter declines to apply. Out-of-scope findings are errors and move the exit code to1. 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 onesgrund checkreports on the same tree, so--fullcan 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 outsideincludewould otherwise make an in-scope citation resolve under--fulland 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 everyincluderoot and reads each file once — starting at the root alone would lose a gitignored, excluded, or hiddenincluderoot that the ordinary run reads as a root of its own, and--fullwould read fewer files thangrund check. One thing the flag deliberately does not change: an in-scope declaration cited only from outsideincludekeeps itsdeclared but never citedwarning (§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 wideninclude. 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 plaincheckexits0can exit2under--full. In a workspace the flag applies per project — root and every member widens past its owninclude, becauseincludeis a per-project statement; with[workspace] include_root = falsethere 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 agrund.tomlkey (§FS-config.3.5): a project that wants its whole tree governed widensincludeand gets the whole rule set, and a key that made--fullthe default would be a second, weakerinclude— two knobs describing one scope, which is how two correctly-configured installs come to disagree (§FS-non-goals.13). Nogrund_config_versionbump and noAGENTS.mdblock 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 anout-of-scope-prefix — so--format=jsonfilters the tier by prefix and the rule by exact match on thecodefield 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 owndid you mean …?hint is the part most likely to be wrong.CheckOptsgainsfull: bool, and theConfigthe crate hands an embedder gainsscan_full: bool(the same flag, after the CLI has resolved it, so a caller building aConfigdirectly can widen the walk too) — two documented, additive breaks in the library surface, likeinclude_suggestionsbefore 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 classstrict = trueexists 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 incrates/grund-core/src/checker_references.rs(§AR-checker.2.13) — the reference-resolution family and the scope layer in one file, because--fullis 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 thegrund checkpre-commit hook are bothcheck --full(§AR-ci.1), which found ten unresolvable illustrative citations inREADME.mdandexamples/workspace/README.md— two files[scan] includehas 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-042for§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}formatgrund initgenerates, the number alone already identifies a declaration within its kind, so people write the short form;grundhad 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 fromrefsandcover— while the declaration it cited was simultaneously reporteddeclared 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-042lands§FS-042-user-loginin one step, in the editor (§FS-lsp.1.4) and undergrund fmt --write(§FS-fmt.2.4, a newshorthand → canonicalrewrite that is also the bulk fix-it for the error).grund FS-042andgrund refs FS-042resolve, which is what makes a clicked§FS-042open 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 understrict = false, where a bare full ID does count:KIND-NNNcarries 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-Loginwould be reported as<§>FS-042and rewritten with-User-Loginleft glued to the canonical slug. And a resolved shorthand is a real edge:refslists it,covergroups it, it grounds its file underrequire_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 sounknown reference FS-042never 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 agrund fmt --writethat applies it. The shape is derived from[id] formatby 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 likegrunditself compiles no extra pattern and pays nothing, which is why this tree gains no findings. Cross-namespace<§>api/FS-042resolves too (escaped here — this repository has noapimember), against the aliased project's declarations, andgrund fmt --writerewrites 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 everythingcheckreports is somethingfmtcan clear. Nogrund_config_versionbump and no[id]key — a second knob would let two installs disagree about what a citation is (§FS-non-goals.13) — and noAGENTS.mdblock bump: the block tells an agent to write canonical citations, which is still exactly right, and bumping it would hand every repository on v7 anagents-initerror for a rule that changes nothing about what they should write. The rule itself lives in onecrates/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] formattemplate 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, becauseFS-042is a prefix of everyFS-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 ingrund fmtwhenever the repo could have a shorthand cost +89% onfmt --check, which otherwise never scans at all — sofmtnow 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-coreon-type helper is nowon_type_line_edits(path, text, line_index, cursor, declarations) -> Vec<LineEdit>, replacingon_type_trigger_replacement'sOption<TriggerReplacement>. An embedder applies the returned edits in order instead of reading a trigger span plus an optional expansion;TriggerReplacementandTokenExpansionare gone andLineEditandDeclaredIdtake 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 skipsgrund fmtapplies — a fenced block, a declaration heading — which a single line cannot reveal: hence the document plus a line index rather than one line. Anddeclarationscarries 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 fmtnow consumes a trigger followed by the number-only shorthand, so$$FS-042becomes a citation where it used to stay literal text. A repository on a numbered format that contains a typo'd trigger —$$FS-999naming nothing — therefore gains a §FS-check.3.13 error aftergrund fmt --writewhere 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_patternand[id] slug_patternmust each be a valid regex on their own. Two that balanced only against each other —number_pattern = "("withslug_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] includeentries 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 asdocsand./docs/— started the walk twice inside the overlap, sogrund checkread each file there twice and every declaration came back asduplicate declaration of <ID> (also declared at <the same path>:<the same line>), exiting1with 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 redundantincludelist should be a no-op, not an error accusing a file of duplicating itself. Each root is now folded through its path components, sodocs,./docs, anddocs/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 everyincluderoot by construction; there it is keyed on file identity rather than on the path, because anincluderoot may be a symlink to a directory inside the config root and the two walks would otherwise reach one file under two names. Theincluderoots are walked first so the surviving spelling is the one the plain run prints, which is what makes the in-scope half of a--fullreport byte-identical togrund checkon the same tree. PR #64