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 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` but at/before the doc file's last commit | (nothing) | fresh: doc and source landed together (or the doc was committed later) |
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 any commit touching the doc (a typo fix, a repo-wide formatter run, a rename, which resets the doc's last-commit time because `git log` runs without `--follow`) silences drift on all sources changed before it, even ones nobody re-verified. The frontmatter `timestamp` still governs sources changed after the doc's last commit.
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`, so a shallow clone reports paths as untracked.
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.9.0 check path/to/bundle
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 };
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
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)