okf-kit 0.8.0 → 0.10.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 +291 -0
- package/README.md +85 -11
- 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 +21 -0
- package/dist/cli.js +29 -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/citations-resolve.d.ts +105 -0
- package/dist/rules/citations-resolve.js +154 -17
- package/dist/rules/citations-resolve.js.map +1 -1
- package/dist/rules/index.d.ts +3 -2
- package/dist/rules/index.js +15 -2
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/prose-line-references.d.ts +2 -0
- package/dist/rules/prose-line-references.js +512 -0
- package/dist/rules/prose-line-references.js.map +1 -0
- package/dist/rules/sources-fresh.d.ts +35 -0
- package/dist/rules/sources-fresh.js +398 -18
- package/dist/rules/sources-fresh.js.map +1 -1
- package/dist/types.d.ts +27 -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
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getValidSources } from "../util.js";
|
|
4
|
+
import { CITATION_RE, computeExcludedSpans, computeFencedSpans, computeIndentedCodeSpans, computeParagraphStarts, computeTableRowSpans, hasParentSegment, paragraphStartFor, resolveCitation, } from "./citations-resolve.js";
|
|
5
|
+
const RULE_ID = "prose-line-references";
|
|
6
|
+
/**
|
|
7
|
+
* Opt-in (`--prose-line-references`, see `src/cli.ts`) checker for a prose
|
|
8
|
+
* line reference outside `citations-resolve`'s own `` `path:N` `` grammar:
|
|
9
|
+
* plain English shapes like "lines 496-498" or "generate-codex-config.ts
|
|
10
|
+
* lines 129-132" that name a line number but were never written as a
|
|
11
|
+
* backtick-delimited citation. `citations-resolve`'s `CITATION_RE` requires
|
|
12
|
+
* a literal `:` between the path and the digits, so these are structurally
|
|
13
|
+
* invisible to it -- a doc can be re-verified, re-stamped, and pass `check`
|
|
14
|
+
* with 0 findings while its prose line numbers are drifted, because nothing
|
|
15
|
+
* ever looked at them. See harness task ad66c43f (2026-08-30/31): both
|
|
16
|
+
* review rounds of that OKF sweep found wrong prose line numbers behind a
|
|
17
|
+
* fresh `citations-resolve`-clean stamp, and named the missing mechanical
|
|
18
|
+
* guard as the structural cause.
|
|
19
|
+
*
|
|
20
|
+
* Gated on `ctx.proseLineReferences` being present (see
|
|
21
|
+
* `ProseLineReferencesOptions` in `src/types.ts`), mirroring
|
|
22
|
+
* `citations-resolve`'s own `ctx.requireAnchors` discipline: a consumer
|
|
23
|
+
* that never passes `--prose-line-references` gets byte-identical `check`
|
|
24
|
+
* output to before this rule existed.
|
|
25
|
+
*
|
|
26
|
+
* Extraction grammar (conservative, see the README's "Prose line references
|
|
27
|
+
* (opt-in, `--prose-line-references`)" section for the authoritative
|
|
28
|
+
* writeup): `line N`, `lines N-M`, `lines N-M` with an en-dash/em-dash, and
|
|
29
|
+
* `lines N to M` (`LINE_REF_RE`). Deliberately NOT matched:
|
|
30
|
+
* - `L N` / `L1` -- not observed in the corpus this rule was measured
|
|
31
|
+
* against (see the CHANGELOG entry), and far more ambiguous than
|
|
32
|
+
* `line N` (`L1` already means "review finding 1" in this package's own
|
|
33
|
+
* README authoring convention for `citations-resolve`'s short-form
|
|
34
|
+
* citations).
|
|
35
|
+
* - `<file>:N` outside backticks -- already matched by
|
|
36
|
+
* `citations-resolve`'s own `CITATION_RE`, which has no backtick
|
|
37
|
+
* requirement (only its heading-section form does); duplicating it here
|
|
38
|
+
* would double-report the same drift under two rule ids.
|
|
39
|
+
* - a comma-separated list of several line numbers/ranges after one
|
|
40
|
+
* `lines` keyword (e.g. "lines 178, 234, 265-268") -- only the first
|
|
41
|
+
* number/range immediately after the keyword is extracted; the rest are
|
|
42
|
+
* silently under-extracted rather than mis-parsed. Conservative
|
|
43
|
+
* under-extraction, never mis-binding, matches this rule's "never
|
|
44
|
+
* guess" posture throughout.
|
|
45
|
+
* - a second, unlabelled range chained by "vs"/"and" onto an already-
|
|
46
|
+
* extracted one (e.g. "lines 676-698 vs 564-591" only extracts
|
|
47
|
+
* 676-698) -- same reasoning.
|
|
48
|
+
*
|
|
49
|
+
* Exemptions for the LINE REFERENCE match itself (`line N`/`lines N-M`):
|
|
50
|
+
* fenced code blocks, indented code blocks, inline code spans, and
|
|
51
|
+
* Markdown table rows (`computeExcludedSpans`, reused verbatim from
|
|
52
|
+
* `citations-resolve`), the char span of any real `citations-resolve`
|
|
53
|
+
* full citation (`CITATION_RE`, reused verbatim) -- the latter mostly
|
|
54
|
+
* matters for a citation's own quoted string anchor, e.g.
|
|
55
|
+
* `` `path.md:10-20#"see line 5 above"` ``, whose anchor text could
|
|
56
|
+
* otherwise itself look like a bare prose line reference -- and an HTML
|
|
57
|
+
* comment span (`` <!-- see line 5 above --> ``, `computeHtmlCommentSpans`),
|
|
58
|
+
* which narrates a past state rather than citing current content. A
|
|
59
|
+
* reference is also skipped when its own line is a Markdown ATX heading
|
|
60
|
+
* (`## Line 3 semantics`, `isAtxHeadingAt`): a heading names a section
|
|
61
|
+
* about a line, not a citation to it. A URL with a port (`host:8080`), an
|
|
62
|
+
* ISO timestamp, and a version string (`1.2.3`) need no special-casing:
|
|
63
|
+
* `LINE_REF_RE` requires the literal word `line`/`lines` immediately
|
|
64
|
+
* before the digits, which none of those three shapes contain.
|
|
65
|
+
*
|
|
66
|
+
* A DIFFERENT, narrower exclusion set applies to FILE MENTION detection:
|
|
67
|
+
* fenced code, indented code, and table rows only -- deliberately NOT
|
|
68
|
+
* `computeInlineCodeSpans`. A bare backtick-wrapped filename
|
|
69
|
+
* (`` `src/cli.ts` ``) is the normal, encouraged way to name a file in this
|
|
70
|
+
* package's own prose (see the README's authoring guidance), so excluding
|
|
71
|
+
* inline code from mention detection the way `citations-resolve` excludes
|
|
72
|
+
* it from short-form citation matching would make this rule unable to bind
|
|
73
|
+
* against the overwhelming majority of real file mentions in a bundle.
|
|
74
|
+
*
|
|
75
|
+
* Binding rule (conservative, "never guess across paragraphs"): a bare line
|
|
76
|
+
* reference is bound to a "file mention" -- a path-like token
|
|
77
|
+
* (`FILE_MENTION_RE`, the same extension set `CITATION_RE` recognises) that
|
|
78
|
+
* resolves to a real file under the bundle's repo root via
|
|
79
|
+
* `resolveCitation`, reused verbatim from `citations-resolve` so file
|
|
80
|
+
* resolution follows the identical rules (frontmatter `sources` match,
|
|
81
|
+
* ancestor climb for a bare filename, repo-root-relative, doc-relative,
|
|
82
|
+
* "last full path mentioned", repo-wide basename search) rather than a
|
|
83
|
+
* second, drift-prone copy of that logic:
|
|
84
|
+
* 1. the nearest file mention in the same sentence, preceding first, then
|
|
85
|
+
* following (`findSentenceStart`/`findSentenceEnd` locate the sentence
|
|
86
|
+
* boundary around the reference; see there for the "what counts as a
|
|
87
|
+
* sentence end" heuristic);
|
|
88
|
+
* 2. otherwise the nearest PRECEDING file mention in the same paragraph
|
|
89
|
+
* (`computeParagraphStarts`/`paragraphStartFor`, reused verbatim);
|
|
90
|
+
* 3. otherwise `unresolvable` -- never guessed, never silently bound to
|
|
91
|
+
* the wrong file.
|
|
92
|
+
* A candidate file-mention token that itself fails to resolve (a stray
|
|
93
|
+
* path-like word that is not a real file) or resolves ambiguously (more
|
|
94
|
+
* than one real file shares its basename) is NOT skipped in favor of a
|
|
95
|
+
* farther candidate: "nearest wins" is taken literally, so the outcome is
|
|
96
|
+
* `unresolvable`/`ambiguous` rather than quietly falling through to a
|
|
97
|
+
* second-nearest mention that was not what the prose actually named.
|
|
98
|
+
*
|
|
99
|
+
* Reserved citing docs (`index.md`/`log.md`, `doc.isReserved`) are skipped
|
|
100
|
+
* entirely, the same carve-out `citations-resolve` already gives them: an
|
|
101
|
+
* append-only narrative journal routinely narrates historical line-number
|
|
102
|
+
* deltas as prose about the past, not live citations against current
|
|
103
|
+
* content.
|
|
104
|
+
*
|
|
105
|
+
* Findings, one per extracted reference (`--prose-line-references-strict`
|
|
106
|
+
* can add a second, see below): `unresolvable` and `ambiguous` are
|
|
107
|
+
* `notice`-severity (real false-positive risk: "line" occurs in ordinary
|
|
108
|
+
* English -- "in line with", "product line" -- with no adjacent number
|
|
109
|
+
* often enough that "no file mention nearby" is weaker evidence of drift
|
|
110
|
+
* than a resolved-but-wrong reference); `out-of-bounds` (covers both
|
|
111
|
+
* range-exceeds-file and an inverted range) and `blank-start-line` are
|
|
112
|
+
* `warning`-severity, mirroring `citations-resolve`'s own "a wrong
|
|
113
|
+
* start/target is strong drift evidence" posture once a reference DOES
|
|
114
|
+
* resolve to a single real file.
|
|
115
|
+
*
|
|
116
|
+
* Strict mode (`ProseLineReferencesOptions.strict`, `--prose-line-
|
|
117
|
+
* references-strict`, ignored unless `--prose-line-references` is also
|
|
118
|
+
* passed -- same "ignored unless the base flag is set" discipline
|
|
119
|
+
* `--require-anchors-allow` already uses): flags EVERY extracted reference,
|
|
120
|
+
* regardless of whether it also resolved cleanly, with its own
|
|
121
|
+
* `warning`-severity `prose-line-reference-not-anchored` finding and a
|
|
122
|
+
* fixed remedy: lift it into a backtick `path:N-M` citation (so
|
|
123
|
+
* `citations-resolve` can verify it going forward), or de-precise it to a
|
|
124
|
+
* symbol name. This is intentionally additive, not a replacement for the
|
|
125
|
+
* base checks above: a drifted reference under strict mode gets both its
|
|
126
|
+
* `out-of-bounds`/etc. finding AND the policy finding, since both facts are
|
|
127
|
+
* independently true of it.
|
|
128
|
+
*/
|
|
129
|
+
// Same extension set CITATION_RE recognises -- see the "Extraction
|
|
130
|
+
// grammar" doc block above for why this rule does not invent a second one.
|
|
131
|
+
// The trailing `(?!\w)` matters here in a way it does not for CITATION_RE:
|
|
132
|
+
// CITATION_RE requires a literal `:` immediately after the extension
|
|
133
|
+
// group, which already forces the regex engine to backtrack past a
|
|
134
|
+
// shorter alternative that is a prefix of a longer one (`js` is a prefix
|
|
135
|
+
// of `json`) until the full, correct extension matches. FILE_MENTION_RE
|
|
136
|
+
// has nothing after the extension group to force that backtracking, so
|
|
137
|
+
// without this lookahead a real `foo.json` mention would silently match
|
|
138
|
+
// as `foo.js` (the `on` left dangling) -- `(?!\w)` rejects that truncated
|
|
139
|
+
// match outright, so the engine backtracks to the real extension instead.
|
|
140
|
+
const FILE_MENTION_RE = /[\w./-]+\.(?:ts|js|mjs|md|yml|yaml|json)(?!\w)/g;
|
|
141
|
+
// "line N" / "lines N-M" / "lines N-M" (en-/em-dash) / "lines N to M".
|
|
142
|
+
// Deliberately requires the literal keyword right before the digits -- see
|
|
143
|
+
// the "Extraction grammar" doc block above for every shape this leaves out
|
|
144
|
+
// on purpose. The leading `(?<![\w-])` (rather than a plain `\b`) rejects a
|
|
145
|
+
// match starting right after a hyphen: `\b` alone is a word/non-word
|
|
146
|
+
// transition, and `-` is a non-word character, so `\b` still fires between
|
|
147
|
+
// the `-` and the `l` of "in-line 999" / "multi-line 999" /
|
|
148
|
+
// "command-line 999" -- all compound words meaning "an in-place edit" or
|
|
149
|
+
// "the shell", not a citation to line 999. Node 20 supports lookbehind
|
|
150
|
+
// assertions.
|
|
151
|
+
const LINE_REF_RE = /(?<![\w-])[Ll]ines?\s+(\d+)(?:\s*(?:-|–|—|to)\s*(\d+))?\b/g;
|
|
152
|
+
// An HTML comment span (`<!-- ... -->`), excluded from LINE_REF_RE
|
|
153
|
+
// extraction: a comment narrating a past state ("<!-- see line 5 above
|
|
154
|
+
// for the earlier draft -->") is not a live prose citation any more than
|
|
155
|
+
// a fenced code block's own example is. Scoped to prose-line-references'
|
|
156
|
+
// own extraction only (not folded into citations-resolve's shared
|
|
157
|
+
// `computeExcludedSpans`), since that function's own callers were not
|
|
158
|
+
// asked to change behavior here.
|
|
159
|
+
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
160
|
+
function computeHtmlCommentSpans(content) {
|
|
161
|
+
const spans = [];
|
|
162
|
+
const re = new RegExp(HTML_COMMENT_RE.source, HTML_COMMENT_RE.flags);
|
|
163
|
+
let m;
|
|
164
|
+
while ((m = re.exec(content)) !== null) {
|
|
165
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
166
|
+
}
|
|
167
|
+
return spans;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* True when the line containing `index` is a Markdown ATX heading
|
|
171
|
+
* (`# Heading` through `###### Heading`, up to 3 leading spaces per
|
|
172
|
+
* CommonMark). A heading like "## Line 3 semantics" names a section about
|
|
173
|
+
* line 3, not a citation to it -- excluded from LINE_REF_RE extraction the
|
|
174
|
+
* same way a fenced code block's example is.
|
|
175
|
+
*/
|
|
176
|
+
function isAtxHeadingAt(content, index) {
|
|
177
|
+
const lineStart = content.lastIndexOf("\n", index - 1) + 1;
|
|
178
|
+
let lineEnd = content.indexOf("\n", index);
|
|
179
|
+
if (lineEnd === -1)
|
|
180
|
+
lineEnd = content.length;
|
|
181
|
+
const line = content.slice(lineStart, lineEnd);
|
|
182
|
+
return /^ {0,3}#{1,6}(?:\s|$)/.test(line);
|
|
183
|
+
}
|
|
184
|
+
function isWithinAnySpan(index, spans) {
|
|
185
|
+
return spans.some(([start, end]) => index >= start && index < end);
|
|
186
|
+
}
|
|
187
|
+
/** 1-based line number of `index` within `content`. */
|
|
188
|
+
function lineNumberAt(content, index) {
|
|
189
|
+
let line = 1;
|
|
190
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
191
|
+
if (content[i] === "\n")
|
|
192
|
+
line++;
|
|
193
|
+
}
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
// Duplicated from citations-resolve's own splitLines (not exported: this
|
|
197
|
+
// rule needs the identical "trailing newline does not count as an extra
|
|
198
|
+
// blank final line" semantics so a file's line count agrees with what
|
|
199
|
+
// citations-resolve would report for the same file, but the function
|
|
200
|
+
// itself is a two-line utility not worth threading a new export for).
|
|
201
|
+
function splitLines(content) {
|
|
202
|
+
const lines = content.split("\n");
|
|
203
|
+
if (lines.length > 0 &&
|
|
204
|
+
lines[lines.length - 1] === "" &&
|
|
205
|
+
content.endsWith("\n")) {
|
|
206
|
+
lines.pop();
|
|
207
|
+
}
|
|
208
|
+
return lines;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Every `FILE_MENTION_RE` match in `content` outside `mentionExcludedSpans`
|
|
212
|
+
* (fenced/indented code, table rows -- see the "Exemptions" doc block
|
|
213
|
+
* above for why inline code is deliberately NOT one of these), skipping a
|
|
214
|
+
* leading-`/` (out-of-scope, same as `citations-resolve`'s
|
|
215
|
+
* `citedPath.startsWith("/")`) or `..`-segment (`hasParentSegment`, same as
|
|
216
|
+
* `citations-resolve`'s `path-traversal-rejected`) token outright -- such a
|
|
217
|
+
* token is never a candidate binding target, so it is left out of the
|
|
218
|
+
* mention list entirely rather than being found nearest and then reported
|
|
219
|
+
* unresolvable.
|
|
220
|
+
*/
|
|
221
|
+
function collectFileMentions(content, mentionExcludedSpans) {
|
|
222
|
+
const mentions = [];
|
|
223
|
+
const re = new RegExp(FILE_MENTION_RE.source, FILE_MENTION_RE.flags);
|
|
224
|
+
let m;
|
|
225
|
+
while ((m = re.exec(content)) !== null) {
|
|
226
|
+
if (isWithinAnySpan(m.index, mentionExcludedSpans))
|
|
227
|
+
continue;
|
|
228
|
+
const text = m[0];
|
|
229
|
+
if (text.startsWith("/") || hasParentSegment(text))
|
|
230
|
+
continue;
|
|
231
|
+
mentions.push({ index: m.index, end: m.index + text.length, text });
|
|
232
|
+
}
|
|
233
|
+
return mentions;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* True when `content[i]` is a sentence-ending `.`/`!`/`?` NOT inside
|
|
237
|
+
* `protectedSpans` (fenced/inline code, table rows, or a real citation's
|
|
238
|
+
* own span) and followed, after any run of repeated terminal punctuation,
|
|
239
|
+
* by whitespace and then either the end of the content or a character that
|
|
240
|
+
* plausibly starts a new sentence (uppercase letter, digit, backtick,
|
|
241
|
+
* quote, or open paren/bracket). This is a heuristic, not a real sentence
|
|
242
|
+
* grammar -- it exists only to scope the "same sentence" binding
|
|
243
|
+
* preference (see the "Binding rule" doc block above); getting it wrong in
|
|
244
|
+
* either direction degrades to the paragraph-level fallback, never to a
|
|
245
|
+
* silently wrong bind, since a reference with no in-sentence mention still
|
|
246
|
+
* falls through to `nearestPrecedingMentionInParagraph`. A filename's own
|
|
247
|
+
* internal dot (`generate-codex-config.ts`) never needs a dedicated
|
|
248
|
+
* exemption here: it is never followed by whitespace (the extension
|
|
249
|
+
* letters come right after), so the "followed by whitespace" requirement
|
|
250
|
+
* below already disqualifies it on its own.
|
|
251
|
+
*/
|
|
252
|
+
function isSentenceEnderAt(content, i, protectedSpans) {
|
|
253
|
+
const ch = content[i];
|
|
254
|
+
if (ch !== "." && ch !== "!" && ch !== "?")
|
|
255
|
+
return false;
|
|
256
|
+
if (isWithinAnySpan(i, protectedSpans))
|
|
257
|
+
return false;
|
|
258
|
+
let j = i + 1;
|
|
259
|
+
while (content[j] === "." || content[j] === "!" || content[j] === "?")
|
|
260
|
+
j++;
|
|
261
|
+
const after = content[j];
|
|
262
|
+
if (after !== undefined && !/\s/.test(after))
|
|
263
|
+
return false;
|
|
264
|
+
let k = j;
|
|
265
|
+
while (k < content.length && /\s/.test(content[k]))
|
|
266
|
+
k++;
|
|
267
|
+
const nextChar = content[k];
|
|
268
|
+
if (nextChar === undefined)
|
|
269
|
+
return true;
|
|
270
|
+
return /[A-Z0-9`"'([]/.test(nextChar);
|
|
271
|
+
}
|
|
272
|
+
/** Nearest sentence-ender at or after `paragraphStart` and before `idx`, or `paragraphStart` when none exists. */
|
|
273
|
+
function findSentenceStart(content, idx, paragraphStart, protectedSpans) {
|
|
274
|
+
for (let i = idx - 1; i >= paragraphStart; i--) {
|
|
275
|
+
if (isSentenceEnderAt(content, i, protectedSpans))
|
|
276
|
+
return i + 1;
|
|
277
|
+
}
|
|
278
|
+
return paragraphStart;
|
|
279
|
+
}
|
|
280
|
+
/** Nearest sentence-ender at or after `idx` and before `paragraphEnd`, or `paragraphEnd` when none exists. */
|
|
281
|
+
function findSentenceEnd(content, idx, paragraphEnd, protectedSpans) {
|
|
282
|
+
for (let i = idx; i < paragraphEnd; i++) {
|
|
283
|
+
if (isSentenceEnderAt(content, i, protectedSpans))
|
|
284
|
+
return i + 1;
|
|
285
|
+
}
|
|
286
|
+
return paragraphEnd;
|
|
287
|
+
}
|
|
288
|
+
/** Offset one past the paragraph starting at `paragraphStart` (see computeParagraphStarts), or `contentLength` for the last paragraph. */
|
|
289
|
+
function paragraphEndFor(starts, paragraphStart, contentLength) {
|
|
290
|
+
for (const s of starts) {
|
|
291
|
+
if (s > paragraphStart)
|
|
292
|
+
return s;
|
|
293
|
+
}
|
|
294
|
+
return contentLength;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The binding target for a line reference at `idx`: the nearest file
|
|
298
|
+
* mention in the same sentence (preceding first, then following), or
|
|
299
|
+
* `null` when the sentence has none at all -- see the "Binding rule" doc
|
|
300
|
+
* block above.
|
|
301
|
+
*/
|
|
302
|
+
function nearestMentionInSentence(mentions, idx, sentenceStart, sentenceEnd) {
|
|
303
|
+
let preceding = null;
|
|
304
|
+
for (const mention of mentions) {
|
|
305
|
+
if (mention.index >= sentenceStart && mention.end <= idx) {
|
|
306
|
+
if (!preceding || mention.index > preceding.index)
|
|
307
|
+
preceding = mention;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (preceding)
|
|
311
|
+
return preceding;
|
|
312
|
+
let following = null;
|
|
313
|
+
for (const mention of mentions) {
|
|
314
|
+
if (mention.index >= idx && mention.end <= sentenceEnd) {
|
|
315
|
+
if (!following || mention.index < following.index)
|
|
316
|
+
following = mention;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return following;
|
|
320
|
+
}
|
|
321
|
+
/** Nearest preceding file mention anywhere in the same paragraph -- the fallback once the same-sentence search finds nothing. */
|
|
322
|
+
function nearestPrecedingMentionInParagraph(mentions, idx, paragraphStart) {
|
|
323
|
+
let best = null;
|
|
324
|
+
for (const mention of mentions) {
|
|
325
|
+
if (mention.index >= paragraphStart && mention.end <= idx) {
|
|
326
|
+
if (!best || mention.index > best.index)
|
|
327
|
+
best = mention;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return best;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Base drift checks against an already-resolved single target: an
|
|
334
|
+
* inverted or file-exceeding range, a start line below 1, or a blank
|
|
335
|
+
* start line (`blank-start-line`) -- one problem reported, `out-of-bounds`
|
|
336
|
+
* checked first since a blank-line check on an out-of-range line number
|
|
337
|
+
* is meaningless. A start line below 1 is checked before the file-length
|
|
338
|
+
* comparison and reported as its own `out-of-bounds` message, rather than
|
|
339
|
+
* falling through to `lines[startLine - 1]` (`line 0` would otherwise
|
|
340
|
+
* index `lines[-1]`, i.e. `undefined`, and read as a blank start line
|
|
341
|
+
* instead of the invalid line number it actually is). Returns `null` (no
|
|
342
|
+
* `Problem`, not "unreadable") when the
|
|
343
|
+
* target cannot be read at all; the caller folds that into `unresolvable`
|
|
344
|
+
* (see the "Findings" doc block above: a fifth, `unreadable-target`
|
|
345
|
+
* reason was deliberately not added, to keep to the four reasons this
|
|
346
|
+
* rule's design settled on).
|
|
347
|
+
*/
|
|
348
|
+
function checkTarget(resolvedPath, startLine, endLine) {
|
|
349
|
+
let content;
|
|
350
|
+
try {
|
|
351
|
+
content = fs.readFileSync(resolvedPath, "utf8");
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return "unreadable";
|
|
355
|
+
}
|
|
356
|
+
const lines = splitLines(content);
|
|
357
|
+
const last = endLine ?? startLine;
|
|
358
|
+
if (endLine !== null && endLine < startLine) {
|
|
359
|
+
return {
|
|
360
|
+
reason: "out-of-bounds",
|
|
361
|
+
message: `range end (${endLine}) is before its start (${startLine})`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (startLine < 1) {
|
|
365
|
+
return {
|
|
366
|
+
reason: "out-of-bounds",
|
|
367
|
+
message: `start line (${startLine}) is not a valid line number`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (startLine > lines.length || last > lines.length) {
|
|
371
|
+
return {
|
|
372
|
+
reason: "out-of-bounds",
|
|
373
|
+
message: `citation exceeds file length (${lines.length} line(s))`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const startText = (lines[startLine - 1] ?? "").trim();
|
|
377
|
+
if (startText === "") {
|
|
378
|
+
return { reason: "blank-start-line", message: "start line is blank" };
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
/** Canonical hyphen-ranged rendering, e.g. `lines 2-3` -- distinct from a
|
|
383
|
+
* doc's own matched text, which may use an en-dash, an em-dash, or "to".
|
|
384
|
+
* See `pushFinding` below for why this is kept separate from what gets
|
|
385
|
+
* quoted in a finding's message. */
|
|
386
|
+
function formatRef(startLine, endLine) {
|
|
387
|
+
return endLine !== null
|
|
388
|
+
? `lines ${startLine}-${endLine}`
|
|
389
|
+
: `line ${startLine}`;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* `matchedText` is the doc's own matched text verbatim (`m[0]`, e.g.
|
|
393
|
+
* `lines 2–3` with its original en-dash, or `lines 2` for a comma-separated
|
|
394
|
+
* list like "lines 2, 99, 100-200" where only the first number is
|
|
395
|
+
* extracted -- see the "Extraction grammar" doc block above). Quoting this
|
|
396
|
+
* rather than a re-rendering means a reader can always find the exact text
|
|
397
|
+
* the doc actually contains. `normalizedRef` is `formatRef`'s canonical
|
|
398
|
+
* hyphen-ranged rendering of the same startLine/endLine; it is folded into
|
|
399
|
+
* the message body (not the leading quote) only when it differs from
|
|
400
|
+
* `matchedText`, so the overwhelming common case (a doc already using a
|
|
401
|
+
* plain hyphen) produces the identical message this rule always has.
|
|
402
|
+
*/
|
|
403
|
+
function pushFinding(findings, file, matchedText, normalizedRef, docLine, reason, message, severity, detail) {
|
|
404
|
+
const normalizedNote = matchedText === normalizedRef ? "" : ` (normalized \`${normalizedRef}\`)`;
|
|
405
|
+
findings.push({
|
|
406
|
+
ruleId: RULE_ID,
|
|
407
|
+
severity,
|
|
408
|
+
file,
|
|
409
|
+
message: `\`${matchedText}\`${normalizedNote} (doc line ${docLine}): ${message} [${reason}]`,
|
|
410
|
+
...(detail ? { detail } : {}),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function scanDoc(cache, root, bundleDir, doc, options) {
|
|
414
|
+
if (doc.isReserved)
|
|
415
|
+
return [];
|
|
416
|
+
const findings = [];
|
|
417
|
+
const content = doc.raw;
|
|
418
|
+
const sources = getValidSources(doc.frontmatter.parsed) ?? [];
|
|
419
|
+
const docAbsPath = path.join(bundleDir, doc.relPath);
|
|
420
|
+
const excludedSpans = computeExcludedSpans(content);
|
|
421
|
+
const citationSpans = [];
|
|
422
|
+
const citationRe = new RegExp(CITATION_RE.source, "g");
|
|
423
|
+
let cm;
|
|
424
|
+
while ((cm = citationRe.exec(content)) !== null) {
|
|
425
|
+
citationSpans.push([cm.index, cm.index + cm[0].length]);
|
|
426
|
+
}
|
|
427
|
+
const htmlCommentSpans = computeHtmlCommentSpans(content);
|
|
428
|
+
const protectedSpans = [
|
|
429
|
+
...excludedSpans,
|
|
430
|
+
...citationSpans,
|
|
431
|
+
...htmlCommentSpans,
|
|
432
|
+
];
|
|
433
|
+
// A narrower exclusion set for file-mention detection: fenced/indented
|
|
434
|
+
// code and table rows, but NOT inline code -- see the "Exemptions" doc
|
|
435
|
+
// block above.
|
|
436
|
+
const mentionExcludedSpans = [
|
|
437
|
+
...computeFencedSpans(content),
|
|
438
|
+
...computeIndentedCodeSpans(content),
|
|
439
|
+
...computeTableRowSpans(content),
|
|
440
|
+
];
|
|
441
|
+
const fileMentions = collectFileMentions(content, mentionExcludedSpans);
|
|
442
|
+
const paragraphStarts = computeParagraphStarts(content);
|
|
443
|
+
const re = new RegExp(LINE_REF_RE.source, LINE_REF_RE.flags);
|
|
444
|
+
let m;
|
|
445
|
+
while ((m = re.exec(content)) !== null) {
|
|
446
|
+
if (isWithinAnySpan(m.index, protectedSpans))
|
|
447
|
+
continue;
|
|
448
|
+
if (isAtxHeadingAt(content, m.index))
|
|
449
|
+
continue;
|
|
450
|
+
const startLine = Number(m[1]);
|
|
451
|
+
const endLine = m[2] !== undefined ? Number(m[2]) : null;
|
|
452
|
+
const matchedText = m[0];
|
|
453
|
+
const normalizedRef = formatRef(startLine, endLine);
|
|
454
|
+
const docLine = lineNumberAt(content, m.index);
|
|
455
|
+
const paragraphStart = paragraphStartFor(paragraphStarts, m.index);
|
|
456
|
+
const paragraphEnd = paragraphEndFor(paragraphStarts, paragraphStart, content.length);
|
|
457
|
+
const sentenceStart = findSentenceStart(content, m.index, paragraphStart, protectedSpans);
|
|
458
|
+
const sentenceEnd = findSentenceEnd(content, m.index, paragraphEnd, protectedSpans);
|
|
459
|
+
const mention = nearestMentionInSentence(fileMentions, m.index, sentenceStart, sentenceEnd) ??
|
|
460
|
+
nearestPrecedingMentionInParagraph(fileMentions, m.index, paragraphStart);
|
|
461
|
+
let boundFile = null;
|
|
462
|
+
if (!mention) {
|
|
463
|
+
pushFinding(findings, doc.relPath, matchedText, normalizedRef, docLine, "unresolvable", "no file mention could be bound to this prose line reference", "notice");
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const resolution = resolveCitation(cache, root, docAbsPath, content, sources, mention.text, mention.index);
|
|
467
|
+
if (!resolution || "skip" in resolution) {
|
|
468
|
+
pushFinding(findings, doc.relPath, matchedText, normalizedRef, docLine, "unresolvable", `nearest file mention \`${mention.text}\` does not resolve to a real file`, "notice");
|
|
469
|
+
}
|
|
470
|
+
else if ("ambiguous" in resolution) {
|
|
471
|
+
pushFinding(findings, doc.relPath, matchedText, normalizedRef, docLine, "ambiguous", `file mention \`${mention.text}\` resolves to more than one file, not evaluated`, "notice", `candidates: ${resolution.candidates.join(", ")}`);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
boundFile = path.relative(root, resolution.path);
|
|
475
|
+
const problem = checkTarget(resolution.path, startLine, endLine);
|
|
476
|
+
if (problem === "unreadable") {
|
|
477
|
+
pushFinding(findings, doc.relPath, matchedText, normalizedRef, docLine, "unresolvable", `bound target \`${boundFile}\` exists but could not be read`, "notice");
|
|
478
|
+
}
|
|
479
|
+
else if (problem) {
|
|
480
|
+
pushFinding(findings, doc.relPath, matchedText, normalizedRef, docLine, problem.reason, `bound to \`${boundFile}\`, ${problem.message}`, "warning", `resolvedTo: ${boundFile}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (options.strict) {
|
|
485
|
+
const boundSuffix = boundFile ? ` (bound to \`${boundFile}\`)` : "";
|
|
486
|
+
pushFinding(findings, doc.relPath, matchedText, normalizedRef, docLine, "prose-line-reference-not-anchored", `prose line reference; lift into a backtick anchored citation or de-precise to a symbol name${boundSuffix}`, "warning");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return findings;
|
|
490
|
+
}
|
|
491
|
+
export const proseLineReferencesRule = {
|
|
492
|
+
id: RULE_ID,
|
|
493
|
+
description: "(opt-in, `--prose-line-references`) Prose-embedded line references outside citations-resolve's backtick grammar (`line N`, `lines N-M`, `lines N to M`) are bound to the nearest named file mention (same sentence, then same paragraph) and flagged when the bound reference is out of bounds, blank, or unresolvable/ambiguous. `--prose-line-references-strict` additionally flags every such reference as a formatting policy violation, resolved or not.",
|
|
494
|
+
run(ctx) {
|
|
495
|
+
if (!ctx.proseLineReferences)
|
|
496
|
+
return [];
|
|
497
|
+
if (!ctx.repoRoot) {
|
|
498
|
+
return [
|
|
499
|
+
{
|
|
500
|
+
ruleId: RULE_ID,
|
|
501
|
+
severity: "notice",
|
|
502
|
+
file: "",
|
|
503
|
+
message: "prose line reference resolution skipped: not inside a git work tree",
|
|
504
|
+
},
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
const root = ctx.repoRoot;
|
|
508
|
+
const cache = new Map();
|
|
509
|
+
return ctx.docs.flatMap((doc) => scanDoc(cache, root, ctx.bundleDir, doc, ctx.proseLineReferences));
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
//# sourceMappingURL=prose-line-references.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prose-line-references.js","sourceRoot":"","sources":["../../src/rules/prose-line-references.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,GAEhB,MAAM,wBAAwB,CAAC;AAQhC,MAAM,OAAO,GAAG,uBAAuB,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0HG;AAEH,mEAAmE;AACnE,2EAA2E;AAC3E,2EAA2E;AAC3E,qEAAqE;AACrE,mEAAmE;AACnE,yEAAyE;AACzE,wEAAwE;AACxE,uEAAuE;AACvE,wEAAwE;AACxE,0EAA0E;AAC1E,0EAA0E;AAC1E,MAAM,eAAe,GAAG,iDAAiD,CAAC;AAE1E,uEAAuE;AACvE,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,qEAAqE;AACrE,2EAA2E;AAC3E,4DAA4D;AAC5D,yEAAyE;AACzE,uEAAuE;AACvE,cAAc;AACd,MAAM,WAAW,GACf,4DAA4D,CAAC;AAE/D,mEAAmE;AACnE,uEAAuE;AACvE,yEAAyE;AACzE,yEAAyE;AACzE,kEAAkE;AAClE,sEAAsE;AACtE,iCAAiC;AACjC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAE3C,SAAS,uBAAuB,CAAC,OAAe;IAC9C,MAAM,KAAK,GAA4B,EAAE,CAAC;IAC1C,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;IACrE,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACvC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,OAAe,EAAE,KAAa;IACpD,MAAM,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3C,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAaD,SAAS,eAAe,CACtB,KAAa,EACb,KAA8B;IAE9B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,GAAG,CAAC,CAAC;AACrE,CAAC;AAED,uDAAuD;AACvD,SAAS,YAAY,CAAC,OAAe,EAAE,KAAa;IAClD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrD,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,IAAI,EAAE,CAAC;IAClC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,yEAAyE;AACzE,wEAAwE;AACxE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,SAAS,UAAU,CAAC,OAAe;IACjC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,IACE,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE;QAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EACtB,CAAC;QACD,KAAK,CAAC,GAAG,EAAE,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,mBAAmB,CAC1B,OAAe,EACf,oBAA6C;IAE7C,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;IACrE,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACvC,IAAI,eAAe,CAAC,CAAC,CAAC,KAAK,EAAE,oBAAoB,CAAC;YAAE,SAAS;QAC7D,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,gBAAgB,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,iBAAiB,CACxB,OAAe,EACf,CAAS,EACT,cAAuC;IAEvC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IACzD,IAAI,eAAe,CAAC,CAAC,EAAE,cAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IACrD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG;QAAE,CAAC,EAAE,CAAC;IAC3E,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAAE,CAAC,EAAE,CAAC;IACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC;AAED,kHAAkH;AAClH,SAAS,iBAAiB,CACxB,OAAe,EACf,GAAW,EACX,cAAsB,EACtB,cAAuC;IAEvC,KAAK,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,IAAI,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/C,IAAI,iBAAiB,CAAC,OAAO,EAAE,CAAC,EAAE,cAAc,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,8GAA8G;AAC9G,SAAS,eAAe,CACtB,OAAe,EACf,GAAW,EACX,YAAoB,EACpB,cAAuC;IAEvC,KAAK,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,IAAI,iBAAiB,CAAC,OAAO,EAAE,CAAC,EAAE,cAAc,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,0IAA0I;AAC1I,SAAS,eAAe,CACtB,MAAgB,EAChB,cAAsB,EACtB,aAAqB;IAErB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,cAAc;YAAE,OAAO,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;;;;GAKG;AACH,SAAS,wBAAwB,CAC/B,QAAuB,EACvB,GAAW,EACX,aAAqB,EACrB,WAAmB;IAEnB,IAAI,SAAS,GAAuB,IAAI,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,KAAK,IAAI,aAAa,IAAI,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;YACzD,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK;gBAAE,SAAS,GAAG,OAAO,CAAC;QACzE,CAAC;IACH,CAAC;IACD,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,IAAI,SAAS,GAAuB,IAAI,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,KAAK,IAAI,GAAG,IAAI,OAAO,CAAC,GAAG,IAAI,WAAW,EAAE,CAAC;YACvD,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,KAAK;gBAAE,SAAS,GAAG,OAAO,CAAC;QACzE,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iIAAiI;AACjI,SAAS,kCAAkC,CACzC,QAAuB,EACvB,GAAW,EACX,cAAsB;IAEtB,IAAI,IAAI,GAAuB,IAAI,CAAC;IACpC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,KAAK,IAAI,cAAc,IAAI,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;YAC1D,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK;gBAAE,IAAI,GAAG,OAAO,CAAC;QAC1D,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,WAAW,CAClB,YAAoB,EACpB,SAAiB,EACjB,OAAsB;IAEtB,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,OAAO,IAAI,SAAS,CAAC;IAClC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;QAC5C,OAAO;YACL,MAAM,EAAE,eAAe;YACvB,OAAO,EAAE,cAAc,OAAO,0BAA0B,SAAS,GAAG;SACrE,CAAC;IACJ,CAAC;IACD,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QAClB,OAAO;YACL,MAAM,EAAE,eAAe;YACvB,OAAO,EAAE,eAAe,SAAS,8BAA8B;SAChE,CAAC;IACJ,CAAC;IACD,IAAI,SAAS,GAAG,KAAK,CAAC,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACpD,OAAO;YACL,MAAM,EAAE,eAAe;YACvB,OAAO,EAAE,iCAAiC,KAAK,CAAC,MAAM,WAAW;SAClE,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,IAAI,SAAS,KAAK,EAAE,EAAE,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACxE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;oCAGoC;AACpC,SAAS,SAAS,CAAC,SAAiB,EAAE,OAAsB;IAC1D,OAAO,OAAO,KAAK,IAAI;QACrB,CAAC,CAAC,SAAS,SAAS,IAAI,OAAO,EAAE;QACjC,CAAC,CAAC,QAAQ,SAAS,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,WAAW,CAClB,QAAmB,EACnB,IAAY,EACZ,WAAmB,EACnB,aAAqB,EACrB,OAAe,EACf,MAAc,EACd,OAAe,EACf,QAA8B,EAC9B,MAAe;IAEf,MAAM,cAAc,GAClB,WAAW,KAAK,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,aAAa,KAAK,CAAC;IAC5E,QAAQ,CAAC,IAAI,CAAC;QACZ,MAAM,EAAE,OAAO;QACf,QAAQ;QACR,IAAI;QACJ,OAAO,EAAE,KAAK,WAAW,KAAK,cAAc,cAAc,OAAO,MAAM,OAAO,KAAK,MAAM,GAAG;QAC5F,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9B,CAAC,CAAC;AACL,CAAC;AAED,SAAS,OAAO,CACd,KAAoB,EACpB,IAAY,EACZ,SAAiB,EACjB,GAAc,EACd,OAAmC;IAEnC,IAAI,GAAG,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAE9B,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC;IACxB,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAErD,MAAM,aAAa,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,aAAa,GAA4B,EAAE,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvD,IAAI,EAA0B,CAAC;IAC/B,OAAO,CAAC,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAChD,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,cAAc,GAAG;QACrB,GAAG,aAAa;QAChB,GAAG,aAAa;QAChB,GAAG,gBAAgB;KACpB,CAAC;IAEF,uEAAuE;IACvE,uEAAuE;IACvE,eAAe;IACf,MAAM,oBAAoB,GAA4B;QACpD,GAAG,kBAAkB,CAAC,OAAO,CAAC;QAC9B,GAAG,wBAAwB,CAAC,OAAO,CAAC;QACpC,GAAG,oBAAoB,CAAC,OAAO,CAAC;KACjC,CAAC;IACF,MAAM,YAAY,GAAG,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;IACxE,MAAM,eAAe,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAExD,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;IAC7D,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACvC,IAAI,eAAe,CAAC,CAAC,CAAC,KAAK,EAAE,cAAc,CAAC;YAAE,SAAS;QACvD,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC;YAAE,SAAS;QAE/C,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACzD,MAAM,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,aAAa,GAAG,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAE/C,MAAM,cAAc,GAAG,iBAAiB,CAAC,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QACnE,MAAM,YAAY,GAAG,eAAe,CAClC,eAAe,EACf,cAAc,EACd,OAAO,CAAC,MAAM,CACf,CAAC;QACF,MAAM,aAAa,GAAG,iBAAiB,CACrC,OAAO,EACP,CAAC,CAAC,KAAK,EACP,cAAc,EACd,cAAc,CACf,CAAC;QACF,MAAM,WAAW,GAAG,eAAe,CACjC,OAAO,EACP,CAAC,CAAC,KAAK,EACP,YAAY,EACZ,cAAc,CACf,CAAC;QAEF,MAAM,OAAO,GACX,wBAAwB,CACtB,YAAY,EACZ,CAAC,CAAC,KAAK,EACP,aAAa,EACb,WAAW,CACZ;YACD,kCAAkC,CAAC,YAAY,EAAE,CAAC,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;QAE5E,IAAI,SAAS,GAAkB,IAAI,CAAC;QAEpC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,WAAW,CACT,QAAQ,EACR,GAAG,CAAC,OAAO,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,cAAc,EACd,6DAA6D,EAC7D,QAAQ,CACT,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,UAAU,GAAG,eAAe,CAChC,KAAK,EACL,IAAI,EACJ,UAAU,EACV,OAAO,EACP,OAAO,EACP,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,KAAK,CACd,CAAC;YACF,IAAI,CAAC,UAAU,IAAI,MAAM,IAAI,UAAU,EAAE,CAAC;gBACxC,WAAW,CACT,QAAQ,EACR,GAAG,CAAC,OAAO,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,cAAc,EACd,0BAA0B,OAAO,CAAC,IAAI,oCAAoC,EAC1E,QAAQ,CACT,CAAC;YACJ,CAAC;iBAAM,IAAI,WAAW,IAAI,UAAU,EAAE,CAAC;gBACrC,WAAW,CACT,QAAQ,EACR,GAAG,CAAC,OAAO,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,WAAW,EACX,kBAAkB,OAAO,CAAC,IAAI,kDAAkD,EAChF,QAAQ,EACR,eAAe,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClD,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;gBACjD,MAAM,OAAO,GAAG,WAAW,CAAC,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;gBACjE,IAAI,OAAO,KAAK,YAAY,EAAE,CAAC;oBAC7B,WAAW,CACT,QAAQ,EACR,GAAG,CAAC,OAAO,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,cAAc,EACd,kBAAkB,SAAS,iCAAiC,EAC5D,QAAQ,CACT,CAAC;gBACJ,CAAC;qBAAM,IAAI,OAAO,EAAE,CAAC;oBACnB,WAAW,CACT,QAAQ,EACR,GAAG,CAAC,OAAO,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,OAAO,CAAC,MAAM,EACd,cAAc,SAAS,OAAO,OAAO,CAAC,OAAO,EAAE,EAC/C,SAAS,EACT,eAAe,SAAS,EAAE,CAC3B,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,gBAAgB,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,WAAW,CACT,QAAQ,EACR,GAAG,CAAC,OAAO,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,mCAAmC,EACnC,8FAA8F,WAAW,EAAE,EAC3G,SAAS,CACV,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAS;IAC3C,EAAE,EAAE,OAAO;IACX,WAAW,EACT,+bAA+b;IACjc,GAAG,CAAC,GAAG;QACL,IAAI,CAAC,GAAG,CAAC,mBAAmB;YAAE,OAAO,EAAE,CAAC;QACxC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAClB,OAAO;gBACL;oBACE,MAAM,EAAE,OAAO;oBACf,QAAQ,EAAE,QAAQ;oBAClB,IAAI,EAAE,EAAE;oBACR,OAAO,EACL,qEAAqE;iBACxE;aACF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC1B,MAAM,KAAK,GAAkB,IAAI,GAAG,EAAE,CAAC;QACvC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAC9B,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,CAAC,mBAAoB,CAAC,CACnE,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -1,2 +1,37 @@
|
|
|
1
1
|
import type { Rule } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default clock-skew allowance (seconds) for `sources-fresh-future`: a doc
|
|
4
|
+
* timestamp up to this far after the doc's own last commit is still treated
|
|
5
|
+
* as fresh, absorbing the ordinary gap between "author wrote the timestamp"
|
|
6
|
+
* and "the commit that carries it landed". Override via
|
|
7
|
+
* `ctx.freshnessFutureSkewSeconds` (CLI: `--future-skew-minutes`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_FUTURE_SKEW_SECONDS = 600;
|
|
2
10
|
export declare const sourcesFreshRule: Rule;
|
|
11
|
+
/**
|
|
12
|
+
* Complements `sources-fresh`'s "too old" check with the opposite direction:
|
|
13
|
+
* a doc `timestamp` that is later than the doc file's OWN last commit (past
|
|
14
|
+
* a small clock-skew allowance) is almost always a mistake, not a real
|
|
15
|
+
* future date -- typically a local wall-clock time hand-written with a
|
|
16
|
+
* trailing `Z`/UTC suffix it does not actually have. Unlike `sources-fresh`,
|
|
17
|
+
* this check never looks at `sources` commit times at all: it only compares
|
|
18
|
+
* the doc's own `timestamp` against the doc file's own git history, so it
|
|
19
|
+
* has nothing to say about whether any source is stale.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately assessed for the SAME population as `sources-fresh` (docs
|
|
22
|
+
* with a validly-shaped `sources` list and a repo root available): a
|
|
23
|
+
* `timestamp` only has "last verified against sources" semantics for a doc
|
|
24
|
+
* that declares `sources` (see the package README's authoring guidance), so
|
|
25
|
+
* a sourceless doc is out of scope for both freshness rules, not just this
|
|
26
|
+
* one. It shares `sources-fresh`'s "staleness unknown" posture for the two
|
|
27
|
+
* cases that make a real answer impossible: no repo root (silently defers
|
|
28
|
+
* to the single bundle-level notice `sources-fresh` already emits above,
|
|
29
|
+
* rather than duplicating it) and no valid `timestamp` (`sources-fresh`
|
|
30
|
+
* already reports that per-doc notice, so this rule silently skips such a
|
|
31
|
+
* doc rather than reporting it twice). An uncommitted doc (no own commit
|
|
32
|
+
* yet) is likewise "unknown, not flagged": there is no real commit time to
|
|
33
|
+
* compare the timestamp against, and flagging every hand-authored,
|
|
34
|
+
* not-yet-committed doc as "future-dated" would be a false positive on
|
|
35
|
+
* every fresh draft.
|
|
36
|
+
*/
|
|
37
|
+
export declare const sourcesFreshFutureRule: Rule;
|