okf-kit 0.7.0 → 0.9.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.
@@ -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":"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"}
package/dist/types.d.ts CHANGED
@@ -29,12 +29,49 @@ export interface BundleDoc {
29
29
  * real git process.
30
30
  */
31
31
  export type RunGit = (args: string[], cwd: string) => string | null;
32
+ /**
33
+ * Opt-in options for `citations-resolve`'s stricter anchor checks (see
34
+ * `--require-anchors` in `src/cli.ts` and the "Anchor strictness (opt-in)"
35
+ * doc block in `src/rules/citations-resolve.ts`). Absent entirely when the
36
+ * opt-in was not requested, so a rule can gate its stricter behavior on a
37
+ * single `ctx.requireAnchors` truthiness check without a separate boolean.
38
+ */
39
+ export interface RequireAnchorsOptions {
40
+ /**
41
+ * Glob or exact-match patterns (matched against a citation's raw
42
+ * `citedPath` text, e.g. `"README.md"`), exempting a matching in-repo
43
+ * full citation from the `anchor-required` check.
44
+ */
45
+ allow: string[];
46
+ }
47
+ /**
48
+ * Opt-in options for `prose-line-references` (see `--prose-line-references`
49
+ * in `src/cli.ts` and `src/rules/prose-line-references.ts`). Absent entirely
50
+ * when the opt-in was not requested, matching `RequireAnchorsOptions`'
51
+ * single-truthiness-check discipline: a consumer that never passes
52
+ * `--prose-line-references` gets byte-identical `check` output to before
53
+ * this rule existed.
54
+ */
55
+ export interface ProseLineReferencesOptions {
56
+ /**
57
+ * When true, every prose line reference the extraction grammar finds is
58
+ * flagged (`prose-line-reference-not-anchored`), not only a drifted one --
59
+ * the remedy is always the same: lift it into a backtick `path:N-M`
60
+ * citation, or de-precise it to a symbol name. Ignored (no effect) when
61
+ * `proseLineReferences` itself is absent.
62
+ */
63
+ strict?: boolean;
64
+ }
32
65
  export interface BundleContext {
33
66
  bundleDir: string;
34
67
  repoRoot?: string;
35
68
  docs: BundleDoc[];
36
69
  /** Defaults to a real `git` child-process call (see src/git.ts) when a rule needs it and none was injected. */
37
70
  runGit?: RunGit;
71
+ /** See `RequireAnchorsOptions`. Undefined when `--require-anchors` was not passed. */
72
+ requireAnchors?: RequireAnchorsOptions;
73
+ /** See `ProseLineReferencesOptions`. Undefined when `--prose-line-references` was not passed. */
74
+ proseLineReferences?: ProseLineReferencesOptions;
38
75
  }
39
76
  export interface Rule {
40
77
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okf-kit",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "CLI that validates OKF v0.1 knowledge bundles for structural correctness",
5
5
  "type": "module",
6
6
  "bin": {