okf-kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +286 -0
- package/README.md +58 -2
- package/dist/rules/citations-resolve.d.ts +2 -0
- package/dist/rules/citations-resolve.js +1339 -0
- package/dist/rules/citations-resolve.js.map +1 -0
- package/dist/rules/index.d.ts +2 -1
- package/dist/rules/index.js +3 -1
- package/dist/rules/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,292 @@ All notable changes to `okf-kit` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.6.0] - 2026-08-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `citations-resolve` now also resolves **short-form** citations: a bare
|
|
15
|
+
(no backtick, no file name) `:N-M` colon-range, collected only when the
|
|
16
|
+
nearest preceding non-whitespace text is a serial connective (`,`, `;`,
|
|
17
|
+
`(`, or the word `and`/`or`) and bound to the last full `path:N-M`
|
|
18
|
+
citation named earlier in the same paragraph (a paragraph boundary is a
|
|
19
|
+
blank line), with no further check on the two ranges' relationship. A
|
|
20
|
+
gate-cleared candidate with no full citation earlier in its own
|
|
21
|
+
paragraph is reported `short-form-unbound` (notice), not silently
|
|
22
|
+
skipped; a candidate the gate rejects is dropped before binding is ever
|
|
23
|
+
attempted and produces no finding of any kind. Only a range form is
|
|
24
|
+
recognised (`:580-588`); a bare single number (`:5`) is not, since it is
|
|
25
|
+
too easily an unrelated enumeration marker (e.g. a numbered list) rather
|
|
26
|
+
than a citation. Never collected at all inside a fenced/indented code
|
|
27
|
+
block, an inline code span, or a Markdown table row. Reserved files
|
|
28
|
+
(`index.md`, `log.md`) are excluded from short-form matching: they are
|
|
29
|
+
append-only narrative journals that routinely describe historical "old
|
|
30
|
+
:N-M -> new :X-Y" line-number deltas as prose, not live citations
|
|
31
|
+
against current content; full/continuation citations in reserved files
|
|
32
|
+
are unaffected. A bare parenthesized range `(N-M)` is deliberately NOT
|
|
33
|
+
collected as a short-form citation at all -- see "Fixed" below for why.
|
|
34
|
+
See "Short-form citations" in the README for the full shape/exclusion
|
|
35
|
+
rules and their known limitations.
|
|
36
|
+
- `citations-resolve` now additionally checks, for a short-form citation's
|
|
37
|
+
range into a test file (`.test.ts`/`.spec.ts`, and the `.js`/`.mjs`
|
|
38
|
+
equivalents), that the range starts on a `describe(`/`it(` head line
|
|
39
|
+
(`test-range-start-not-head`, a **warning**) and ends on a matching
|
|
40
|
+
closing `});` line (`test-range-end-not-closing`, a **notice**: a
|
|
41
|
+
correct start with a short end is also consistent with a deliberate
|
|
42
|
+
partial citation, not necessarily drift).
|
|
43
|
+
- `citations-resolve` now additionally checks, for any short-form
|
|
44
|
+
citation's range into a Markdown target, whether its start or end line
|
|
45
|
+
is a bare bracket (`)`, `]`, `}`, `(`, `[`, `{`, optionally with a
|
|
46
|
+
trailing `,`/`;`) or a bare code-fence delimiter -- a common signature of
|
|
47
|
+
a boundary that drifted onto structural punctuation rather than real
|
|
48
|
+
content. This is a **notice**, not a warning (`markdown-range-boundary-bracket-or-fence`):
|
|
49
|
+
mechanical verification of "is this still the same block" is far less
|
|
50
|
+
reliable for prose than for code brace structure. A range starting on a
|
|
51
|
+
genuine *opening* fence line is exempted from the fence-as-drift-signal
|
|
52
|
+
part of this check: citing a fenced block starting at its own opening
|
|
53
|
+
delimiter is the natural, correct way to cite it.
|
|
54
|
+
- `citations-resolve` now supports an optional **anchor** on a full
|
|
55
|
+
citation: `` `path:N-M#anchor` `` (e.g. `` `CHANGELOG.md:50-144#0.24.0` ``),
|
|
56
|
+
closing a gap the range/blank/closing-brace checks above cannot: a
|
|
57
|
+
CHANGELOG.md that grows by insertion at the top shifts every later
|
|
58
|
+
entry's absolute line numbers on every release, so a citation that lands
|
|
59
|
+
15 lines off in the wrong release section is exactly as green as before
|
|
60
|
+
the shift (the motivating case: 16 `docs/okf/*.md` -> `CHANGELOG.md`
|
|
61
|
+
citations in this repo's own `orchestrator-workflow` bundle, migrated to
|
|
62
|
+
this form as part of the same change -- see that package's CHANGELOG).
|
|
63
|
+
Two forms, told apart by the anchor text itself:
|
|
64
|
+
- **Heading form** (bare/unquoted, e.g. `#0.24.0` or `#[0.24.0]`): the
|
|
65
|
+
citation's nearest *enclosing* Markdown heading must contain the
|
|
66
|
+
anchor text, and no heading of the same or shallower level may start
|
|
67
|
+
before the range's end line -- i.e. the heading must enclose the
|
|
68
|
+
*whole* range, not merely precede its start. Deliberately capped at
|
|
69
|
+
heading level 2: a Keep-a-Changelog `CHANGELOG.md` nests `## [x.y.z]`
|
|
70
|
+
release headings around identically-named `### Added`/`### Changed`/
|
|
71
|
+
`### Fixed` subsections repeated in every release, so "nearest heading
|
|
72
|
+
of any level" would make this check nearly useless (it would match the
|
|
73
|
+
wrong release's own `### Changed` just as readily as the right one's);
|
|
74
|
+
deeper subsection headings are transparent to the search instead. The
|
|
75
|
+
anchor text itself matches word characters, `.`, and `-`, but never
|
|
76
|
+
starts or ends on a `.`/`-`, so a trailing sentence period or a
|
|
77
|
+
following `,`/`)` is never captured as part of it, and a hyphenated
|
|
78
|
+
token (`0.24.0-rc1`) is captured whole rather than truncated at the
|
|
79
|
+
first hyphen. A `#`-led comment line inside a fenced code block in the
|
|
80
|
+
*target* is excluded from the heading search on both ends of the
|
|
81
|
+
enclosure check, the same way a citing doc's own fences are already
|
|
82
|
+
excluded from short-form matching. Mismatch is reported as
|
|
83
|
+
`anchor-heading-mismatch` (wrong section) or
|
|
84
|
+
`anchor-heading-does-not-enclose` (right section, but the range runs
|
|
85
|
+
past its end); no heading at all precedes the citation is
|
|
86
|
+
`anchor-heading-not-found`. All three are **warnings**.
|
|
87
|
+
- **String form** (double-quoted, e.g. `#"reproduction requirement"`):
|
|
88
|
+
the anchor text must occur, verbatim, on at least one line of the
|
|
89
|
+
cited range itself -- "occurs inside it" rather than "encloses it", so
|
|
90
|
+
this form also works against a non-Markdown target where "enclosing
|
|
91
|
+
heading" has no meaning. The quoted text cannot cross a line break or
|
|
92
|
+
a backtick, so an unterminated opening quote fails to parse as an
|
|
93
|
+
anchor at all instead of greedily consuming everything up to some
|
|
94
|
+
unrelated later quote character in the document -- which would
|
|
95
|
+
otherwise silently hide every citation in between from this rule
|
|
96
|
+
entirely. Mismatch is `anchor-not-found-in-range`, a **warning**.
|
|
97
|
+
Anchors are full-citation-only (a continuation or short-form citation
|
|
98
|
+
never carries its own path to hang one on) and strictly additive: the
|
|
99
|
+
`#anchor` suffix is optional, so an existing anchorless citation matches
|
|
100
|
+
and is checked exactly as before. Three rejected alternatives: embedding
|
|
101
|
+
the literal heading markup (`` #"## [0.24.0]" ``) was rejected as reading
|
|
102
|
+
worse in prose for no additional precision over the shorter heading-form
|
|
103
|
+
token; a detached anchor elsewhere in the sentence was rejected as
|
|
104
|
+
needing a second, unparseable-without-a-new-grammar citation site that
|
|
105
|
+
is easy to leave behind when a sentence is edited later; a named-capture
|
|
106
|
+
slug matching `sources-fresh`'s YAML shape was rejected because it would
|
|
107
|
+
require a second citation site (frontmatter plus prose) to stay in sync,
|
|
108
|
+
the exact class of drift this rule exists to catch.
|
|
109
|
+
**Known limitations:** the heading form's containment check is a plain
|
|
110
|
+
substring match against the heading's raw text, not a token-boundary
|
|
111
|
+
match (an anchor `0.1` matches a heading containing `[0.10.0]`); this is
|
|
112
|
+
a deliberate mechanical simplification, not a semantic guarantee. The
|
|
113
|
+
heading form also runs against a target's raw lines regardless of file
|
|
114
|
+
type, so a `# comment` line in a `.yml`/`.json` target is matched as a
|
|
115
|
+
heading; restricting the heading form to `.md` targets is left as a
|
|
116
|
+
known limitation rather than implemented in this change.
|
|
117
|
+
**Measured** (this change; see the PR for the full mutation-probe log):
|
|
118
|
+
the migrated `orchestrator-workflow` bundle (3 docs, 16 anchored
|
|
119
|
+
citations) reports the same 0 errors / 13 warnings / 22 notices with the
|
|
120
|
+
anchors present as without them (0 true findings, since all 16 were
|
|
121
|
+
already correct; 0 false positives from the new check, in this corpus).
|
|
122
|
+
A read-only sample against `agent-grounding/docs/okf` (not migrated to
|
|
123
|
+
this form, out of scope for this change, and carrying no anchors at all)
|
|
124
|
+
serves as a backward-compatibility check rather than a false-positive
|
|
125
|
+
measurement of the anchor check itself: it reports 0 anchor findings,
|
|
126
|
+
confirming an anchorless corpus is unaffected, as expected from the
|
|
127
|
+
backward-compatible design. Two mutation probes against the
|
|
128
|
+
`orchestrator-workflow` bundle: shifting one migrated citation's range
|
|
129
|
+
into its neighbouring release section raised the warning count from 13
|
|
130
|
+
to 14 (`anchor-heading-mismatch`), reverted to 13 clean; inserting an
|
|
131
|
+
8-line dummy entry at the top of that package's `CHANGELOG.md`
|
|
132
|
+
(simulating a normal release-note insertion) raised the warning count
|
|
133
|
+
from 13 to 29, flagging all 16 migrated citations as
|
|
134
|
+
`anchor-heading-mismatch` (the historical failure mode this change
|
|
135
|
+
targets), reverted to 13 clean.
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
|
|
139
|
+
- The test-file block-boundary check above previously applied only to
|
|
140
|
+
paren-form short forms, on the documented theory that a colon-form short
|
|
141
|
+
form inside a compound `path:N-M, :X-Y, :Z-W`-style list marked an
|
|
142
|
+
approximate detail location rather than a block citation. Reviewing that
|
|
143
|
+
claim by sampling this rule's own dogfood bundle -- replacing the
|
|
144
|
+
paren-only gate with an unconditional check locally raised the dogfood
|
|
145
|
+
finding count from 1 to 14, and reading a sample of the new findings
|
|
146
|
+
against both the citing prose and the cited file showed every one was a
|
|
147
|
+
whole cited block shifted by the same constant line offset, i.e. real
|
|
148
|
+
drift the carve-out was hiding for the dominant colon-form syntax, not a
|
|
149
|
+
granularity convention. The carve-out is removed; see the new
|
|
150
|
+
warning/notice severity split above for how a wrong start (strong drift
|
|
151
|
+
evidence) and a wrong end (also consistent with a deliberate partial
|
|
152
|
+
citation) are now told apart instead of one gating the whole check by
|
|
153
|
+
syntax form.
|
|
154
|
+
- `short-form-unbound` previously fired at warning severity, so an
|
|
155
|
+
unrecognised bare `N-M` range anywhere in ordinary prose (a year range,
|
|
156
|
+
a port range, a small plain-English number range) could fail a
|
|
157
|
+
consumer's `--strict` run. It is now a notice.
|
|
158
|
+
- Short-form matching previously scanned fenced code blocks, indented code
|
|
159
|
+
blocks, Markdown table rows, and (partially -- only a match directly
|
|
160
|
+
touching a backtick was excluded) inline code spans for bare ranges. All
|
|
161
|
+
four are now excluded from short-form matching entirely.
|
|
162
|
+
- The README stated the test-file block-boundary check applied "for a
|
|
163
|
+
paren-form short form only", including for the Markdown notice; the
|
|
164
|
+
Markdown notice was never actually gated that way in code (the CHANGELOG
|
|
165
|
+
already said so correctly). Both are now stated accurately and
|
|
166
|
+
consistently (moot for the test-file check specifically, now that its
|
|
167
|
+
paren-only gate is removed).
|
|
168
|
+
- The `short-form-unbound` message ("no target document named earlier in
|
|
169
|
+
this paragraph") was inaccurate for a paragraph that does name a target
|
|
170
|
+
document but without a `:N` line number (e.g. this rule's own dogfood
|
|
171
|
+
target, `log.md`, narrating historical deltas): the target document was
|
|
172
|
+
named, `CITATION_RE` just never matched it as a full citation. Reworded
|
|
173
|
+
to "no full `path:N` citation earlier in this paragraph to bind to".
|
|
174
|
+
- Three successive rounds tried to separate a real short-form citation
|
|
175
|
+
from ordinary prose that merely contains an N-M-shaped number pair by
|
|
176
|
+
deciding from the range's *values*: an inverted-pair/span-cap check,
|
|
177
|
+
then a year/well-known-port plausibility gate, then a
|
|
178
|
+
containment-or-adjacency check against the paragraph's last full
|
|
179
|
+
citation (`isContainedOrAdjacent`, now removed -- there is no longer a
|
|
180
|
+
second value-shape filter layered behind the gate). All three were
|
|
181
|
+
defeated by the same class of false positive: a doc with
|
|
182
|
+
`src/t.test.ts:3-7` followed by "covers phases (1-2), (2-4), and (5-6)"
|
|
183
|
+
produced two false `test-range-start-not-head` warnings under the
|
|
184
|
+
containment/adjacency version, because every real paren-form citation
|
|
185
|
+
this rule was ever built against has the identical shape
|
|
186
|
+
`<English word> (N-M)` as ordinary prose -- there is no lexical signal
|
|
187
|
+
that tells them apart. The bare parenthesized form `(N-M)` is therefore
|
|
188
|
+
no longer collected as a short-form citation candidate at all. Measured
|
|
189
|
+
against this rule's own dogfood bundle
|
|
190
|
+
(`packages/orchestrator-workflow/docs/okf`), a compiled copy with paren
|
|
191
|
+
collection disabled produced an identical finding count to one with it
|
|
192
|
+
enabled (39 findings / 17 warnings / 22 notices either way) -- dropping
|
|
193
|
+
it cost nothing.
|
|
194
|
+
- The colon form does not have the paren form's problem, but still needs
|
|
195
|
+
a gate to avoid the same false-positive class: a candidate `:N-M` is
|
|
196
|
+
now collected only when the nearest preceding non-whitespace text is a
|
|
197
|
+
serial connective -- `,`, `;`, `(`, or the word `and`/`or` -- and, once
|
|
198
|
+
collected, binds unconditionally to the paragraph's last full citation
|
|
199
|
+
(the containment-or-adjacency check above is gone; a bound candidate is
|
|
200
|
+
simply checked, with no further gate on the two ranges' relationship).
|
|
201
|
+
Measured over the 137 markdown files in this repo: 37 bare `:N-M`
|
|
202
|
+
occurrences preceded by whitespace or punctuation, 29 of them
|
|
203
|
+
serial-connective-preceded, and all 29 are genuine citations (13
|
|
204
|
+
preceded by `(`, 12 by a comma, 4 by `and`) -- the comma is
|
|
205
|
+
load-bearing at 12 of 29, so the gate is not narrowed to `(` and `and`
|
|
206
|
+
only. The 8 non-serial occurrences the gate correctly excludes are this
|
|
207
|
+
package's own README quoting false-positive examples (now removed from
|
|
208
|
+
the README along with the plausibility-gate description they
|
|
209
|
+
illustrated), this rule's own test fixture, and `docs/okf/log.md`'s
|
|
210
|
+
"old :N-M -> new :X-Y" delta narration (a reserved file, already
|
|
211
|
+
skipped regardless of the gate). Documented residual, not fixed: a
|
|
212
|
+
prose shape where the serial connective happens to precede a bare range
|
|
213
|
+
that is still not a real citation (e.g. "the exposed ports, :80-443,
|
|
214
|
+
stayed open") still binds and can produce a false warning; not observed
|
|
215
|
+
anywhere in the 137-file corpus.
|
|
216
|
+
- Net effect on this rule's own dogfood bundle
|
|
217
|
+
(`packages/orchestrator-workflow/docs/okf`): 49 findings / 16 warnings /
|
|
218
|
+
33 notices (previous round, containment-or-adjacency) -> 39 findings /
|
|
219
|
+
17 warnings / 22 notices (this round). The delta is exactly the 11 false
|
|
220
|
+
`short-form-unbound` notices the containment/adjacency gate was
|
|
221
|
+
producing disappearing, plus exactly one warning appearing:
|
|
222
|
+
`init.ts:538-569 (short-form)`, `closing-brace-start-line` -- see "Known
|
|
223
|
+
finding" below, this is a real, previously-suppressed finding, not a
|
|
224
|
+
new false positive. All findings present before this round's gate
|
|
225
|
+
change are unaffected.
|
|
226
|
+
|
|
227
|
+
### Known finding
|
|
228
|
+
|
|
229
|
+
Dogfooding this change against `packages/orchestrator-workflow/docs/okf`
|
|
230
|
+
surfaces citation drift beyond the single pre-existing one already known
|
|
231
|
+
(`install-fence-mechanics.md`'s `init.ts:538-569`, a short-form landing on
|
|
232
|
+
a lone closing brace, restored to a warning by this round's gate change --
|
|
233
|
+
see "Fixed" above): a compound colon-form list in the same doc, citing
|
|
234
|
+
`test/init.test.ts`'s `describe("tier variants (\`--tiers\`)")` block,
|
|
235
|
+
names 15 short-form sub-ranges. 13 of them produce a citations-resolve
|
|
236
|
+
finding (12 `test-range-start-not-head` warnings, 1
|
|
237
|
+
`test-range-end-not-closing` notice on `:1229-1265`); 11 of those 13 are
|
|
238
|
+
exact-length blocks shifted by a constant offset from their real
|
|
239
|
+
`describe`/`it` block (+33 lines for three of them, +116 for the other
|
|
240
|
+
eight -- consistent with roughly two rounds of content having been
|
|
241
|
+
inserted earlier in the same block without the rest of the list being
|
|
242
|
+
re-numbered). The other 2 of the 13 do not fit that pattern: `:1229-1265`
|
|
243
|
+
is at offset 0 (its start line is the real block head) but cites a
|
|
244
|
+
37-line span against a real 70-line block that grew; `:1636-1725` cites a
|
|
245
|
+
90-line span against a real 91-line block. Of the 2 remaining
|
|
246
|
+
citations that produce no finding at all: `:1170-1227` is genuinely
|
|
247
|
+
correct (not drifted); `:1614-1626` is drifted by the same +116 pattern
|
|
248
|
+
as the 8 above but produces no finding -- a known, inherent blind spot of
|
|
249
|
+
this mechanical, non-semantic checker: its cited start line coincidentally
|
|
250
|
+
lands on a real (but different) `describe`/`it` head, and its cited end
|
|
251
|
+
line coincidentally lands on a real (but unrelated, nested) closing
|
|
252
|
+
`});`, so the check cannot distinguish it from a correct citation. Fixing
|
|
253
|
+
any of these citations is out of scope for this change (no content fixes
|
|
254
|
+
to consumer-bundle citations).
|
|
255
|
+
|
|
256
|
+
Separately, the reserved-file carve-out (see "Fixed" above) leaves
|
|
257
|
+
genuine short-form citations in `index.md`/`log.md` completely unchecked.
|
|
258
|
+
The current dogfood bundle has four such citations, around
|
|
259
|
+
`log.md:362-370`.
|
|
260
|
+
|
|
261
|
+
## [0.5.0] - 2026-08-22
|
|
262
|
+
|
|
263
|
+
### Added
|
|
264
|
+
|
|
265
|
+
- New rule `citations-resolve`: for docs with a repo root, flags a
|
|
266
|
+
`` `path:N`/`path:N-M` `` citation (and its `` `:N` ``/`` -`M` ``/`` (`N`) ``
|
|
267
|
+
continuations) whose target file is missing, whose range is inverted or
|
|
268
|
+
exceeds the file, or whose start line is blank or (for a non-markdown
|
|
269
|
+
target) only a closing brace. Ported from agent-grounding's
|
|
270
|
+
`scripts/okf-citations-resolve.mjs` (agent-grounding PR #185), a
|
|
271
|
+
repo-local spike that closed a gap `sources-fresh` cannot: `sources-fresh`
|
|
272
|
+
only compares a doc's `sources` list against file mtimes, so it is
|
|
273
|
+
structurally blind to an edit that shifts line numbers inside a still-fresh
|
|
274
|
+
source file. `citations-resolve` gives every repo consuming okf-kit this
|
|
275
|
+
check without copying the script. See "Citation resolution
|
|
276
|
+
(citations-resolve)" in the README. Not ported: the original's
|
|
277
|
+
`docs/testing`-file expansion (an agent-grounding-specific convention, not
|
|
278
|
+
part of the OKF spec, and not exercised by the ported tests) -- this rule
|
|
279
|
+
only scans the docs already loaded for the bundle being checked.
|
|
280
|
+
`citations-resolve` findings are warn-level: an existing `okf-kit check
|
|
281
|
+
--strict` run that was previously green can now fail once this version is
|
|
282
|
+
picked up, purely from this rule's new warnings, with no other change to
|
|
283
|
+
the bundle. Two path-resolution refinements differ from the
|
|
284
|
+
agent-grounding original: (1) a bare filename with no `/` (e.g.
|
|
285
|
+
`README.md`) now tries the citing doc's own directory and each ancestor
|
|
286
|
+
directory up to the repo root, nearest first, before falling back to a
|
|
287
|
+
plain repo-root-relative lookup, so a package-level file is not shadowed
|
|
288
|
+
by a same-named file that happens to also sit at the repo root; (2) a
|
|
289
|
+
`` `path:N` `` match that starts at column 0 of its line, right after a
|
|
290
|
+
previous line ending in `-`/`–`, is treated as the phantom tail of a
|
|
291
|
+
filename hard-wrapped across the line break and skipped, instead of being
|
|
292
|
+
checked (and typically flagged `missing-file`) as its own citation.
|
|
293
|
+
|
|
8
294
|
## [0.4.0] - 2026-08-13
|
|
9
295
|
|
|
10
296
|
### Changed
|
package/README.md
CHANGED
|
@@ -53,11 +53,12 @@ okf-kit init path/to/bundle --force
|
|
|
53
53
|
|
|
54
54
|
Every template doc except `benchmark-template.md` ships with `sources: [path/to/covered/source]`, a placeholder, not a real path. Running `okf-kit check` against the freshly scaffolded bundle (with a repo root available, explicit or auto-detected) will report that placeholder as a `sources-shape` "does not exist" error on every template doc. That is intentional: it is the tool telling you which docs still need a real source path, not a bug in the scaffold. The `init` completion message repeats this so it isn't missed. Replace each placeholder with the real repo-root-relative path(s) the doc describes as you write it, and the error clears doc by doc.
|
|
55
55
|
|
|
56
|
-
### Authoring guidance
|
|
56
|
+
### Authoring guidance
|
|
57
57
|
|
|
58
58
|
- **`timestamp` means "last verified against sources," not "created on."** Bump it, and add a line to `log.md`, every time you re-verify a doc against its sources. Always use the real instant of verification (`new Date().toISOString()` or equivalent); never hand-write an artificial midnight datetime, `sources-fresh` staleness comparisons depend on it being real.
|
|
59
59
|
- **Never list the bundle's own directory in `sources`.** A bundle directory changes on every doc edit inside it, so a self-referential `sources` entry goes permanently stale. This happened to the OKF pilot's own `BENCHMARK.md` (`agent-tasks` `docs/okf/BENCHMARK.md`, `sources: [docs/okf/]`); `benchmark-template.md` here omits `sources` entirely for the same reason, since a benchmark record measures the bundle rather than describing a piece of the codebase.
|
|
60
60
|
- **Keep all links same-directory relative.** Use `name.md`, not `/name.md`; see `no-absolute-links` above for why a leading slash breaks once the bundle is viewed outside its own repository.
|
|
61
|
+
- **Write a sibling short-form citation as a connective-led `:N-M`, not `(N-M)`.** When a paragraph cites several sub-ranges of a source already named by a full `path:N-M` citation earlier in the same paragraph, write each later one as a `:N-M` led by one of the serial connectives the gate accepts -- `,`, `;`, `(`, or a trailing `and`/`or` -- right after the phrase it points at (e.g. `review finding L1 (:1170-1227, ...)`). `citations-resolve` only recognises the colon form; a parenthesized `(N-M)` is never checked, so it can drift silently. This convention is not demonstrated by any scaffolded template; it is a `citations-resolve` authoring rule. See "Citation resolution (citations-resolve)" below.
|
|
61
62
|
|
|
62
63
|
## Check catalog
|
|
63
64
|
|
|
@@ -69,6 +70,7 @@ Every template doc except `benchmark-template.md` ships with `sources: [path/to/
|
|
|
69
70
|
| `no-absolute-links` | warning | Link targets should not start with `/`. GitHub resolves a leading slash against the repository root, not the bundle root, so an absolute link 404s once the bundle is viewed outside its own repository. Use a same-directory relative link instead. |
|
|
70
71
|
| `sources-shape` | error | Frontmatter `sources`, when present, must be a non-empty array of non-empty strings. With a repo root (explicit or auto-detected), each listed path (file or directory) must also exist under it. |
|
|
71
72
|
| `sources-fresh` | warning / notice | For docs with a `sources` list and a repo root, flags a source path whose last git commit is newer than both the doc's `timestamp` and the doc file's own last commit. See "Staleness (sources-fresh)" below. |
|
|
73
|
+
| `citations-resolve` | warning / notice | For docs with a repo root, flags a `` `path:N`/`path:N-M` `` citation (and its `` `:N` ``/`` -`M` ``/`` (`N`) `` continuations, and bare paragraph-bound short forms `:N-M`/`(N-M)`) whose target file is missing, whose range is inverted or exceeds the file, or whose start line is blank or (for a non-markdown target) only a closing brace. A full citation may also carry an optional `#anchor` (e.g. `` `CHANGELOG.md:50-144#0.24.0` ``), checked against the target's own structure/content instead of just its line numbers. A short-form citation's range into a test file is also checked for a describe/it block boundary. See "Citation resolution (citations-resolve)" below. |
|
|
72
74
|
|
|
73
75
|
## repo-root auto-detection
|
|
74
76
|
|
|
@@ -99,6 +101,60 @@ Known limitation: the doc-commit comparison suppresses staleness for every sourc
|
|
|
99
101
|
|
|
100
102
|
**Authoring guidance:** when you re-verify a doc against its sources, bump its frontmatter `timestamp` (and add a line to the bundle's `log.md`) so `sources-fresh` reflects that the doc is current again.
|
|
101
103
|
|
|
104
|
+
## Citation resolution (citations-resolve)
|
|
105
|
+
|
|
106
|
+
`sources-fresh` catches a source file changing after a doc's `timestamp`, but it is structurally blind to an edit that shifts *line numbers* inside a still-fresh file: a doc citing `path:42` keeps citing line 42 even after an edit moves the referenced content to line 50. `citations-resolve` finds every `` `path:N` ``/`` `path:N-M` `` citation in a doc, resolves `path` to a real file, and flags a citation that clearly cannot be pointing at real content any more. It is mechanical only (no symbol/AST resolution): it does not verify the cited line is *semantically* the right one, only that the target exists, the range is sound, and the start line is not blank or (for a non-markdown target) a lone closing brace/bracket.
|
|
107
|
+
|
|
108
|
+
| Rule id | Meaning |
|
|
109
|
+
|---------|---------|
|
|
110
|
+
| `missing-file` | The cited path could not be resolved to a real file (see path resolution below). |
|
|
111
|
+
| `path-traversal-rejected` | The cited path contains a `..` segment; rejected without ever being resolved. |
|
|
112
|
+
| `inverted-range` | A range's end line is before its start line. |
|
|
113
|
+
| `range-exceeds-file` | The cited line (or the end of a range) is past the end of the resolved file. |
|
|
114
|
+
| `blank-start-line` | The start line resolves to a blank line. A citation whose start line is blank is flagged; cite the first content line instead. Consumers enabling this rule on an existing bundle should expect to fix citations like this once, the first time they turn it on. |
|
|
115
|
+
| `closing-brace-start-line` | For a non-markdown target, the start line is only a closing brace/bracket/paren (`}`, `)`, `]`, optionally with a trailing `,`/`;`) -- a common signature of a cited block having moved. |
|
|
116
|
+
| `unresolved-ambiguous` | More than one file in the repo matches the cited path; reported as a `notice` (never counted toward `--strict`), not guessed at. |
|
|
117
|
+
| `unreadable-target` | The resolved target file exists but could not be read (e.g. permission denied); reported as a `notice` (never counted toward `--strict`) with the OS error code in `detail`, since an unreadable file is not evidence the citation itself is wrong. |
|
|
118
|
+
| `short-form-unbound` | A short-form citation (see below) has no full citation earlier in its own paragraph to bind to; reported as a `notice` (never counted toward `--strict`). |
|
|
119
|
+
| `test-range-start-not-head` | A short-form citation's range into a test file does not start on a `describe(`/`it(` head line. A wrong start is strong drift evidence, so this is a **warning**. |
|
|
120
|
+
| `test-range-end-not-closing` | A short-form citation's range into a test file has a correct start (a real `describe(`/`it(` head) but does not end on a matching closing `});` line; reported as a `notice` (never counted toward `--strict`), since a correct start with a short end is also consistent with a deliberate partial citation. |
|
|
121
|
+
| `markdown-range-boundary-bracket-or-fence` | A short-form citation's range into a Markdown target starts or ends on a bare bracket, or (except see the fence-opening exception below) a bare code-fence line; reported as a `notice` (never counted toward `--strict`). |
|
|
122
|
+
| `anchor-heading-mismatch` | A heading-anchored citation's (see below) nearest enclosing heading does not contain the anchor text -- the range now lands in the wrong section. **Warning**. |
|
|
123
|
+
| `anchor-heading-does-not-enclose` | A heading-anchored citation's anchor text matches its nearest enclosing heading, but the range runs past that heading's own section (a heading of the same or shallower level starts before the range ends). **Warning**. |
|
|
124
|
+
| `anchor-heading-not-found` | A heading-anchored citation has no heading (level 1 or 2) anywhere before its start line to anchor against; use a string anchor (`#"..."`) instead against a target with no heading structure. **Warning**. |
|
|
125
|
+
| `anchor-not-found-in-range` | A string-anchored citation's (see below) anchor text does not occur, verbatim, on any line of the cited range. **Warning**. |
|
|
126
|
+
|
|
127
|
+
**Anchored citations.** A full citation may carry an anchor directly after its range: `` `path:N-M#anchor` ``, e.g. `` `CHANGELOG.md:50-144#0.24.0` ``. This closes a gap the checks above cannot: a CHANGELOG.md that grows by inserting each new release at the top shifts every later entry's absolute line numbers, so a citation that lands 15 lines off in the *wrong* release section is exactly as green as before the shift -- none of `missing-file`/`inverted-range`/`range-exceeds-file`/`blank-start-line`/`closing-brace-start-line` can tell the difference. An anchor pins the citation to a piece of the target's own structure or content that a pure line-shift does not preserve. Two forms, told apart by the anchor text itself:
|
|
128
|
+
|
|
129
|
+
- **Heading form** (bare/unquoted, e.g. `#0.24.0` or `#[0.24.0]`): the citation's nearest *enclosing* Markdown heading (level 1 or 2 only -- see below) must contain the anchor text, and the range must not run past that heading's own section (no heading of the same or shallower level may start before the range's end line). Capped at heading level 2 deliberately: a Keep-a-Changelog `CHANGELOG.md` nests `## [x.y.z]` release headings around identically-named `### Added`/`### Changed`/`### Fixed` subsections repeated in every release, so matching "the nearest heading of any level" would make this check nearly useless there (it would match the wrong release's own `### Changed` just as readily as the right one's); deeper subsection headings are transparent to the search instead. The anchor text itself may contain word characters, `.`, and `-`, but never starts or ends on a `.` or `-`, so a trailing sentence period or a following `,`/`)` never becomes part of it (e.g. `` `path:7-8#0.24.0.` `` at the end of a sentence captures `0.24.0`, not `0.24.0.`); a hyphenated token like `0.24.0-rc1` is captured whole. A fenced code block inside the *target* (e.g. a `` ```bash `` example containing a `#`-led comment) is excluded from the heading search the same way a citing doc's own fences are already excluded from short-form matching (see below): a comment line inside an example is never mistaken for a real heading, on either end of the enclosure check.
|
|
130
|
+
- **String form** (double-quoted, e.g. `#"reproduction requirement"`): the anchor text must occur, verbatim, on at least one line of the cited range itself -- "occurs inside it" rather than "encloses it", so this form also works against a non-Markdown target (`.ts`/`.js`/...) where "enclosing heading" has no meaning. The quoted text cannot cross a line break or a backtick: an unterminated opening quote (a typo) fails to parse as an anchor at all (the citation is then checked as if no anchor had been written, with no diagnostic) rather than greedily consuming the rest of the document up to some unrelated later quote character, which would otherwise silently hide every citation in between from this rule entirely.
|
|
131
|
+
|
|
132
|
+
Anchors are checked only once the base checks above (blank-start-line, closing-brace-start-line, inverted-range, range-exceeds-file) already came back clean for that citation. Anchors are full-citation-only: a continuation or a short-form citation never carries its own path, so there is nowhere natural to hang one on. The `#anchor` suffix is entirely optional and strictly additive: an existing anchorless citation matches and is checked exactly as it was before this feature existed.
|
|
133
|
+
|
|
134
|
+
**Anchor syntax note.** Any `#` immediately following a citation's range is read as the start of an anchor, with no other signal required -- there is no way to write a bare range immediately followed by an unrelated `#` and have it ignored. This means a Markdown link like `` [note](path:1-2#x) `` or an editor-style fragment such as `path:1-2#L7` is interpreted as an anchored citation (`x` and `L7` respectively), whether that was intended or not. The recommended, unambiguous form is the one used throughout this doc: the whole citation, range and anchor together, inside a single pair of backticks, e.g. `` `path:1-2#L7` ``.
|
|
135
|
+
|
|
136
|
+
**Known limitations.** The heading form's `heading.text.includes(anchor.text)` check is a plain substring match against the heading's raw text, not a token-boundary match: an anchor `0.1` matches a heading containing `[0.10.0]` (and, by the same construction, `[0.24.0]` "contains" `24.0`). This is a deliberate simplification, not implemented as token-boundary matching in this round; treat a heading-anchored citation as a strong drift signal, not a semantic guarantee, the same way the rest of this rule's checks are mechanical rather than semantic. Separately, the heading form runs `MD_HEADING_RE` (a bare `` `#{1,6}\s+...` `` match) against the target's raw lines regardless of the target's own file type, so a `# comment` line in a `.yml` or `.json` target is matched as if it were a Markdown heading; restricting the heading form to `.md` targets is left as a known limitation rather than implemented here.
|
|
137
|
+
|
|
138
|
+
**Continuation citations.** Once a sentence states a full `path:N` citation, prose commonly repeats just the line (or range) for a later reference in the same sentence: `` `:N` ``/`` `:N-M` `` (bare colon-prefixed), `` -`M` ``/`` –`M` `` (hyphen- or en-dash-led, the tail of a split range like `` `path:N`-`M` ``), or `` (`N`) `` (parenthesized). Each resolves against the nearest preceding citation in the same doc that resolved to a real file; a continuation right after an unresolved, ambiguous, or out-of-scope citation is silently skipped, not misattributed to an earlier, unrelated file.
|
|
139
|
+
|
|
140
|
+
**Short-form citations.** Distinct from a continuation above: a *bare* (no backtick, no file name at all) colon-range `:N-M`, e.g. `:580-588` in running prose. A short-form citation binds to **the last full `path:N-M` citation named earlier in the same paragraph** -- not the nearest preceding citation anywhere in the doc (that is what a continuation does); a paragraph boundary is a blank (empty or whitespace-only) line. A short-form citation with no full citation earlier in its own paragraph is reported `short-form-unbound`, not silently skipped. Only a range is recognised, never a bare single number (`:5`): a bare number is too easily an unrelated enumeration marker (e.g. a numbered list item) to detect mechanically without a large false-positive cost. A resolved short-form citation gets every check a full citation gets, plus the test-file/Markdown block-boundary check above (`test-range-*` / `markdown-range-boundary-bracket-or-fence`).
|
|
141
|
+
|
|
142
|
+
**Serial-connective gate, and why the paren form is not collected at all.** Three earlier rounds each tried to separate a real short-form citation from ordinary prose that merely contains an N-M-shaped number pair ("the window (2026-2027)", "follow steps (2-4)", "three engineers (1-3)") by deciding from the range's *values*: an inverted-pair check, a span cap plus a year/well-known-port plausibility gate, then a containment-or-adjacency check against the paragraph's last full citation. All three were eventually defeated by the same class of false positive, because every real paren-form citation this rule was ever built against has the identical shape `<English word> (N-M)` as ordinary prose -- there is no lexical signal that tells them apart. The bare parenthesized form `(N-M)` is therefore not collected as a short-form citation candidate at all; measured against this repo's own dogfood bundle, dropping it changed nothing (39 findings / 17 warnings / 22 notices, identical with and without it).
|
|
143
|
+
|
|
144
|
+
The colon form does not have this problem, but still needs a gate: a candidate `:N-M` is collected only when the nearest preceding non-whitespace text, after trimming whitespace, is one of `,`, `;`, `(`, or ends in the word `and`/`or` -- the shape a short-form citation takes in a serial list of sub-ranges ("the `TODO` cells, :72-74 (the ...), and :92-97 (the ...)", "review finding L1 (:1170-1227, ...)"). This is a measured, not a guessed, gate: see the CHANGELOG for the corpus sample it was calibrated against. The comma is load-bearing, not just `(` and `and`; do not narrow the gate to those two. A candidate the gate rejects is dropped before any paragraph-binding is attempted -- it is not a citation, and produces nothing at all, not even `short-form-unbound`.
|
|
145
|
+
|
|
146
|
+
**Documented residual (not fixed).** A prose shape where the serial connective happens to precede a bare range that is still not a real citation -- e.g. "the exposed ports, :80-443, stayed open" -- still binds to the paragraph's last full citation and can produce a false warning. This was not observed anywhere in the corpus this gate was calibrated against (see the CHANGELOG); it is a known, accepted limitation of a mechanical (non-semantic) gate rather than something this round attempted to close.
|
|
147
|
+
|
|
148
|
+
**Excluded from short-form matching entirely** (not collected, regardless of the serial-connective gate): a match inside a fenced code block, an indented code block, an inline code span (`` `like this` ``), or a Markdown table row -- a bare numeric range in any of these is virtually never a citation. Also excluded: reserved files (`index.md`, `log.md`), which are append-only narrative journals that routinely narrate historical "old :N-M -> new :X-Y" line-number deltas as prose about the past, not live citations against current content; full/continuation citations in reserved files are still checked as normal. This carve-out has a known cost: it also leaves genuine short-form citations in those files completely unchecked (see the CHANGELOG for the current dogfood bundle's gap).
|
|
149
|
+
|
|
150
|
+
**Markdown fence-opening exception.** A range boundary landing on a bare bracket line is always a drift signal. A bare code-fence delimiter (```` ``` ```` or `~~~`) at the range's END is also always a drift signal, but at the range's START it is exempted when that line is a genuine *opening* fence (determined by replaying the doc's own fence open/close state from the top, not by the line's text alone, since an untagged opening fence and a closing fence are lexically identical): citing a fenced code block starting at its own opening delimiter is the natural, correct way to cite it, not drift.
|
|
151
|
+
|
|
152
|
+
**Hard-wrapped prose.** A doc that hard-wraps prose at a fixed column can split a hyphenated filename across a line break right after its trailing `-` (e.g. `run-state-lifecycle-and-markers.md` wrapping to `run-state-lifecycle-and-\nmarkers.md`). A `` `path:N` `` match is skipped entirely (not checked, not counted as a citation) when it starts at column 0 of its line -- optionally after only whitespace or a list/quote marker (`-`, `*`, `>`, digits, `.`) -- and the previous line ends with `-`/`–`: the signature of a wrapped continuation rather than a genuine citation to a short bare filename.
|
|
153
|
+
|
|
154
|
+
**Path resolution**, tried in order: (1) the doc's own frontmatter `sources` list, matched by exact suffix, only when exactly one source matches; (2) for a citedPath with no `/` only, doc-relative and then each ancestor directory of the doc up to the repo root, nearest first (so a bare filename like `README.md` prefers the README *for the package the doc lives in* over a same-named file that merely happens to also sit at the repo root); (3) repo-root-relative; (4) doc-relative; (5) the nearest earlier citation in the same doc whose path contains a `/` and ends with the same suffix (the "full path was mentioned earlier" convention); (6) a repo-wide search by basename. A cited path starting with `/` is treated as an absolute or placeholder path (e.g. inside a fabricated example stack trace) and skipped without a finding.
|
|
155
|
+
|
|
156
|
+
Like `sources-fresh`, this rule requires a repo root (explicit `--repo-root` or auto-detected): without one, it emits a single bundle-level notice (`citation resolution skipped: not inside a git work tree`) rather than silently reporting nothing.
|
|
157
|
+
|
|
102
158
|
## Exit codes
|
|
103
159
|
|
|
104
160
|
| Code | Meaning |
|
|
@@ -113,7 +169,7 @@ This is advisory: don't fail the build on warnings unless you pass `--strict`. U
|
|
|
113
169
|
|
|
114
170
|
```yaml
|
|
115
171
|
- name: OKF bundle check
|
|
116
|
-
run: npx okf-kit@0.
|
|
172
|
+
run: npx okf-kit@0.6.0 check path/to/bundle
|
|
117
173
|
```
|
|
118
174
|
|
|
119
175
|
Pin the version: an unpinned `npx okf-kit` picks up new rules on their release day, which turns an unrelated PR red.
|