okf-kit 0.9.0 → 0.11.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 +256 -0
- package/README.md +102 -8
- package/dist/bundle.d.ts +18 -1
- package/dist/bundle.js +8 -1
- package/dist/bundle.js.map +1 -1
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -1
- package/dist/git.d.ts +2 -2
- package/dist/git.js +16 -2
- package/dist/git.js.map +1 -1
- package/dist/rules/index.d.ts +2 -2
- package/dist/rules/index.js +8 -2
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/sources-fresh.d.ts +42 -0
- package/dist/rules/sources-fresh.js +797 -20
- package/dist/rules/sources-fresh.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/util.d.ts +52 -0
- package/dist/util.js +72 -0
- package/dist/util.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,262 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.0] - 2026-09-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `check` gets `--dirty-as-now`: an opt-in flag under which every
|
|
15
|
+
uncommitted change (modified, staged, or untracked per `git status
|
|
16
|
+
--porcelain --untracked-files=all`) is modeled as though it landed in
|
|
17
|
+
ONE virtual commit made
|
|
18
|
+
right now, applied at a single shared choke point
|
|
19
|
+
(`commitEpochWithDirtyAsNow` in `src/rules/sources-fresh.ts`) so both
|
|
20
|
+
`sources-fresh` (a dirty `sources` path's epoch) AND `sources-fresh-future`
|
|
21
|
+
(a dirty DOC's own epoch) read the identical virtual instant, rather than
|
|
22
|
+
each rule -- or each path kind -- getting its own patch. Closes a gap
|
|
23
|
+
surfaced by the 2026-09-11 review-method dogfood (agent-dx PR #236): a
|
|
24
|
+
test file four OKF docs list as a source was edited, a pre-commit `check`
|
|
25
|
+
run reported 0 warnings because the source's last commit still predated
|
|
26
|
+
every doc's `timestamp`, and CI's strict freshness guard then failed once
|
|
27
|
+
the commit landed and gave the source a new, later commit time. Omit the
|
|
28
|
+
flag and both rules are unchanged; see the README's "Uncommitted edits
|
|
29
|
+
(`--dirty-as-now`)" section for the recommended pre-commit invocation and
|
|
30
|
+
the exact parity matrix (six working-tree states, each reproduced
|
|
31
|
+
through the built CLI with `--strict` exit codes, including the one
|
|
32
|
+
shape inherent to the model where a clean local run does NOT imply a
|
|
33
|
+
clean CI run: a source edit committed alone while its doc re-stamp stays
|
|
34
|
+
uncommitted). Carries a working-tree
|
|
35
|
+
analogue of `sources-fresh`'s own co-commit re-stamp rescue, evaluated
|
|
36
|
+
against that same virtual commit: a doc re-stamped locally (uncommitted,
|
|
37
|
+
on-disk `timestamp` differing -- in EITHER direction -- from the value
|
|
38
|
+
committed at `HEAD`; an untracked doc counts as re-stamped too) rescues a
|
|
39
|
+
dirty source exactly like a real re-stamp landing in the same commit
|
|
40
|
+
does, so the README's recommended `--dirty-as-now --strict` pre-commit
|
|
41
|
+
gate can actually be made green by following its own remedy (re-stamp,
|
|
42
|
+
then commit) while the source stays uncommitted. Reads the work tree's
|
|
43
|
+
dirty paths once per `check` run (`git --no-optional-locks status
|
|
44
|
+
--porcelain=v2 -z --untracked-files=all`), not once per unique source
|
|
45
|
+
path, and normalizes a `./`-prefixed or `/`-suffixed path spelling
|
|
46
|
+
before matching it against that dirty-paths set, so any spelling of the
|
|
47
|
+
same source or doc path matches consistently. `--untracked-files=all` is
|
|
48
|
+
load-bearing, not decoration: git's default untracked mode collapses a
|
|
49
|
+
brand-new directory into a single `? newdir/` record and never names the
|
|
50
|
+
files inside it, so a source or doc inside one matched nothing and the
|
|
51
|
+
flag reported `untracked by git` (exit 0) for content a plain `check`
|
|
52
|
+
reported STALE (exit 1) the moment it was committed -- the exact
|
|
53
|
+
divergence the flag exists to close. A `sources` entry spelled `.`/`./`
|
|
54
|
+
(the repo root) is matched by containment rather than by lookup, since
|
|
55
|
+
`git status` never reports the root as an entry; a `--repo-root` naming
|
|
56
|
+
a SUBDIRECTORY of the repository is handled by rebasing the queried path
|
|
57
|
+
onto the repository top level (`git rev-parse --show-prefix`, the run's
|
|
58
|
+
second and last extra git process) instead of silently matching nothing.
|
|
59
|
+
When git cannot be read at all (`status` failing, its output past the
|
|
60
|
+
16 MiB `MAX_GIT_OUTPUT_BYTES` cap, git missing), the run emits one
|
|
61
|
+
bundle-level notice -- "`--dirty-as-now` not applied: git status could
|
|
62
|
+
not be read, judging by committed history only" -- rather than falling
|
|
63
|
+
back to committed history in a silence indistinguishable from a clean
|
|
64
|
+
tree. Distinguishes a genuinely untracked doc (no entry
|
|
65
|
+
for it at `HEAD` at all) from a real git failure reading an existing
|
|
66
|
+
`HEAD` blob when deciding whether a dirty doc's own re-stamp counts,
|
|
67
|
+
falling through to the ordinary not-assessable notice in the latter
|
|
68
|
+
case rather than guessing.
|
|
69
|
+
|
|
70
|
+
### Documented
|
|
71
|
+
|
|
72
|
+
- Decision (task agent-dx 27c4f709): the anchored, path-less continuation
|
|
73
|
+
form the `orchestrator-workflow` package's dogfood bundle writes
|
|
74
|
+
(`` `:N-M#"anchor"` ``) is declared bundle-local, not ported into
|
|
75
|
+
`CONT_COLON_RE`/`SHORT_FORM_COLON_RE`. See the README's "Citation
|
|
76
|
+
resolution (citations-resolve)" section for the who-pays and the port
|
|
77
|
+
trigger.
|
|
78
|
+
- Decision, kept option (a): the rule stays as-is; two loosening
|
|
79
|
+
alternatives were considered and rejected. The README's "Staleness
|
|
80
|
+
(sources-fresh)" section now documents the two-branch squash-merge
|
|
81
|
+
interaction as the rule's own three-step decision procedure (the
|
|
82
|
+
doc-last-commit lookup under git's default history simplification, the
|
|
83
|
+
epoch gate, and the value-level re-stamp check) instead of an
|
|
84
|
+
enumeration of merge shapes, plus the one consequence that follows for
|
|
85
|
+
any branch merging a squash-carrying trunk. The two rejected
|
|
86
|
+
alternatives: (b) a source counts fresh whenever the doc's last commit
|
|
87
|
+
merely postdates it, with no re-stamp required -- readers pay, because
|
|
88
|
+
any merge or unrelated doc touch would silence real drift; (c) compare
|
|
89
|
+
against the doc's own last commit rather than the frontmatter stamp when
|
|
90
|
+
they disagree -- readers pay, because the stamp stops being the
|
|
91
|
+
verification claim. Under the kept option (a), branch authors pay
|
|
92
|
+
instead: one re-verify-and-re-stamp commit after merging a
|
|
93
|
+
squash-carrying trunk (or after changing a source past a trunk squash
|
|
94
|
+
the doc's history resolved through), the recipe now in the README.
|
|
95
|
+
Reproduced with a fixture repo (the trunk and eight branch variants:
|
|
96
|
+
doc untouched, B1; doc untouched with a second source changed after the
|
|
97
|
+
squash, B3; own-older-stamp kept wholesale before the squash, B2, and
|
|
98
|
+
after it, B2late, discriminating the epoch gate from the re-stamp
|
|
99
|
+
check; own-older-stamp with a mixed conflict resolution that changes
|
|
100
|
+
the stamp value, B2a, or does not, B2b; and both re-verify-and-re-stamp
|
|
101
|
+
recipe follow-ups, B2r and B3r) before writing the decision text, then
|
|
102
|
+
checked the procedure's own prediction against all eight plus the
|
|
103
|
+
trunk; no rule change. No fleet pin bump needed (no rule/behavior
|
|
104
|
+
change).
|
|
105
|
+
- Concrete case that motivated this decision: in the `agent-grounding`
|
|
106
|
+
repo, `docs/okf/grounding-stack-overview.md` read STALE after merging
|
|
107
|
+
master (batch-42 task branch `f31ad37f`'s squash-merged re-stamp) into a
|
|
108
|
+
second, longer-lived task branch (`d341afd5`), which carried its own
|
|
109
|
+
older stamp of the same doc; the batch-42 orchestrator run (pandora
|
|
110
|
+
workspace) recorded the resolution as decision D-015, exactly the
|
|
111
|
+
re-verify-and-re-stamp commit this README section now generalizes as
|
|
112
|
+
the recipe.
|
|
113
|
+
|
|
114
|
+
### Security
|
|
115
|
+
|
|
116
|
+
- devDependencies: vitest and @vitest/mocker 4.1.6 -> 4.1.11
|
|
117
|
+
(GHSA-82fw-gwwq-j7x9), lockfile only, no runtime dependency changed
|
|
118
|
+
(CVE sweep 2026-09-11, agent-dx PR #235).
|
|
119
|
+
|
|
120
|
+
## [0.10.0] - 2026-09-06
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- A new rule, `sources-fresh-future`, complements `sources-fresh`: it flags
|
|
125
|
+
a doc's frontmatter `timestamp` that is later than the doc file's own
|
|
126
|
+
last commit by more than a clock-skew allowance (default 10 minutes,
|
|
127
|
+
`--future-skew-minutes <n>`), catching a local wall-clock time
|
|
128
|
+
mistakenly written with a `Z`/UTC suffix it doesn't actually have.
|
|
129
|
+
Unlike `sources-fresh`, it never looks at `sources` commit times, only
|
|
130
|
+
at the doc file's own git history, and it is assessed over the same doc
|
|
131
|
+
population (a validly-shaped `sources` list and a repo root available).
|
|
132
|
+
An uncommitted doc (no own commit yet) is "unknown, not flagged", the
|
|
133
|
+
same posture `sources-fresh` already takes for an untracked source path.
|
|
134
|
+
`FUTURE-DATED` findings are `warning` severity, same as `STALE`; run
|
|
135
|
+
with the existing `--strict` flag to fail the build on either, rather
|
|
136
|
+
than a second, rule-specific strictness switch. Extends
|
|
137
|
+
`src/rules/sources-fresh.ts` (shares its `getLastCommitEpoch` git
|
|
138
|
+
helper and its doc-population filter) instead of adding a parallel
|
|
139
|
+
mechanism; see the README's "Staleness (sources-fresh)" section,
|
|
140
|
+
"Future-dated timestamps (`sources-fresh-future`)" subsection, for the
|
|
141
|
+
full rule contract and how the two rules relate. Together with the
|
|
142
|
+
`sources-fresh` narrowing below, catches: a source path committed
|
|
143
|
+
after the doc's `timestamp`; a source and the doc co-committed
|
|
144
|
+
together where that commit did not re-stamp the doc; and a local
|
|
145
|
+
wall-clock time hand-written with a `Z`/UTC suffix it doesn't
|
|
146
|
+
actually have. Does NOT catch a doc-only prose edit that leaves
|
|
147
|
+
`sources` untouched and the `timestamp` stale -- neither rule has a
|
|
148
|
+
source-side signal to compare against in that case, so it stays a
|
|
149
|
+
reviewer judgment call.
|
|
150
|
+
- `sources-fresh-future` skips (severity `notice`) a `timestamp` string
|
|
151
|
+
with no `Z`/UTC designator or numeric offset (e.g.
|
|
152
|
+
`2026-01-01T00:00:00`): such a string parses in the machine's own
|
|
153
|
+
local timezone under `Date.parse`, which would swing the check's
|
|
154
|
+
verdict by hours between a UTC+2 laptop and a UTC CI runner against a
|
|
155
|
+
default 10-minute allowance. A numeric offset (`+02:00`, `-0500`) is
|
|
156
|
+
unambiguous and is still assessed normally. `sources-fresh`'s own
|
|
157
|
+
thresholds are days wide, so this ambiguity does not practically
|
|
158
|
+
matter there; the gate applies only to `sources-fresh-future`.
|
|
159
|
+
- CI: the agent-dx `okf-anchor-guard` job (`.github/workflows/ci.yml`)
|
|
160
|
+
gained a strict freshness step that fails the build on any
|
|
161
|
+
`sources-fresh`/`sources-fresh-future` warning for
|
|
162
|
+
`packages/orchestrator-workflow/docs/okf`. **Release dependency:**
|
|
163
|
+
`sources-fresh-future` did not exist in the okf-kit version this job
|
|
164
|
+
installed before this release (a pinned release from npm, kept in sync
|
|
165
|
+
with `package.json`'s own version by
|
|
166
|
+
`orchestrator-workflow/test/docs-consistency.test.ts`), so the step's
|
|
167
|
+
filter matched only `sources-fresh` findings until this release; the
|
|
168
|
+
job's pin moves to 0.10.0 in the same commit as every other
|
|
169
|
+
`okf-kit@<version>` pin (per this file's "Changed" entry below), so it
|
|
170
|
+
now matches both rule ids and no other change to the step is needed,
|
|
171
|
+
since it already filters by rule id rather than a fixed list.
|
|
172
|
+
|
|
173
|
+
### Fixed
|
|
174
|
+
|
|
175
|
+
- `sources-fresh`'s co-commit staleness exception (a source committed
|
|
176
|
+
at/before the doc file's own last commit is treated as fresh) is
|
|
177
|
+
narrowed to a commit that actually re-stamped the doc. A commit that
|
|
178
|
+
co-commits a source change with the doc (a prose edit, a typo fix)
|
|
179
|
+
WITHOUT touching the stamp no longer suppresses staleness --
|
|
180
|
+
previously this unconditional exception let exactly that case (a
|
|
181
|
+
source and the doc's prose committed together with the timestamp left
|
|
182
|
+
stale) pass silently, which was the review class this rule pair
|
|
183
|
+
exists to close. "Re-stamped" is decided by VALUE: the doc's parsed
|
|
184
|
+
frontmatter `timestamp` at that commit is compared against its value
|
|
185
|
+
in the commit's FIRST PARENT, and they must differ (a doc created
|
|
186
|
+
there, having no parent revision at all, counts as re-stamped). The
|
|
187
|
+
lookup follows a rename, so a `git mv` is not read as a creation, and
|
|
188
|
+
it reads trees rather than a patch, so a merge commit -- including the
|
|
189
|
+
`refs/pull/N/merge` ref CI checks out -- is assessed like any other
|
|
190
|
+
commit. Consequently a `timestamp:` line inside a fenced YAML example
|
|
191
|
+
in the doc's BODY is not mistaken for a re-stamp. The check answers
|
|
192
|
+
"did the value change", never "is the new value right": a hand-typed
|
|
193
|
+
or backdated stamp still counts (`sources-fresh-future` is the rule
|
|
194
|
+
that catches an implausible value), and a doc-only prose edit with
|
|
195
|
+
unchanged sources remains outside both rules' reach. When git cannot
|
|
196
|
+
answer the question at all, the doc gets one `staleness not
|
|
197
|
+
assessable` notice rather than a STALE warning or a silent pass. See
|
|
198
|
+
the README's "Staleness (sources-fresh)" section for the full contract
|
|
199
|
+
and its remaining known limitations.
|
|
200
|
+
- `sources-fresh`'s re-stamp lookup no longer misjudges two more shapes
|
|
201
|
+
the value comparison above did not yet cover. (1) In a SHALLOW clone
|
|
202
|
+
(`git clone --depth`, including `actions/checkout`'s default), the
|
|
203
|
+
grafted history boundary commit reports an EMPTY parent list for every
|
|
204
|
+
path touched at or before it -- indistinguishable from a real root
|
|
205
|
+
commit by the parent-list check alone, which previously trusted it and
|
|
206
|
+
assumed "created" (re-stamped) unconditionally. The lookup now checks
|
|
207
|
+
`git rev-parse --is-shallow-repository` (once per `check` run, not per
|
|
208
|
+
doc: only spent at all when some doc's re-stamp lookup actually
|
|
209
|
+
reaches a commit with no parents) and, when the repository is shallow,
|
|
210
|
+
answers `not assessable` there instead -- see the README's "CI usage"
|
|
211
|
+
section for the `fetch-depth: 0` remedy. (2) A NON-ASCII doc path was
|
|
212
|
+
C-quoted by `git diff-tree`'s default `--name-status` output
|
|
213
|
+
(`core.quotePath` defaults to true, e.g. `"bundle/\303\266lt.md"` for
|
|
214
|
+
`bundle/ölt.md`), so the rename/created lookup's plain string match
|
|
215
|
+
against the doc's real (unquoted) path never matched, silently fell
|
|
216
|
+
through to the wrong path, and turned a normal rename or creation into
|
|
217
|
+
a `not assessable` notice instead of the real verdict. The lookup now
|
|
218
|
+
runs with `-z` (NUL-delimited, never quoted regardless of
|
|
219
|
+
`core.quotePath`) instead of the default form. New fixtures also pin:
|
|
220
|
+
an octopus merge (three parents) as a doc's last commit without a
|
|
221
|
+
re-stamp still reports STALE (only the FIRST parent is ever consulted,
|
|
222
|
+
however many there are); and a cosmetic rewrite of the stamp to the
|
|
223
|
+
same instant in another string representation (`...00Z` to
|
|
224
|
+
`...00.000Z`) still counts as a re-stamp, since the comparison is by
|
|
225
|
+
raw parsed VALUE, not by resolved instant -- while adding or removing
|
|
226
|
+
quotes around an otherwise-unchanged value does NOT count, since YAML
|
|
227
|
+
parsing already normalizes those away before the comparison ever sees
|
|
228
|
+
them.
|
|
229
|
+
- `--future-skew-minutes ''` (empty or whitespace-only) is now rejected
|
|
230
|
+
as the same usage error (exit 2) a negative value already gets,
|
|
231
|
+
instead of silently accepting it as `0`.
|
|
232
|
+
- The internal git runner now sets an explicit 16 MiB output cap.
|
|
233
|
+
Node's default for a synchronous child process is 1 MiB, and
|
|
234
|
+
`sources-fresh` reads whole doc blobs to compare frontmatter
|
|
235
|
+
timestamps, so a doc larger than 1 MiB previously resolved to "git
|
|
236
|
+
failed" -- and therefore to a permanent not-assessable notice -- on a
|
|
237
|
+
perfectly healthy repository.
|
|
238
|
+
- CI: the `okf-anchor-guard` job's freshness step (`.github/workflows/ci.yml`)
|
|
239
|
+
now rejects any `error`-severity freshness finding too (was: only
|
|
240
|
+
`warning`), gained a self-test mirroring the neighbouring
|
|
241
|
+
Anchor-citation guard's shape (its `assert_numeric` guard included),
|
|
242
|
+
guards explicitly against a missing or malformed report before its
|
|
243
|
+
first `jq`, and runs with `if: success() || failure()` so its findings
|
|
244
|
+
surface in the same CI round as the Anchor-citation guard's rather
|
|
245
|
+
than being skipped after an anchor failure -- while, unlike
|
|
246
|
+
`always()`, staying out of the way of a cancelled run or an earlier
|
|
247
|
+
setup failure that never produced a report. The self-test step now
|
|
248
|
+
carries the same `if: success() || failure()` (it previously ran
|
|
249
|
+
unconditionally), and both it and the real step now read the SAME
|
|
250
|
+
`FRESHNESS_FILTER` jq expression from the job's `env:` rather than the
|
|
251
|
+
self-test guarding its own hand-kept copy: an edit to the real filter
|
|
252
|
+
is now exercised by the self-test, not silently bypassed by it.
|
|
253
|
+
|
|
254
|
+
### Changed
|
|
255
|
+
|
|
256
|
+
- The release procedure now bumps orchestrator-workflow's okf-kit pins
|
|
257
|
+
(every `npm install -g okf-kit@<version>` or `npx okf-kit@<version>`
|
|
258
|
+
occurrence under `.github/workflows/`) in the same commit as the
|
|
259
|
+
okf-kit version cut, via `scripts/bump-okf-kit-pin.mjs` (repo root,
|
|
260
|
+
supports `--dry-run`). The reason: a release PR that only cuts
|
|
261
|
+
okf-kit's own version leaves OW's docs-consistency parity guard red on
|
|
262
|
+
master, and since `publish-npm.yml` runs the tests at the tag tree, the
|
|
263
|
+
OW tag then cannot publish either. See CONTRIBUTING.md's "Releasing
|
|
264
|
+
okf-kit" section for the full order.
|
|
265
|
+
|
|
10
266
|
## [0.9.0] - 2026-09-01
|
|
11
267
|
|
|
12
268
|
### Added
|
package/README.md
CHANGED
|
@@ -29,8 +29,15 @@ okf-kit check path/to/bundle --repo-root /path/to/repo
|
|
|
29
29
|
# JSON output for tooling
|
|
30
30
|
okf-kit check path/to/bundle --json
|
|
31
31
|
|
|
32
|
-
# fail on warnings too, not just errors (STALE findings are warnings)
|
|
32
|
+
# fail on warnings too, not just errors (STALE and FUTURE-DATED findings are warnings)
|
|
33
33
|
okf-kit check path/to/bundle --strict
|
|
34
|
+
|
|
35
|
+
# narrow sources-fresh-future's default 10-minute clock-skew allowance
|
|
36
|
+
okf-kit check path/to/bundle --future-skew-minutes 2
|
|
37
|
+
|
|
38
|
+
# pre-commit: treat an uncommitted source edit as happening now, so this run
|
|
39
|
+
# reports the same staleness CI will report once you commit
|
|
40
|
+
okf-kit check path/to/bundle --dirty-as-now
|
|
34
41
|
```
|
|
35
42
|
|
|
36
43
|
## Scaffold a bundle (`init`)
|
|
@@ -55,7 +62,7 @@ Every template doc except `benchmark-template.md` ships with `sources: [path/to/
|
|
|
55
62
|
|
|
56
63
|
### Authoring guidance
|
|
57
64
|
|
|
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.
|
|
65
|
+
- **`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, or a local wall-clock time with a `Z` suffix it doesn't actually have -- `sources-fresh` and `sources-fresh-future` staleness comparisons both depend on it being real. See "Future-dated timestamps (`sources-fresh-future`)" below for the measure-after-commit discipline this enforces.
|
|
59
66
|
- **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
67
|
- **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
68
|
- **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.
|
|
@@ -69,7 +76,8 @@ Every template doc except `benchmark-template.md` ships with `sources: [path/to/
|
|
|
69
76
|
| `links-resolve` | error | Markdown links to other `.md` files in the bundle must resolve to a real file. Relative targets resolve against the containing file's directory; targets starting with `/` resolve against the bundle root. A relative target that climbs out of the bundle directory (`../outside.md`) and still resolves on disk is accepted; the rule checks resolution, not containment. |
|
|
70
77
|
| `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. |
|
|
71
78
|
| `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. |
|
|
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. |
|
|
79
|
+
| `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 re-stamping commit (re-stamp decided by comparing frontmatter `timestamp` values across that commit's first-parent boundary). With `--dirty-as-now`, a source with an uncommitted change is judged as of right now instead of its last commit. See "Staleness (sources-fresh)" below. |
|
|
80
|
+
| `sources-fresh-future` | warning | For the same docs as `sources-fresh`, flags a `timestamp` later than the doc file's own last commit by more than a clock-skew allowance (default 10 minutes, `--future-skew-minutes`). Catches a local wall-clock time mistakenly written with a `Z`/UTC suffix. See "Staleness (sources-fresh)" below. |
|
|
73
81
|
| `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 backtick-delimited `` `path:#heading` `` citation (`.md` targets only) resolves to a whole Markdown section instead of a line range, immune to every line-number shift above it; see "Heading-section citations" below. A short-form citation's range into a test file is also checked for a describe/it block boundary. `--require-anchors` opts into five additional checks; see "Anchor strictness (opt-in, `--require-anchors`)" below. See "Citation resolution (citations-resolve)" below. |
|
|
74
82
|
| `prose-line-references` | (opt-in, `--prose-line-references`) warning / notice | Off by default. Flags a prose-embedded line reference outside `citations-resolve`'s own backtick grammar (`line N`, `lines N-M`, `lines N to M`) that is drifted, unresolvable, or ambiguous once bound to the nearest named file. `--prose-line-references-strict` additionally flags every such reference as a formatting policy violation. See "Prose line references (opt-in, `--prose-line-references`)" below. |
|
|
75
83
|
|
|
@@ -83,12 +91,15 @@ Pass `--repo-root` explicitly to pin a specific root (useful in CI when the bund
|
|
|
83
91
|
|
|
84
92
|
## Staleness (sources-fresh)
|
|
85
93
|
|
|
86
|
-
`sources-fresh` compares each frontmatter `sources` entry's last git commit time against the doc's `timestamp`, and additionally against the doc file's own last commit time: a source committed at or before the doc file's last commit is treated as fresh even when the frontmatter `timestamp` is older. That keeps squash-merge PRs honest: when a doc re-stamp lands in the same commit as its changed sources, the merge gives every source a commit time later than any pre-merge `timestamp`, which used to make such docs stale-on-arrival. The rule never blocks a doc that has no `sources`, and it never invents an error where git can't give a real answer:
|
|
94
|
+
`sources-fresh` compares each frontmatter `sources` entry's last git commit time against the doc's `timestamp`, and additionally against the doc file's own last commit time: a source committed at or before the doc file's last commit is treated as fresh even when the frontmatter `timestamp` is older, PROVIDED that same commit actually re-stamped the doc. "Re-stamped" is decided by VALUE, not by diff text: okf-kit reads the doc's frontmatter at that commit and in the commit's first parent, parses both, and compares the `timestamp` values -- they differ, or the doc did not exist in the parent at all (it was created there), and it counts as re-stamped. The lookup follows a rename, so `git mv`ing a doc is not a creation, and it reads trees rather than a patch, so a merge commit (including the `refs/pull/N/merge` ref CI checks out) is assessed like any other commit. That keeps squash-merge PRs honest: when a doc re-stamp lands in the same commit as its changed sources, the merge gives every source a commit time later than any pre-merge `timestamp`, which used to make such docs stale-on-arrival. It does NOT extend to a commit that merely happens to also touch the doc file -- a typo fix, a co-committed prose edit, a repo-wide formatter run -- without rewriting the stamp: that commit carries no verification claim, so staleness stands. "Created" is trusted only in a genuinely unshallow repository: in a shallow clone (`git clone --depth`), a commit with an empty parent list can simply be the boundary history was grafted onto, not the doc's real first commit, so this case gets the same not-assessable notice as any other unanswerable re-stamp question rather than an assumed pass (see "CI usage" below for the `fetch-depth: 0` remedy). The rule never blocks a doc that has no `sources`, and it never invents an error where git can't give a real answer:
|
|
87
95
|
|
|
88
96
|
| Situation | Severity | Message |
|
|
89
97
|
|-----------|----------|---------|
|
|
90
98
|
| A source path's last commit is newer than the doc's `timestamp` and the doc file's last commit | warning | `STALE: <path> changed <iso> after doc timestamp <iso>` |
|
|
91
|
-
| A source path's last commit is newer than the doc's `timestamp
|
|
99
|
+
| A source path's last commit is newer than the doc's `timestamp`, at/before the doc file's last commit, AND that commit re-stamped the doc | (nothing) | fresh: doc and source landed together with a real re-stamp (or the doc was committed later, or created there) |
|
|
100
|
+
| A source path's last commit is newer than the doc's `timestamp`, at/before the doc file's last commit, but that commit did NOT re-stamp the doc | warning | `STALE: <path> changed <iso> after doc timestamp <iso>` (co-commit with no re-stamp is not an exception) |
|
|
101
|
+
| A source path's last commit is newer than the doc's `timestamp`, at/before the doc file's last commit, and git could not be asked whether that commit re-stamped the doc | notice | `staleness not assessable: git could not read the doc's own last commit to decide whether it re-stamped the doc` (once per doc, never a STALE warning and never a silent pass) |
|
|
102
|
+
| Same, but the repository is a shallow clone and the doc's last commit is the grafted history boundary (an empty parent list that is not really a root commit) | notice | `staleness not assessable: this is a shallow clone ... use fetch-depth: 0 ... to assess it` (once per doc) |
|
|
92
103
|
| A source path exists but has no git history (untracked) | notice | `untracked by git, staleness unknown: <path>` |
|
|
93
104
|
| The doc's `timestamp` is missing or not a parseable date, while `sources` is present | notice | `staleness not assessable: no valid timestamp` |
|
|
94
105
|
| No repo root available (see auto-detection above) | notice | `staleness skipped: not inside a git work tree` |
|
|
@@ -98,10 +109,83 @@ STALE findings are warnings, so they are advisory by default; run with `--strict
|
|
|
98
109
|
|
|
99
110
|
Known limitation: a `git log` call that fails for a reason other than "no history for this path" (for example a corrupt object or a transient git error) is reported the same way as a genuinely untracked path, the `untracked by git, staleness unknown` notice; okf-kit does not currently distinguish a real git failure from "no commits touch this path".
|
|
100
111
|
|
|
101
|
-
Known limitation: the doc-commit comparison suppresses staleness for every source older than the doc file's last commit, not only for sources from the same commit. For a multi-source doc that means
|
|
112
|
+
Known limitation: the doc-commit comparison suppresses staleness for every source older than the doc file's last re-stamping commit, not only for sources from the same commit -- provided that commit is the one that rewrote the stamp. For a multi-source doc that means a re-stamp silences drift on every source changed before it, even ones that specific re-stamp did not itself verify. The frontmatter `timestamp` still governs sources changed after the doc's last commit. Also note: the check only looks at WHETHER the `timestamp` value changed, never at whether the new value is itself correct -- a hand-typed or deliberately backdated stamp still counts as a re-stamp (`sources-fresh-future` is the rule that catches an implausible value). "Changed" means the parsed frontmatter VALUE changed: a rewrite to the same instant in another representation (`...00Z` to `...00.000Z`) still counts as a re-stamp, since the raw string differs even though it names the same instant, while adding or removing quotes around an otherwise-unchanged value does NOT count, since YAML parsing already normalizes those away before the comparison ever sees them. And the comparison is against the FIRST parent only: a merge that takes the doc wholesale from its second parent is judged against the first-parent baseline, which is the baseline the pull request under review is measured against anyway.
|
|
113
|
+
|
|
114
|
+
**Two-branch squash interaction:** the rule follows one decision procedure, never a catalog of merge shapes, and the procedure is what to read a squash-merge scenario against, not a list of named cases:
|
|
115
|
+
|
|
116
|
+
1. **The doc's last commit.** `git log -1 -- <doc>` under git's default history simplification: a merge whose result for the doc path is identical to one of its parents is skipped, and the walk continues on that side to the real content-changing commit instead; only a merge that changed the doc against every parent is itself returned. The epoch lookup and the re-stamp check below both start from this same resolved commit.
|
|
117
|
+
2. **The epoch gate.** A source whose own last commit is at or before the resolved doc commit is judged by step 3; a source committed after the resolved doc commit skips step 3 and is judged by the frontmatter `timestamp` alone: STALE whenever the source change also postdates that stamp (the normal case, since nothing the doc showed as of its last commit could reflect a change that had not happened yet), fresh in the rare case that the stamp itself is later than the source change, because the frontmatter comparison is the first thing the rule evaluates.
|
|
118
|
+
3. **The re-stamp check (`restampedByOwnLastCommit`).** Compare the resolved commit's parsed frontmatter `timestamp` VALUE against its value in that SAME commit's first parent (rename-aware); a difference, or the doc being created there, counts as re-stamped.
|
|
119
|
+
|
|
120
|
+
One sentence for the outcome: a source reads fresh when its own last commit is at or before the doc's resolved last commit and that commit re-stamped the value, or when the source's own last commit is at or before the frontmatter `timestamp`; every other assessable case is STALE (the notice outcomes in the table above are neither fresh nor STALE).
|
|
121
|
+
|
|
122
|
+
The one consequence that follows for a branch merging a trunk carrying a squash: the squash re-dates every source it touched to the merge instant, and steps 1-3 can only ever see the MERGING branch's own resolved doc commit, never the trunk's route to that squash. So after such a merge, a clean verdict is not evidence the branch's doc was verified against the re-dated source (the known limitation above already says a re-stamp silences drift on sources it never itself checked, and step 3 only asks whether the value moved, never what it verified); a STALE verdict means the branch's resolved doc commit is older than the re-dated source, or newer but carrying no re-stamp of its own. Which of the two the rule reports depends only on where the branch's resolved doc commit falls relative to the squash (steps 1 and 2), never on who edited what:
|
|
123
|
+
|
|
124
|
+
| Branch shape | Doc commit the lookup resolves to | Verdict | Why |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| B never touched the doc | The trunk's squash commit (the merge is skipped: its doc content matches the trunk's) | Fresh for a source at/before the squash; STALE for a source B itself changes after the squash and after the stamp | The squash's own re-stamp (step 3) only covers sources the epoch gate lets reach it (step 2); a later change on B's own branch fails that gate |
|
|
127
|
+
| B carries its own older doc edit and keeps it through the merge | B's own doc-edit commit (the merge is skipped: its doc content matches B's pre-merge copy) | STALE when the edit predates the squash; fresh when it postdates the squash and changed the stamp value (the re-stamp exception); STALE when it postdates the squash but left the stamp alone | Step 3 only asks whether B's edit changed the value against its OWN first parent, never what the edit verified; an edit that lands after the squash and differs from its own prior value passes the gate and reads re-stamped regardless |
|
|
128
|
+
|
|
129
|
+
**Recipe:** after merging a trunk that carries a squash into any branch whose bundle declares the sources that squash re-dated, re-verify each such doc against its current `sources` and bump its `timestamp` in one dedicated commit, whatever `sources-fresh` currently reports for it -- the same recipe as the Authoring guidance below, just triggered by the merge. Seen in practice on a long-lived second branch after a squash-merged re-stamp landed on the trunk; see the CHANGELOG for the concrete case.
|
|
102
130
|
|
|
103
131
|
**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.
|
|
104
132
|
|
|
133
|
+
### Uncommitted edits (`--dirty-as-now`)
|
|
134
|
+
|
|
135
|
+
`sources-fresh` and `sources-fresh-future` normally judge every path by its last GIT COMMIT time, so a source or doc you have edited but not yet committed still reads by whatever it looked like at its last commit -- often still "fresh" relative to an already-stale doc `timestamp`. Once you commit, that same path gets a brand-new commit time, and CI's freshness guard (typically run in strict mode) can fail on a doc your local, pre-commit `check` run reported as clean. `--dirty-as-now` closes that gap by modeling every uncommitted change -- modified, staged, or untracked, i.e. anything `git status --porcelain --untracked-files=all` reports -- as though it landed in ONE VIRTUAL COMMIT made right now: a dirty `sources` path's epoch becomes now, and a dirty DOC's OWN epoch becomes now too, both read from the exact same shared instant so two dirty paths in one `check` run never disagree about what "now" was. Both rules read a dirty path's epoch this same way -- there is exactly one place in the code that applies the substitution, not a separate patch per rule -- so a pre-commit run reports the same verdict CI will report once the commit lands, for either rule. It has no effect on a path with no uncommitted change (judged exactly as before), and it is entirely opt-in: omit it and both rules are byte-for-byte unchanged. One consequence worth calling out: an untracked source's ordinary `untracked by git, staleness unknown` notice (see the table above) becomes a STALE warning under this flag instead, since an untracked path is itself an uncommitted change; symmetrically, an untracked DOC -- which `sources-fresh-future` otherwise leaves unassessed entirely (see that rule's table above) -- gets a real `sources-fresh-future` verdict too, since its virtual epoch is now a real "now" to compare its `timestamp` against. Both hold for a file inside a BRAND-NEW untracked directory as well, which is why the status read passes `--untracked-files=all`: git's default untracked mode reports a new directory as one single `? newdir/` record and never names the files in it, so a source or doc inside one used to be invisible to this flag (reported merely `untracked by git`, exit 0) right up until the commit that made it STALE in CI. The cost of `--untracked-files=all` is a longer status output in a work tree carrying many untracked, non-ignored files; if it ever exceeds the internal 16 MiB git-output cap, the run reports the not-applied notice below rather than pretending the tree is clean.
|
|
136
|
+
|
|
137
|
+
Three smaller behaviors worth knowing. A `sources` entry spelled `.` or `./` (the repo root itself) is dirty whenever ANY path in the work tree is dirty -- it is matched by containment, since `git status` never reports the root as an entry of its own. `--repo-root` may name a SUBDIRECTORY of the repository: dirty paths are reported relative to the repository's top level and are rebased onto the passed root (via `git rev-parse --show-prefix`) before matching, so dirty-path detection works there instead of silently matching nothing; the doc re-stamp rescue does not yet read the doc blob relative to such a root (it reads `HEAD:<path>` relative to the top level), so under a subdirectory root a locally re-stamped doc can still read as STALE and a dirty-but-not-re-stamped doc as clean; prefer the repository top level as `--repo-root` until that is fixed. And if git cannot be read at all (`status` failing, output past the cap, git missing), `check` emits one bundle-level `sources-fresh` notice reading "`--dirty-as-now` not applied: git status could not be read, judging by committed history only", and both rules fall back to committed history; the flag never reports a silent all-clear it did not actually verify.
|
|
138
|
+
|
|
139
|
+
The flag also carries a working-tree analogue of the doc-commit re-stamp rescue described above, evaluated against that same virtual commit: a doc re-stamped LOCALLY (its on-disk frontmatter `timestamp` value differs from the value committed at `HEAD` -- a change in EITHER direction, including a backwards move, counts -- and the doc file itself is dirty; an untracked doc counts as re-stamped too, the working-tree equivalent of "the doc being created there") rescues a dirty source exactly like a real re-stamp landing in the same commit does. Without this, bumping a doc's `timestamp` locally and re-running `check --dirty-as-now` could still report STALE, purely because a few seconds pass between writing the new timestamp and running the check -- defeating the point of the recommended invocation below. A doc that is dirty for some OTHER reason (a body edit that leaves the `timestamp` value unchanged) is not rescued: that case still reports STALE, same as before. Reading the doc's committed value distinguishes a genuinely untracked doc (no entry for it at `HEAD` at all -- always a re-stamp) from a real git failure reading an EXISTING `HEAD` blob (a corrupt object, an unreadable blob): only the former is treated as "created"; the latter falls through to the ordinary `staleness not assessable` notice instead of being silently guessed either way.
|
|
140
|
+
|
|
141
|
+
**Recommended pre-commit invocation**, matching the "re-verify, bump `timestamp`, then commit" discipline above -- run this on the working tree BEFORE committing an edit to any file a bundle lists as a `sources` entry, so a doc that needs re-stamping is caught before the commit that would otherwise make it stale in CI:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
okf-kit check path/to/bundle --dirty-as-now --strict
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If it reports a doc STALE, re-verify that doc against its current `sources` and bump its `timestamp` in the same commit as the source edit (or its own dedicated commit), THEN commit -- exactly the measure-after-commit discipline described above, just checked before the commit exists rather than after CI runs.
|
|
148
|
+
|
|
149
|
+
**What this actually produces for the recipe above, reproduced through the built CLI (source committed, doc committed with a matching `timestamp`, then):**
|
|
150
|
+
|
|
151
|
+
| Working-tree state | `check --dirty-as-now --strict` |
|
|
152
|
+
|---|---|
|
|
153
|
+
| Source edited, doc re-stamped on disk to now (a few seconds to a minute after writing the stamp) | exit 0, clean -- neither `sources-fresh` nor `sources-fresh-future` fires |
|
|
154
|
+
| Source edited, doc left alone (not re-stamped) | exit 1, `sources-fresh` STALE |
|
|
155
|
+
| Source edit and doc re-stamp committed TOGETHER, flag OFF | exit 0, clean (the committed-history co-commit rescue, unaffected by this flag) |
|
|
156
|
+
| Doc dirty for an unrelated reason (a body edit, `timestamp` value unchanged), source edited | exit 1, `sources-fresh` STALE -- not rescued |
|
|
157
|
+
| Doc re-stamped on disk to a value BEFORE the one committed at `HEAD` (a backwards move), source edited | exit 0, clean -- still counts as a re-stamp (see above) |
|
|
158
|
+
| Source edit committed ALONE, the doc's re-stamp left uncommitted in the working tree | exit 0, clean locally -- but CI is RED (`sources-fresh` STALE) until that re-stamp is committed too |
|
|
159
|
+
|
|
160
|
+
That last row is the one shape where a clean local run does NOT imply a clean CI run, and it is inherent to the model rather than a gap to be fixed: the flag judges the content in YOUR WORKING TREE, while CI judges only what you actually pushed. Commit the source edit without its doc re-stamp and CI sees a source newer than the doc's committed `timestamp`, exactly as it should. The remedy is the one this section already recommends: commit the re-stamp, in the same commit as the source edit or its own.
|
|
161
|
+
|
|
162
|
+
What "matches CI" actually means here: `--dirty-as-now` makes a local run compute the same `sources-fresh`/`sources-fresh-future` VERDICT (fresh, STALE, or not-assessable) that a post-commit `check` run would compute for the same content -- it says nothing about whether a given CI setup turns that verdict into a failed build, and nothing about warning classes outside these two rules. Whether it does depends entirely on how that CI wires up `okf-kit check`: this package's own CI runs it two different ways for the same bundle -- `.github/workflows/okf-staleness.yml` calls `check` warn-only (findings surface in the job summary and as annotations, but the job never fails on them), while `.github/workflows/ci.yml`'s freshness guard step runs `check --require-anchors` and then filters the JSON report through `jq` for `sources-fresh`/`sources-fresh-future` findings, only failing the build on THOSE. `--strict` is broader than either: it fails on every warning-severity finding from EVERY rule (including ones this flag says nothing about, like `citations-resolve`), so a locally-strict, CI-clean bundle is not a contradiction -- it means the CI in question does not block on every warning class `--strict` does. Read the target repo's actual CI wiring before treating `--dirty-as-now --strict` as equivalent to "will this pass CI".
|
|
163
|
+
|
|
164
|
+
`--dirty-as-now` ships in the same `okf-kit` release as everything else in this CHANGELOG entry; a CI step that installs a PINNED `okf-kit` version (as both workflows above do) only gains the flag once that pin is bumped to a release that includes it.
|
|
165
|
+
|
|
166
|
+
**Known limitations, stated rather than hidden.** Ignored files (`.gitignore`) are deliberately NOT treated as dirty here (`git status` is run without `--ignored`): an ignored source keeps its ordinary `untracked by git, staleness unknown` notice rather than becoming newly dirty under this flag. A source living inside a dirty SUBMODULE is likewise not detected as dirty: `git status` reports a submodule with uncommitted changes as one single record for the submodule's own path, not per file inside it, and this flag's path-matching does not currently descend into that report to find the individual dirty files a `sources` entry might name.
|
|
167
|
+
|
|
168
|
+
### Future-dated timestamps (`sources-fresh-future`)
|
|
169
|
+
|
|
170
|
+
`sources-fresh` catches a `timestamp` that is too OLD relative to a source's last commit. `sources-fresh-future` catches the opposite mistake: a `timestamp` that is too NEW relative to the doc file's OWN last commit -- in practice, almost always a local wall-clock time hand-written with a trailing `Z` (or another UTC marker) it does not actually have, rather than a genuine future date. It never looks at `sources` commit times at all, only at the doc file's own git history, so it has nothing to say about whether any source is stale; the two rules are complementary, not overlapping, and are assessed over the same population of docs (a validly-shaped `sources` list and a repo root available, exactly like `sources-fresh` above).
|
|
171
|
+
|
|
172
|
+
| Situation | Severity | Message |
|
|
173
|
+
|-----------|----------|---------|
|
|
174
|
+
| The doc's `timestamp` is later than the doc file's own last commit by more than the skew allowance | warning | `FUTURE-DATED: doc timestamp <iso> is after the doc's own last commit <iso> (skew allowance <n>s)` |
|
|
175
|
+
| The doc's `timestamp` is at or within the skew allowance of the doc file's own last commit | (nothing) | fresh: an ordinary write-then-commit gap, not a mistake |
|
|
176
|
+
| The doc's `timestamp` string has no `Z`/UTC designator or numeric offset (e.g. `2026-01-01T00:00:00`, no offset) | notice | future-dated check skipped: timestamp has no UTC designator (`Z`) or numeric offset, can't be compared reliably across timezones |
|
|
177
|
+
| The doc has no git history yet (uncommitted) | (nothing) | unknown, not flagged: there is no real commit time to compare against -- UNLESS `--dirty-as-now` is passed, in which case a dirty (including untracked) doc gets a real verdict against the shared virtual-commit "now" instant instead (see "Uncommitted edits" below) |
|
|
178
|
+
| The doc's `timestamp` is missing or not a parseable date | (nothing) | left to `sources-fresh`'s own notice, not duplicated here |
|
|
179
|
+
| No repo root available (see auto-detection above) | (nothing) | left to `sources-fresh`'s single bundle-level notice, not duplicated here |
|
|
180
|
+
|
|
181
|
+
The skew allowance defaults to 10 minutes (600 seconds), absorbing the ordinary gap between writing a timestamp and the commit that carries it landing; override it with `--future-skew-minutes <n>`. Like `STALE` findings, `FUTURE-DATED` findings are warnings, advisory by default; run with `--strict` to fail the build on either.
|
|
182
|
+
|
|
183
|
+
**The measure-after-commit discipline both rules enforce:** re-verify the doc against its sources, THEN bump `timestamp` to the real instant of that verification (`new Date().toISOString()` or equivalent, never a hand-written value), THEN commit -- committing without re-stamping is what `sources-fresh` catches, and hand-writing a local time with a `Z` suffix it doesn't have is what `sources-fresh-future` catches.
|
|
184
|
+
|
|
185
|
+
**Why the UTC-designator gate exists:** a bare local datetime string (no `Z`, no numeric offset) parses under `Date.parse` in the machine's OWN timezone, so the same frontmatter value would compare differently on a UTC+2 laptop than on a UTC CI runner -- unusable against a default allowance measured in minutes. A numeric offset (`+02:00`, `-0500`) is unambiguous and IS assessed normally (`Date.parse` already normalizes it to a real UTC instant); only a fully bare datetime is skipped. `sources-fresh`'s own thresholds are days wide, so this ambiguity doesn't practically matter there and the gate applies only to `sources-fresh-future`.
|
|
186
|
+
|
|
187
|
+
**What these two rules catch, precisely, and what they don't:** a source path committed after the doc's `timestamp` (`sources-fresh`'s base case); a source and the doc co-committed together where that commit did NOT re-stamp the doc (`sources-fresh`'s narrowed co-commit exception, see "Staleness (sources-fresh)" above); and a local wall-clock time hand-written with a `Z`/UTC suffix it doesn't actually have (`sources-fresh-future`). What they do NOT catch: a doc-only prose edit that leaves `sources` untouched and the `timestamp` stale -- neither rule has a source-side signal to compare against in that case, so it is not mechanically assessable and stays a reviewer judgment call, the same as before this pair of rules existed.
|
|
188
|
+
|
|
105
189
|
## Citation resolution (citations-resolve)
|
|
106
190
|
|
|
107
191
|
`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.
|
|
@@ -152,6 +236,8 @@ Anchors are checked only once the base checks above (blank-start-line, closing-b
|
|
|
152
236
|
|
|
153
237
|
**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. A heading-section citation (see above) carries no line number and is never a continuation's target: it never sets or clears which citation a later continuation resolves against.
|
|
154
238
|
|
|
239
|
+
**Anchored path-less continuation form: not recognised (bundle-local).** The `orchestrator-workflow` package's own dogfood bundle writes a backtick-wrapped, path-less continuation with a content anchor, e.g. `` `:913-923#"..."` ``, distinct from the two forms above. `CONT_COLON_RE` needs its closing backtick immediately after the digit range, so a trailing `#"anchor"` before the backtick never matches it; `SHORT_FORM_COLON_RE` excludes any match immediately preceded by a backtick and otherwise requires a serial-connective prefix, neither of which this form carries. So this rule sees the form as nothing at all, not merely as unresolved. Recognising it (an opt-in rule, or folding it into this one) would move the cost to okf-kit's maintainers: the rule itself, an allowlist/anchor-geometry contract as public API, and a fleet-wide pin bump on every fix. Declaring it bundle-local instead leaves the cost where it already sits: only the `orchestrator-workflow` package's own `test/docs-consistency.test.ts` guard checks the form, and every other fleet bundle that might someday write it stays unguarded until it grows the same check by hand. Port trigger, restated from the orchestrator-workflow CHANGELOG's "Citation-sibling-drift guard, okf-kit-porting decision" entry: a second fleet bundle observed carrying real sibling-citation drift in a review pass (not merely plausible in the abstract) reopens the port decision. A fleet count at the time of this decision found the live form only in that one bundle (`docs/okf/model-preselection.md`, 4 occurrences); other apparent hits were historical prose in a `log.md` narrating a re-point, not live citations.
|
|
240
|
+
|
|
155
241
|
**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`).
|
|
156
242
|
|
|
157
243
|
**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).
|
|
@@ -227,15 +313,23 @@ A candidate file-mention token that itself fails to resolve, or resolves to more
|
|
|
227
313
|
|
|
228
314
|
## CI usage
|
|
229
315
|
|
|
230
|
-
This is advisory: don't fail the build on warnings unless you pass `--strict`. Use a normal (non-shallow) checkout of the repo that owns the bundle: repo-root detection runs `git rev-parse --show-toplevel` from the `path/to/bundle` argument itself, not from the shell's working directory, and `sources-fresh` reads `git log`,
|
|
316
|
+
This is advisory: don't fail the build on warnings unless you pass `--strict`. Use a normal (non-shallow) checkout of the repo that owns the bundle: repo-root detection runs `git rev-parse --show-toplevel` from the `path/to/bundle` argument itself, not from the shell's working directory, and `sources-fresh` reads `git log`. A shallow clone (`actions/checkout`'s default `fetch-depth: 1`, or any `git clone --depth`) does NOT report paths as untracked -- every path is still tracked at the boundary commit's own commit time. What it DOES cost: `sources-fresh`'s re-stamp check (see "Staleness (sources-fresh)" above) can no longer tell a doc's real root commit from the grafted boundary commit history was cut off at, so a doc whose own last commit lands there gets a `staleness not assessable` notice instead of a real STALE/pass verdict. Pass `fetch-depth: 0` (a full checkout) to get a real verdict there too.
|
|
231
317
|
|
|
232
318
|
```yaml
|
|
319
|
+
- uses: actions/checkout@v5
|
|
320
|
+
with:
|
|
321
|
+
fetch-depth: 0
|
|
233
322
|
- name: OKF bundle check
|
|
234
|
-
run: npx okf-kit@0.
|
|
323
|
+
run: npx okf-kit@0.11.0 check path/to/bundle
|
|
235
324
|
```
|
|
236
325
|
|
|
237
326
|
Pin the version: an unpinned `npx okf-kit` picks up new rules on their release day, which turns an unrelated PR red.
|
|
238
327
|
|
|
328
|
+
Releasing a new okf-kit version to npm must also bump the `npm install -g
|
|
329
|
+
okf-kit@<version>` pins this repo's own `orchestrator-workflow` package
|
|
330
|
+
carries in `.github/workflows/`, in the same release commit; see
|
|
331
|
+
`CONTRIBUTING.md`'s "Releasing okf-kit" section for the order.
|
|
332
|
+
|
|
239
333
|
## Where this fits
|
|
240
334
|
|
|
241
335
|
okf-kit is the producer-side check: it validates a bundle you are authoring or maintaining. Consuming an OKF bundle at query time (loading, indexing, ranking passages for an agent) lives in [codebase-oracle](https://github.com/LanNguyenSi/codebase-oracle), a separate tool.
|
package/dist/bundle.d.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
|
-
import type { BundleContext, RunGit } from "./types.js";
|
|
1
|
+
import type { BundleContext, FrontmatterInfo, RunGit } from "./types.js";
|
|
2
2
|
export declare function loadBundle(bundleDir: string, repoRoot?: string, runGit?: RunGit): BundleContext;
|
|
3
|
+
/**
|
|
4
|
+
* A frontmatter block is the first line being exactly `---` up to the next
|
|
5
|
+
* line that is exactly `---`. Anything else (no opening delimiter, or an
|
|
6
|
+
* opening delimiter with no matching close) counts as no frontmatter block
|
|
7
|
+
* at all, per the OKF v0.1 shape rule.
|
|
8
|
+
*
|
|
9
|
+
* Exported so `sources-fresh` can apply the IDENTICAL parse to a historical
|
|
10
|
+
* blob (`git show <sha>:<path>`) that `loadBundle` applies to the working-tree
|
|
11
|
+
* file: the rule's re-stamp test compares the parsed frontmatter `timestamp`
|
|
12
|
+
* VALUE across a commit boundary, and a second, subtly different parser there
|
|
13
|
+
* would decide freshness by a different notion of "frontmatter" than the rest
|
|
14
|
+
* of the tool.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseFrontmatter(raw: string): {
|
|
17
|
+
frontmatter: FrontmatterInfo;
|
|
18
|
+
body: string;
|
|
19
|
+
};
|
package/dist/bundle.js
CHANGED
|
@@ -39,8 +39,15 @@ function walkMarkdownFiles(dir) {
|
|
|
39
39
|
* line that is exactly `---`. Anything else (no opening delimiter, or an
|
|
40
40
|
* opening delimiter with no matching close) counts as no frontmatter block
|
|
41
41
|
* at all, per the OKF v0.1 shape rule.
|
|
42
|
+
*
|
|
43
|
+
* Exported so `sources-fresh` can apply the IDENTICAL parse to a historical
|
|
44
|
+
* blob (`git show <sha>:<path>`) that `loadBundle` applies to the working-tree
|
|
45
|
+
* file: the rule's re-stamp test compares the parsed frontmatter `timestamp`
|
|
46
|
+
* VALUE across a commit boundary, and a second, subtly different parser there
|
|
47
|
+
* would decide freshness by a different notion of "frontmatter" than the rest
|
|
48
|
+
* of the tool.
|
|
42
49
|
*/
|
|
43
|
-
function parseFrontmatter(raw) {
|
|
50
|
+
export function parseFrontmatter(raw) {
|
|
44
51
|
const lines = raw.split(/\r?\n/);
|
|
45
52
|
if (lines[0] !== "---") {
|
|
46
53
|
return { frontmatter: { present: false }, body: raw };
|
package/dist/bundle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AAQxB,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE3D,MAAM,UAAU,UAAU,CACxB,SAAiB,EACjB,QAAiB,EACjB,MAAe;IAEf,MAAM,KAAK,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAgB,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpD,OAAO;YACL,OAAO;YACP,QAAQ;YACR,UAAU,EAAE,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC5C,GAAG;YACH,WAAW;YACX,IAAI;SACL,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;QACvC,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AAQxB,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC;AAE3D,MAAM,UAAU,UAAU,CACxB,SAAiB,EACjB,QAAiB,EACjB,MAAe;IAEf,MAAM,KAAK,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAgB,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACpD,OAAO;YACL,OAAO;YACP,QAAQ;YACR,UAAU,EAAE,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC5C,GAAG;YACH,WAAW;YACX,IAAI;SACL,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;QACvC,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAI1C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;QACvB,OAAO,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IACxD,CAAC;IACD,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;YACvB,YAAY,GAAG,CAAC,CAAC;YACjB,MAAM;QACR,CAAC;IACH,CAAC;IACD,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;IACxD,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,UAAU,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpE,OAAO,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC;IAC9D,CAAC;AACH,CAAC"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -33,6 +33,25 @@ export interface CheckOptions {
|
|
|
33
33
|
* set.
|
|
34
34
|
*/
|
|
35
35
|
proseLineReferencesStrict?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* `sources-fresh-future`'s clock-skew allowance, in minutes (default 10
|
|
38
|
+
* when omitted; see `DEFAULT_FUTURE_SKEW_SECONDS` in
|
|
39
|
+
* `src/rules/sources-fresh.ts`). Converted to seconds and stored on
|
|
40
|
+
* `ctx.freshnessFutureSkewSeconds`.
|
|
41
|
+
*/
|
|
42
|
+
futureSkewMinutes?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Opt-in for `sources-fresh` AND `sources-fresh-future`: model every
|
|
45
|
+
* uncommitted change (modified, staged, or untracked) as though it
|
|
46
|
+
* landed in ONE virtual commit made right now, instead of at its last
|
|
47
|
+
* real commit. Both a dirty `sources` path and a dirty DOC read their
|
|
48
|
+
* commit time from that same shared instant, so a pre-commit run reports
|
|
49
|
+
* the verdict CI will report once the commit lands -- for either rule.
|
|
50
|
+
* Stored on `ctx.dirtyAsNow`; see its jsdoc in `src/types.ts` (the
|
|
51
|
+
* canonical description of the model) and the README's "Uncommitted
|
|
52
|
+
* edits (`--dirty-as-now`)" section.
|
|
53
|
+
*/
|
|
54
|
+
dirtyAsNow?: boolean;
|
|
36
55
|
/** Test-only override for git access; production code shells out to the real `git` binary. */
|
|
37
56
|
runGit?: RunGit;
|
|
38
57
|
}
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,12 @@ export function runCheck(bundleDir, options = {}) {
|
|
|
33
33
|
strict: Boolean(options.proseLineReferencesStrict),
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
if (options.futureSkewMinutes !== undefined) {
|
|
37
|
+
ctx.freshnessFutureSkewSeconds = Math.round(options.futureSkewMinutes * 60);
|
|
38
|
+
}
|
|
39
|
+
if (options.dirtyAsNow) {
|
|
40
|
+
ctx.dirtyAsNow = true;
|
|
41
|
+
}
|
|
36
42
|
const findings = allRules.flatMap((rule) => rule.run(ctx));
|
|
37
43
|
const summary = summarize(findings);
|
|
38
44
|
const exitCode = summary.errors > 0 || (Boolean(options.strict) && summary.warnings > 0)
|
|
@@ -67,9 +73,26 @@ program
|
|
|
67
73
|
'reference outside citations-resolve\'s own backtick grammar, e.g. "lines 129-132" (opt-in, see README)')
|
|
68
74
|
.option("--prose-line-references-strict", "prose-line-references: also flag every prose line reference, not only a drifted one, with " +
|
|
69
75
|
"the remedy to lift it into a backtick citation or a symbol name (ignored unless --prose-line-references is also passed)")
|
|
76
|
+
.option("--future-skew-minutes <n>", "sources-fresh-future: clock-skew allowance in minutes before a doc `timestamp` later than " +
|
|
77
|
+
"the doc's own last commit is flagged as future-dated (default 10)")
|
|
78
|
+
.option("--dirty-as-now", "sources-fresh + sources-fresh-future: model every uncommitted change (modified, staged, or " +
|
|
79
|
+
"untracked) -- a `sources` path and the doc itself alike -- as one virtual commit made right " +
|
|
80
|
+
"now, so a pre-commit run matches what CI reports after the commit lands (opt-in, see README)")
|
|
70
81
|
.exitOverride()
|
|
71
82
|
.action((bundleDir, opts) => {
|
|
72
83
|
try {
|
|
84
|
+
let futureSkewMinutes;
|
|
85
|
+
if (opts.futureSkewMinutes !== undefined) {
|
|
86
|
+
const trimmed = opts.futureSkewMinutes.trim();
|
|
87
|
+
// `Number("")` and `Number(" ")` both resolve to 0, which would
|
|
88
|
+
// otherwise silently accept an empty/whitespace-only value as
|
|
89
|
+
// "0 minutes" instead of rejecting it as the usage error it is.
|
|
90
|
+
const n = trimmed === "" ? Number.NaN : Number(trimmed);
|
|
91
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
92
|
+
throw new UsageError(`--future-skew-minutes must be a non-negative number, got \`${opts.futureSkewMinutes}\``);
|
|
93
|
+
}
|
|
94
|
+
futureSkewMinutes = n;
|
|
95
|
+
}
|
|
73
96
|
const result = runCheck(bundleDir, {
|
|
74
97
|
repoRoot: opts.repoRoot,
|
|
75
98
|
strict: opts.strict,
|
|
@@ -77,6 +100,8 @@ program
|
|
|
77
100
|
requireAnchorsAllow: opts.requireAnchorsAllow,
|
|
78
101
|
proseLineReferences: opts.proseLineReferences,
|
|
79
102
|
proseLineReferencesStrict: opts.proseLineReferencesStrict,
|
|
103
|
+
futureSkewMinutes,
|
|
104
|
+
dirtyAsNow: opts.dirtyAsNow,
|
|
80
105
|
});
|
|
81
106
|
const output = opts.json
|
|
82
107
|
? renderJson(result.bundleDir, result.findings)
|