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.
@@ -1,16 +1,24 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { parseFrontmatter } from "../bundle.js";
3
4
  import { runGit as defaultRunGit } from "../git.js";
4
- import { getTimestampEpoch, getValidSources } from "../util.js";
5
+ import { getRawTimestampString, getTimestampEpoch, getTimestampIdentity, getValidSources, hasUtcDesignator, } from "../util.js";
5
6
  const RULE_ID = "sources-fresh";
7
+ const FUTURE_RULE_ID = "sources-fresh-future";
8
+ /**
9
+ * Default clock-skew allowance (seconds) for `sources-fresh-future`: a doc
10
+ * timestamp up to this far after the doc's own last commit is still treated
11
+ * as fresh, absorbing the ordinary gap between "author wrote the timestamp"
12
+ * and "the commit that carries it landed". Override via
13
+ * `ctx.freshnessFutureSkewSeconds` (CLI: `--future-skew-minutes`).
14
+ */
15
+ export const DEFAULT_FUTURE_SKEW_SECONDS = 600;
6
16
  export const sourcesFreshRule = {
7
17
  id: RULE_ID,
8
- description: "Frontmatter `sources` paths must not have a last-commit time newer than the doc's `timestamp` and the doc file's own last commit.",
18
+ description: "Frontmatter `sources` paths must not have a last-commit time newer than the doc's `timestamp`, unless the doc's own last commit lands at/after the source's and that same commit actually re-stamped the doc: the doc's parsed frontmatter `timestamp` VALUE at that commit differs from its value in the commit's first parent (rename-aware; creating the doc counts as re-stamping it). Creation is trusted only in a genuinely unshallow repository: in a shallow clone, a commit with no parents can simply be where history was cut off, not a real root commit, so it gets the same `not assessable` notice instead of being assumed created. When git cannot answer the question at all, the doc gets a `not assessable` notice instead of either a STALE warning or a silent pass.",
9
19
  run(ctx) {
10
20
  const findings = [];
11
- const docsWithSources = ctx.docs
12
- .map((doc) => ({ doc, sources: getValidSources(doc.frontmatter.parsed) }))
13
- .filter((entry) => entry.sources !== undefined);
21
+ const docsWithSources = getDocsWithSources(ctx);
14
22
  if (docsWithSources.length === 0)
15
23
  return findings;
16
24
  if (!ctx.repoRoot) {
@@ -18,6 +26,9 @@ export const sourcesFreshRule = {
18
26
  // either (sources-shape), so staleness truly was not assessed, not
19
27
  // "everything looked fine". One notice for the whole bundle, not one
20
28
  // per doc: this is a bundle-level condition, not a per-doc finding.
29
+ // sources-fresh-future shares this same population and posture; it
30
+ // relies on this single notice too instead of emitting its own (see
31
+ // that rule below).
21
32
  findings.push({
22
33
  ruleId: RULE_ID,
23
34
  severity: "notice",
@@ -51,15 +62,36 @@ export const sourcesFreshRule = {
51
62
  continue;
52
63
  }
53
64
  // A doc whose own last commit is at or after the source's last commit
54
- // (typically: both landed in one squash-merge) is not stale, even if
55
- // its frontmatter timestamp is old. Lazy + memoized: the lookup costs a
56
- // git process per doc but is only ever consulted on the stale path.
57
- const repoRelDocPath = path
58
- .relative(repoRoot, path.join(ctx.bundleDir, doc.relPath))
59
- .split(path.sep)
60
- .join("/");
61
- let docCommitEpochMemo;
62
- const docCommitEpochFor = () => (docCommitEpochMemo ??= getLastCommitEpoch(git, repoRoot, repoRelDocPath));
65
+ // (typically: both landed in one squash-merge), AND that same commit
66
+ // actually re-stamped the doc, is not stale, even if its frontmatter
67
+ // timestamp predates the merge -- see restampedByOwnLastCommit below
68
+ // for what "re-stamped" means and why the plain commit-ordering check
69
+ // alone is not enough. Both lookups are lazy + memoized: they are only
70
+ // ever consulted on the stale path, and the epoch lookup is shared
71
+ // with sources-fresh-future via getDocCommitEpochShared so the two
72
+ // rules don't each spawn their own `git log` for the same doc in one
73
+ // `check` run.
74
+ //
75
+ // GIT PROCESS BUDGET, per doc, per `check` run: 1 (the shared
76
+ // `git log -1 --format=%ct` epoch lookup, always) + at most 4 more on
77
+ // the re-stamp path (`git log -1 --format=%H%n%P`, then `git diff-tree`,
78
+ // then two `git show`s), i.e. AT MOST 5 -- regardless of how many
79
+ // sources the doc declares, since both lookups are memoized per doc.
80
+ // A doc created by its last commit (or by the repo's root commit)
81
+ // stops after 2 (or 1). Plus one `git log` per UNIQUE source path
82
+ // across the whole bundle (commitEpochCache above). Pinned by
83
+ // "spends at most five git processes per doc" in
84
+ // test/sources-fresh.test.ts. PLUS: at most 1 `git rev-parse
85
+ // --is-shallow-repository` for the ENTIRE run, not per doc (see
86
+ // isShallowRepoShared below) -- only spent at all when some doc's
87
+ // re-stamp lookup actually reaches a root commit.
88
+ const repoRelDocPath = toRepoRelDocPath(repoRoot, ctx.bundleDir, doc);
89
+ const docCommitEpochFor = () => getDocCommitEpochShared(ctx, git, repoRoot, repoRelDocPath);
90
+ let restampMemo;
91
+ const restampFor = () => (restampMemo ??= restampedByOwnLastCommit(git, repoRoot, repoRelDocPath, () => isShallowRepoShared(ctx, git, repoRoot)));
92
+ // At most one "not assessable" notice per doc, however many of its
93
+ // sources hit the unanswerable re-stamp question.
94
+ let notAssessableReported = false;
63
95
  for (const source of sources) {
64
96
  // A missing path on disk is sources-shape's job to report; avoid a
65
97
  // duplicate/confusing finding here.
@@ -76,13 +108,35 @@ export const sourcesFreshRule = {
76
108
  continue;
77
109
  }
78
110
  let isStale = commitEpoch > timestampEpoch;
79
- // Doc committed at/after the source: not stale (see comment above).
80
- // A doc without git history (null epoch, e.g. uncommitted) keeps the
81
- // frontmatter-only comparison.
111
+ // Doc committed at/after the source, AND that commit actually
112
+ // re-stamped it: not stale (see comment above). A doc without git
113
+ // history (null epoch, e.g. uncommitted) keeps the frontmatter-only
114
+ // comparison. When git cannot answer the re-stamp question at all,
115
+ // the doc is reported as not assessable rather than being guessed
116
+ // either way: calling it STALE would turn a git hiccup into a red
117
+ // build, and calling it fresh would be a silent pass.
82
118
  if (isStale) {
83
119
  const docCommitEpoch = docCommitEpochFor();
84
120
  if (docCommitEpoch !== null && docCommitEpoch >= commitEpoch) {
85
- isStale = false;
121
+ const verdict = restampFor();
122
+ if (verdict === "restamped") {
123
+ isStale = false;
124
+ }
125
+ else if (verdict === "unknown" ||
126
+ verdict === "unknown-shallow-root") {
127
+ if (!notAssessableReported) {
128
+ notAssessableReported = true;
129
+ findings.push({
130
+ ruleId: RULE_ID,
131
+ severity: "notice",
132
+ file: doc.relPath,
133
+ message: verdict === "unknown-shallow-root"
134
+ ? "staleness not assessable: this is a shallow clone (`git clone --depth`), so git cannot tell whether the doc's earliest available commit really created it or is just where history was cut off -- use `fetch-depth: 0` (or an unshallow checkout) to assess it"
135
+ : "staleness not assessable: git could not read the doc's own last commit to decide whether it re-stamped the doc",
136
+ });
137
+ }
138
+ continue;
139
+ }
86
140
  }
87
141
  }
88
142
  if (isStale) {
@@ -98,6 +152,102 @@ export const sourcesFreshRule = {
98
152
  return findings;
99
153
  },
100
154
  };
155
+ /**
156
+ * Complements `sources-fresh`'s "too old" check with the opposite direction:
157
+ * a doc `timestamp` that is later than the doc file's OWN last commit (past
158
+ * a small clock-skew allowance) is almost always a mistake, not a real
159
+ * future date -- typically a local wall-clock time hand-written with a
160
+ * trailing `Z`/UTC suffix it does not actually have. Unlike `sources-fresh`,
161
+ * this check never looks at `sources` commit times at all: it only compares
162
+ * the doc's own `timestamp` against the doc file's own git history, so it
163
+ * has nothing to say about whether any source is stale.
164
+ *
165
+ * Deliberately assessed for the SAME population as `sources-fresh` (docs
166
+ * with a validly-shaped `sources` list and a repo root available): a
167
+ * `timestamp` only has "last verified against sources" semantics for a doc
168
+ * that declares `sources` (see the package README's authoring guidance), so
169
+ * a sourceless doc is out of scope for both freshness rules, not just this
170
+ * one. It shares `sources-fresh`'s "staleness unknown" posture for the two
171
+ * cases that make a real answer impossible: no repo root (silently defers
172
+ * to the single bundle-level notice `sources-fresh` already emits above,
173
+ * rather than duplicating it) and no valid `timestamp` (`sources-fresh`
174
+ * already reports that per-doc notice, so this rule silently skips such a
175
+ * doc rather than reporting it twice). An uncommitted doc (no own commit
176
+ * yet) is likewise "unknown, not flagged": there is no real commit time to
177
+ * compare the timestamp against, and flagging every hand-authored,
178
+ * not-yet-committed doc as "future-dated" would be a false positive on
179
+ * every fresh draft.
180
+ */
181
+ export const sourcesFreshFutureRule = {
182
+ id: FUTURE_RULE_ID,
183
+ description: "A doc's frontmatter `timestamp` must not be later than the doc file's own last commit time by more than a clock-skew allowance (default 10 minutes, `--future-skew-minutes`); catches a local time mistakenly written with a `Z`/UTC suffix. Skipped (notice) for a timestamp with no explicit UTC designator (`Z`) or numeric offset, since that parses in the local timezone and cannot be compared reliably against a minutes-wide allowance. Assessed for the same docs as `sources-fresh` (a `sources` list and a repo root); see the README's \"Staleness (sources-fresh)\" section for how the two rules relate.",
184
+ run(ctx) {
185
+ const findings = [];
186
+ const docsWithSources = getDocsWithSources(ctx);
187
+ if (docsWithSources.length === 0)
188
+ return findings;
189
+ if (!ctx.repoRoot)
190
+ return findings;
191
+ const repoRoot = ctx.repoRoot;
192
+ const git = ctx.runGit ?? defaultRunGit;
193
+ const skewSeconds = ctx.freshnessFutureSkewSeconds ?? DEFAULT_FUTURE_SKEW_SECONDS;
194
+ for (const { doc } of docsWithSources) {
195
+ const timestampEpoch = getTimestampEpoch(doc.frontmatter.parsed);
196
+ if (timestampEpoch === undefined)
197
+ continue;
198
+ // A string timestamp with no `Z`/numeric-offset suffix parses in the
199
+ // machine's local timezone (`Date.parse`), which would make this
200
+ // check's verdict swing by hours between machines against a
201
+ // minutes-wide allowance -- not usable. A native `Date` frontmatter
202
+ // value (see getTimestampEpoch's doc comment) carries no such
203
+ // ambiguity and always passes this gate. sources-fresh's own
204
+ // thresholds are in days, wide enough that this ambiguity doesn't
205
+ // practically matter there, so this gate is deliberately NOT applied
206
+ // to that rule.
207
+ const rawTimestamp = getRawTimestampString(doc.frontmatter.parsed);
208
+ if (rawTimestamp !== undefined && !hasUtcDesignator(rawTimestamp)) {
209
+ findings.push({
210
+ ruleId: FUTURE_RULE_ID,
211
+ severity: "notice",
212
+ file: doc.relPath,
213
+ message: "future-dated check skipped: timestamp has no UTC designator (`Z`) or numeric offset, can't be compared reliably across timezones",
214
+ });
215
+ continue;
216
+ }
217
+ const repoRelDocPath = toRepoRelDocPath(repoRoot, ctx.bundleDir, doc);
218
+ const docCommitEpoch = getDocCommitEpochShared(ctx, git, repoRoot, repoRelDocPath);
219
+ if (docCommitEpoch === null)
220
+ continue;
221
+ if (timestampEpoch > docCommitEpoch + skewSeconds) {
222
+ findings.push({
223
+ ruleId: FUTURE_RULE_ID,
224
+ severity: "warning",
225
+ file: doc.relPath,
226
+ message: `FUTURE-DATED: doc timestamp ${epochToIso(timestampEpoch)} is after the doc's own last commit ${epochToIso(docCommitEpoch)} (skew allowance ${skewSeconds}s)`,
227
+ });
228
+ }
229
+ }
230
+ return findings;
231
+ },
232
+ };
233
+ /**
234
+ * Docs carrying a validly-shaped frontmatter `sources` list (see
235
+ * `getValidSources`), each paired with that list. Shared by both rules in
236
+ * this file: `sources-fresh` and `sources-fresh-future` assess the same
237
+ * doc population, just in opposite time directions.
238
+ */
239
+ function getDocsWithSources(ctx) {
240
+ return ctx.docs
241
+ .map((doc) => ({ doc, sources: getValidSources(doc.frontmatter.parsed) }))
242
+ .filter((entry) => entry.sources !== undefined);
243
+ }
244
+ /** `doc`'s own path, relative to `repoRoot`, forward-slash separated -- the pathspec `git log` needs. */
245
+ function toRepoRelDocPath(repoRoot, bundleDir, doc) {
246
+ return path
247
+ .relative(repoRoot, path.join(bundleDir, doc.relPath))
248
+ .split(path.sep)
249
+ .join("/");
250
+ }
101
251
  /**
102
252
  * Last-commit epoch (seconds) for `source` relative to `repoRoot`, or null
103
253
  * when the path has no git history (untracked) or the git call itself
@@ -105,6 +255,10 @@ export const sourcesFreshRule = {
105
255
  * empty stdout, which is exactly the "untracked" case, distinct from a real
106
256
  * git failure (which RunGit also reports as null): both collapse to null
107
257
  * here because sources-fresh treats them the same way, "staleness unknown".
258
+ * Uses committer time (`%ct`), not author time (`%at`): a rebase or
259
+ * cherry-pick can carry a stale author date forward while the committer
260
+ * date reflects when the content actually landed on this branch, which is
261
+ * what both freshness rules care about.
108
262
  */
109
263
  function getLastCommitEpoch(git, repoRoot, source) {
110
264
  const out = git(["log", "-1", "--format=%ct", "--", source], repoRoot);
@@ -116,4 +270,230 @@ function getLastCommitEpoch(git, repoRoot, source) {
116
270
  function epochToIso(epochSeconds) {
117
271
  return new Date(epochSeconds * 1000).toISOString();
118
272
  }
273
+ /**
274
+ * Per-run cache of a doc's own last-commit epoch, keyed by the
275
+ * `BundleContext` instance so `sources-fresh`'s doc-commit comparison and
276
+ * `sources-fresh-future`'s timestamp comparison -- both need the IDENTICAL
277
+ * (repoRoot, doc path) `getLastCommitEpoch` lookup for every doc in one
278
+ * `check` invocation -- share one `git log` process per doc instead of each
279
+ * rule spawning its own. Safe to key on the context object itself: a fresh
280
+ * `BundleContext` is built per `runCheck`/`loadBundle` call, so nothing
281
+ * reuses a stale cache entry across invocations, and the WeakMap lets the
282
+ * cache be garbage-collected with the context once a run is done.
283
+ */
284
+ const docCommitEpochCache = new WeakMap();
285
+ function getDocCommitEpochShared(ctx, git, repoRoot, repoRelDocPath) {
286
+ let cache = docCommitEpochCache.get(ctx);
287
+ if (!cache) {
288
+ cache = new Map();
289
+ docCommitEpochCache.set(ctx, cache);
290
+ }
291
+ const cached = cache.get(repoRelDocPath);
292
+ if (cached !== undefined)
293
+ return cached;
294
+ const epoch = getLastCommitEpoch(git, repoRoot, repoRelDocPath);
295
+ cache.set(repoRelDocPath, epoch);
296
+ return epoch;
297
+ }
298
+ /**
299
+ * Per-run cache (keyed by `BundleContext`, same pattern as
300
+ * `docCommitEpochCache` above) of whether `repoRoot` is a shallow clone
301
+ * (`git clone --depth <n>`), so `restampedByOwnLastCommit`'s root-commit
302
+ * shortcut spends at most ONE `git rev-parse --is-shallow-repository` for
303
+ * the whole `check` run, not one per doc. A failed git call (repoRoot
304
+ * somehow not a real git work tree after all) is treated as shallow: this
305
+ * function's only consumer only ever asks it to decide whether a commit
306
+ * with an empty parent list is trustworthy as a genuine root commit, and
307
+ * the file's standing rule is to answer "unknown" rather than invent
308
+ * either verdict when git itself cannot be asked -- so a failure here must
309
+ * NOT fall back to the previous "always trust it" behavior.
310
+ */
311
+ const shallowRepoCache = new WeakMap();
312
+ function isShallowRepoShared(ctx, git, repoRoot) {
313
+ const cached = shallowRepoCache.get(ctx);
314
+ if (cached !== undefined)
315
+ return cached;
316
+ const out = git(["rev-parse", "--is-shallow-repository"], repoRoot);
317
+ const isShallow = out === null ? true : out.trim() === "true";
318
+ shallowRepoCache.set(ctx, isShallow);
319
+ return isShallow;
320
+ }
321
+ /**
322
+ * Whether `doc`'s own last commit actually re-stamped it, decided by
323
+ * comparing the doc's PARSED FRONTMATTER `timestamp` VALUE at that commit
324
+ * against its value in the commit's FIRST PARENT. A doc created by that
325
+ * commit (or by a genuine root commit of an unshallow repository) counts as
326
+ * re-stamped: its stamp arrived with it. In a SHALLOW clone (`git clone
327
+ * --depth`), the doc's last commit can have an EMPTY parent list purely
328
+ * because that is where history was grafted off, not because it is really
329
+ * the repo's first commit -- `isShallowRepo` (checked lazily, only when a
330
+ * commit with no parents is actually seen) distinguishes the two, so a
331
+ * shallow checkout gets `unknown-shallow-root` there instead of an assumed
332
+ * `restamped`.
333
+ *
334
+ * This is what narrows `sources-fresh`'s co-commit staleness exception: a
335
+ * commit that merely happens to also touch the doc file (a typo fix, a
336
+ * repo-wide formatter run, a rename) without changing the stamp carries no
337
+ * verification claim and must NOT suppress staleness; only a commit that
338
+ * actually rewrote the stamp (or created the doc) does.
339
+ *
340
+ * WHY VALUES AND NOT DIFF TEXT. An earlier version of this check scanned
341
+ * `git log -1 -p -- <doc>` for a `^\+timestamp:` line. Scanning diff TEXT is
342
+ * wrong in three distinct, independently reachable ways, each of which this
343
+ * value comparison closes structurally rather than by another special case:
344
+ *
345
+ * 1. A fenced YAML EXAMPLE in the doc's BODY can contain an unindented
346
+ * `timestamp:` line. Added in a commit that also changed a source, that
347
+ * body line reads as a re-stamp and silently suppresses staleness --
348
+ * exactly the review class this rule exists to close. Parsing
349
+ * frontmatter cannot see a body line at all.
350
+ * 2. A RENAME (`git mv`) shows as `new file mode` under a single-path
351
+ * `git log -p`, firing the "created counts as stamped" branch even
352
+ * though the stamp never moved. The rename-aware lookup below reads the
353
+ * doc's real previous path instead.
354
+ * 3. A MERGE commit prints NO patch at all under `git log -p` (git's
355
+ * default combined-diff suppression), so a genuine re-stamp landing on
356
+ * a `refs/pull/N/merge` ref -- the ref CI actually checks out -- became
357
+ * an invisible false-positive STALE. Both trees are perfectly readable
358
+ * via `git show`, merge or not.
359
+ *
360
+ * Its limits, stated rather than hidden: this answers "did the value
361
+ * change", never "is the new value right". A hand-typed or backdated stamp
362
+ * still counts as a re-stamp (`sources-fresh-future` is the rule that
363
+ * catches an implausible value), and comparing against only the FIRST parent
364
+ * means a merge that takes its doc content wholesale from the second parent
365
+ * is judged against the first-parent baseline, which is the same baseline
366
+ * the PR under review is measured against.
367
+ *
368
+ * Spends at most 4 git processes and returns early before most of them: 1
369
+ * for the commit + parents, 1 for the rename-aware name-status lookup
370
+ * (skipped for a root commit), and 2 blob reads (skipped when the doc was
371
+ * created there). See the GIT PROCESS BUDGET comment in the rule above.
372
+ */
373
+ function restampedByOwnLastCommit(git, repoRoot, repoRelDocPath, isShallowRepo) {
374
+ // %H then %P on its own line: the doc's last commit and its parent list in
375
+ // ONE process. Default history simplification is exactly what this needs
376
+ // for a single path: a merge whose result for that path differs from every
377
+ // parent (a conflict resolution, or a clean auto-merge of two sides that
378
+ // both touched the doc) IS returned here, while a merge that is TREESAME
379
+ // to a parent resolves to the real content-changing commit on that side.
380
+ const head = git(["log", "-1", "--format=%H%n%P", "--", repoRelDocPath], repoRoot);
381
+ if (head === null)
382
+ return "unknown";
383
+ const [sha, parentLine] = head.split("\n");
384
+ if (!sha)
385
+ return "unknown";
386
+ const parents = (parentLine ?? "").split(" ").filter((p) => p !== "");
387
+ // An empty parent list means "the doc arrived with the repo's first
388
+ // commit, stamp and all" ONLY in a genuinely unshallow repository. In a
389
+ // shallow clone (`git clone --depth`), the boundary commit git grafted
390
+ // the history onto also reports an empty parent list for every path
391
+ // touched at or before it -- indistinguishable from a real root commit
392
+ // by this lookup alone -- so trusting it there would let the shallow-clone
393
+ // shortcut fire unconditionally and silently suppress every doc's
394
+ // staleness. isShallowRepo() is checked here, not unconditionally at the
395
+ // top of this function, so an unshallow repo never pays for it.
396
+ if (parents.length === 0) {
397
+ return isShallowRepo() ? "unknown-shallow-root" : "restamped";
398
+ }
399
+ const firstParent = parents[0];
400
+ const previous = previousPathIn(git, repoRoot, firstParent, sha, repoRelDocPath);
401
+ if (previous.kind === "unknown")
402
+ return "unknown";
403
+ if (previous.kind === "created")
404
+ return "restamped";
405
+ const current = git(["show", `${sha}:${repoRelDocPath}`], repoRoot);
406
+ if (current === null)
407
+ return "unknown";
408
+ const before = git(["show", `${firstParent}:${previous.path}`], repoRoot);
409
+ if (before === null)
410
+ return "unknown";
411
+ const currentStamp = getTimestampIdentity(parseFrontmatter(current).frontmatter.parsed);
412
+ const beforeStamp = getTimestampIdentity(parseFrontmatter(before).frontmatter.parsed);
413
+ // Both undefined (no parseable stamp on either side) compares equal, i.e.
414
+ // "not re-stamped" -- nothing was rewritten, so nothing is claimed.
415
+ return currentStamp !== beforeStamp ? "restamped" : "not-restamped";
416
+ }
417
+ /**
418
+ * Where the doc lived in `parentSha`, so its previous revision can be read
419
+ * by that path: the same path (`same`), a different one it was renamed from
420
+ * (`renamed`), or nowhere at all because that commit created it (`created`).
421
+ *
422
+ * Runs `git diff-tree` WITHOUT a pathspec on purpose. A pathspec is applied
423
+ * BEFORE rename detection, so `git diff-tree -M --name-status <parent> <sha>
424
+ * -- <doc>` reports a renamed doc as `A` (verified against a real `git mv`
425
+ * fixture) -- which is precisely the false "created" verdict that made a
426
+ * rename suppress staleness. The unfiltered output is still bounded and
427
+ * small: `--name-status -r` prints one line per changed path, not any file
428
+ * content, and the raised RunGit output cap (see src/git.ts) covers even a
429
+ * repo-wide formatting commit.
430
+ *
431
+ * A doc that does not appear in the diff at all resolves to `same`: the two
432
+ * revisions are then byte-identical by construction, and the value
433
+ * comparison above resolves that to "not re-stamped" without a second code
434
+ * path deciding it.
435
+ *
436
+ * Runs with `-z` (NUL-delimited output) rather than the default newline/tab
437
+ * form, and for one reason that is NOT about newlines: git's default
438
+ * `--name-status` output C-QUOTES any path containing a non-ASCII byte
439
+ * (`core.quotePath` defaults to true) as a double-quoted string with octal
440
+ * escapes (e.g. `"bundle/\303\266lt.md"` for `bundle/ölt.md`), so a plain
441
+ * `repoRelDocPath` never string-equals that quoted form and a non-ASCII doc
442
+ * falls through every branch below to the `same` fallback, which returns
443
+ * the doc's real (unquoted) CURRENT path unchanged. When that commit was
444
+ * actually a rename or creation, the doc was NOT at that path in the first
445
+ * parent -- it lived under a different name there, or did not exist yet --
446
+ * so the caller's `git show <firstParent>:<that path>` blob read fails,
447
+ * turning a normal rename or creation into a false `not assessable`
448
+ * notice. `-z` prints every path verbatim,
449
+ * unquoted, regardless of `core.quotePath`, closing that structurally
450
+ * rather than by passing `-c core.quotePath=false` (which is a config
451
+ * override this rule would otherwise have to remember on every git
452
+ * invocation touching a path, not just this one).
453
+ */
454
+ function previousPathIn(git, repoRoot, parentSha, sha, repoRelDocPath) {
455
+ const nameStatus = git([
456
+ "diff-tree",
457
+ "-r",
458
+ "-M",
459
+ "-z",
460
+ "--name-status",
461
+ "--no-commit-id",
462
+ parentSha,
463
+ sha,
464
+ ], repoRoot);
465
+ if (nameStatus === null)
466
+ return { kind: "unknown" };
467
+ // `-z` NUL-terminates every field (status, then path(s)) instead of the
468
+ // default "status TAB path NEWLINE" (or, for a rename/copy row, "status
469
+ // TAB old-path TAB new-path NEWLINE") -- including a trailing NUL after
470
+ // the very last field, which RunGit's `.trim()` does not strip (NUL is
471
+ // not whitespace), so the split below always drops one empty trailing
472
+ // token. A rename/copy row is 3 NUL-terminated tokens (status, old path,
473
+ // new path); every other row is 2 (status, path) -- read positionally,
474
+ // not by re-joining on a separator, since a path can itself legitimately
475
+ // contain a tab or newline once quoting is off.
476
+ const tokens = nameStatus.split("\0").filter((t) => t !== "");
477
+ let i = 0;
478
+ while (i < tokens.length) {
479
+ const status = tokens[i];
480
+ if (status.startsWith("R") || status.startsWith("C")) {
481
+ const oldPath = tokens[i + 1];
482
+ const newPath = tokens[i + 2];
483
+ i += 3;
484
+ if (newPath === repoRelDocPath) {
485
+ return { kind: "renamed", path: oldPath };
486
+ }
487
+ continue;
488
+ }
489
+ const p = tokens[i + 1];
490
+ i += 2;
491
+ if (p !== repoRelDocPath)
492
+ continue;
493
+ if (status.startsWith("A"))
494
+ return { kind: "created" };
495
+ return { kind: "same", path: repoRelDocPath };
496
+ }
497
+ return { kind: "same", path: repoRelDocPath };
498
+ }
119
499
  //# sourceMappingURL=sources-fresh.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sources-fresh.js","sourceRoot":"","sources":["../../src/rules/sources-fresh.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sources-fresh.js","sourceRoot":"","sources":["../../src/rules/sources-fresh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AASpB,MAAM,OAAO,GAAG,eAAe,CAAC;AAChC,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAE/C,MAAM,CAAC,MAAM,gBAAgB,GAAS;IACpC,EAAE,EAAE,OAAO;IACX,WAAW,EACT,6vBAA6vB;IAC/vB,GAAG,CAAC,GAAG;QACL,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,MAAM,eAAe,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC;QAElD,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAClB,sEAAsE;YACtE,mEAAmE;YACnE,qEAAqE;YACrE,oEAAoE;YACpE,mEAAmE;YACnE,oEAAoE;YACpE,oBAAoB;YACpB,QAAQ,CAAC,IAAI,CAAC;gBACZ,MAAM,EAAE,OAAO;gBACf,QAAQ,EAAE,QAAQ;gBAClB,IAAI,EAAE,EAAE;gBACR,OAAO,EAAE,+CAA+C;aACzD,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,IAAI,aAAa,CAAC;QAExC,qEAAqE;QACrE,6DAA6D;QAC7D,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAyB,CAAC;QAC1D,MAAM,cAAc,GAAG,CAAC,MAAc,EAAiB,EAAE;YACvD,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,MAAM,CAAC;YACxC,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACxD,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACpC,OAAO,KAAK,CAAC;QACf,CAAC,CAAC;QAEF,KAAK,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,eAAe,EAAE,CAAC;YAC/C,MAAM,cAAc,GAAG,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACjE,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,OAAO;oBACf,QAAQ,EAAE,QAAQ;oBAClB,IAAI,EAAE,GAAG,CAAC,OAAO;oBACjB,OAAO,EAAE,8CAA8C;iBACxD,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,sEAAsE;YACtE,qEAAqE;YACrE,qEAAqE;YACrE,qEAAqE;YACrE,sEAAsE;YACtE,uEAAuE;YACvE,mEAAmE;YACnE,mEAAmE;YACnE,qEAAqE;YACrE,eAAe;YACf,EAAE;YACF,8DAA8D;YAC9D,sEAAsE;YACtE,yEAAyE;YACzE,kEAAkE;YAClE,qEAAqE;YACrE,kEAAkE;YAClE,kEAAkE;YAClE,8DAA8D;YAC9D,iDAAiD;YACjD,6DAA6D;YAC7D,gEAAgE;YAChE,kEAAkE;YAClE,kDAAkD;YAClD,MAAM,cAAc,GAAG,gBAAgB,CAAC,QAAQ,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YACtE,MAAM,iBAAiB,GAAG,GAAkB,EAAE,CAC5C,uBAAuB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;YAC9D,IAAI,WAAuC,CAAC;YAC5C,MAAM,UAAU,GAAG,GAAmB,EAAE,CACtC,CAAC,WAAW,KAAK,wBAAwB,CACvC,GAAG,EACH,QAAQ,EACR,cAAc,EACd,GAAG,EAAE,CAAC,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,CAC9C,CAAC,CAAC;YACL,mEAAmE;YACnE,kDAAkD;YAClD,IAAI,qBAAqB,GAAG,KAAK,CAAC;YAElC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,mEAAmE;gBACnE,oCAAoC;gBACpC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;oBAAE,SAAS;gBAE1D,MAAM,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;gBAC3C,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;oBACzB,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,OAAO;wBACf,QAAQ,EAAE,QAAQ;wBAClB,IAAI,EAAE,GAAG,CAAC,OAAO;wBACjB,OAAO,EAAE,0CAA0C,MAAM,IAAI;qBAC9D,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBAED,IAAI,OAAO,GAAG,WAAW,GAAG,cAAc,CAAC;gBAE3C,8DAA8D;gBAC9D,kEAAkE;gBAClE,oEAAoE;gBACpE,mEAAmE;gBACnE,kEAAkE;gBAClE,kEAAkE;gBAClE,sDAAsD;gBACtD,IAAI,OAAO,EAAE,CAAC;oBACZ,MAAM,cAAc,GAAG,iBAAiB,EAAE,CAAC;oBAC3C,IAAI,cAAc,KAAK,IAAI,IAAI,cAAc,IAAI,WAAW,EAAE,CAAC;wBAC7D,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;wBAC7B,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;4BAC5B,OAAO,GAAG,KAAK,CAAC;wBAClB,CAAC;6BAAM,IACL,OAAO,KAAK,SAAS;4BACrB,OAAO,KAAK,sBAAsB,EAClC,CAAC;4BACD,IAAI,CAAC,qBAAqB,EAAE,CAAC;gCAC3B,qBAAqB,GAAG,IAAI,CAAC;gCAC7B,QAAQ,CAAC,IAAI,CAAC;oCACZ,MAAM,EAAE,OAAO;oCACf,QAAQ,EAAE,QAAQ;oCAClB,IAAI,EAAE,GAAG,CAAC,OAAO;oCACjB,OAAO,EACL,OAAO,KAAK,sBAAsB;wCAChC,CAAC,CAAC,gQAAgQ;wCAClQ,CAAC,CAAC,gHAAgH;iCACvH,CAAC,CAAC;4BACL,CAAC;4BACD,SAAS;wBACX,CAAC;oBACH,CAAC;gBACH,CAAC;gBAED,IAAI,OAAO,EAAE,CAAC;oBACZ,QAAQ,CAAC,IAAI,CAAC;wBACZ,MAAM,EAAE,OAAO;wBACf,QAAQ,EAAE,SAAS;wBACnB,IAAI,EAAE,GAAG,CAAC,OAAO;wBACjB,OAAO,EAAE,YAAY,MAAM,cAAc,UAAU,CAAC,WAAW,CAAC,wBAAwB,UAAU,CAAC,cAAc,CAAC,EAAE;qBACrH,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAS;IAC1C,EAAE,EAAE,cAAc;IAClB,WAAW,EACT,ylBAAylB;IAC3lB,GAAG,CAAC,GAAG;QACL,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,MAAM,eAAe,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC;QAClD,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAEnC,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,IAAI,aAAa,CAAC;QACxC,MAAM,WAAW,GACf,GAAG,CAAC,0BAA0B,IAAI,2BAA2B,CAAC;QAEhE,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,eAAe,EAAE,CAAC;YACtC,MAAM,cAAc,GAAG,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACjE,IAAI,cAAc,KAAK,SAAS;gBAAE,SAAS;YAE3C,qEAAqE;YACrE,iEAAiE;YACjE,4DAA4D;YAC5D,oEAAoE;YACpE,8DAA8D;YAC9D,6DAA6D;YAC7D,kEAAkE;YAClE,qEAAqE;YACrE,gBAAgB;YAChB,MAAM,YAAY,GAAG,qBAAqB,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACnE,IAAI,YAAY,KAAK,SAAS,IAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,EAAE,CAAC;gBAClE,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,cAAc;oBACtB,QAAQ,EAAE,QAAQ;oBAClB,IAAI,EAAE,GAAG,CAAC,OAAO;oBACjB,OAAO,EACL,kIAAkI;iBACrI,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,MAAM,cAAc,GAAG,gBAAgB,CAAC,QAAQ,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YACtE,MAAM,cAAc,GAAG,uBAAuB,CAC5C,GAAG,EACH,GAAG,EACH,QAAQ,EACR,cAAc,CACf,CAAC;YACF,IAAI,cAAc,KAAK,IAAI;gBAAE,SAAS;YAEtC,IAAI,cAAc,GAAG,cAAc,GAAG,WAAW,EAAE,CAAC;gBAClD,QAAQ,CAAC,IAAI,CAAC;oBACZ,MAAM,EAAE,cAAc;oBACtB,QAAQ,EAAE,SAAS;oBACnB,IAAI,EAAE,GAAG,CAAC,OAAO;oBACjB,OAAO,EAAE,+BAA+B,UAAU,CAAC,cAAc,CAAC,uCAAuC,UAAU,CAAC,cAAc,CAAC,oBAAoB,WAAW,IAAI;iBACvK,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC;AAEF;;;;;GAKG;AACH,SAAS,kBAAkB,CACzB,GAAkB;IAElB,OAAO,GAAG,CAAC,IAAI;SACZ,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;SACzE,MAAM,CACL,CAAC,KAAK,EAAkD,EAAE,CACxD,KAAK,CAAC,OAAO,KAAK,SAAS,CAC9B,CAAC;AACN,CAAC;AAED,yGAAyG;AACzG,SAAS,gBAAgB,CACvB,QAAgB,EAChB,SAAiB,EACjB,GAAc;IAEd,OAAO,IAAI;SACR,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;SACrD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,kBAAkB,CACzB,GAAW,EACX,QAAgB,EAChB,MAAc;IAEd,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;IACvE,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5C,CAAC;AAED,SAAS,UAAU,CAAC,YAAoB;IACtC,OAAO,IAAI,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;AACrD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,mBAAmB,GAAG,IAAI,OAAO,EAGpC,CAAC;AAEJ,SAAS,uBAAuB,CAC9B,GAAkB,EAClB,GAAW,EACX,QAAgB,EAChB,cAAsB;IAEtB,IAAI,KAAK,GAAG,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;QAClB,mBAAmB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;IAChE,KAAK,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,gBAAgB,GAAG,IAAI,OAAO,EAA0B,CAAC;AAE/D,SAAS,mBAAmB,CAC1B,GAAkB,EAClB,GAAW,EACX,QAAgB;IAEhB,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,yBAAyB,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpE,MAAM,SAAS,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC;IAC9D,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACrC,OAAO,SAAS,CAAC;AACnB,CAAC;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,SAAS,wBAAwB,CAC/B,GAAW,EACX,QAAgB,EAChB,cAAsB,EACtB,aAA4B;IAE5B,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,IAAI,GAAG,GAAG,CACd,CAAC,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,CAAC,EACtD,QAAQ,CACT,CAAC;IACF,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,OAAO,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACtE,oEAAoE;IACpE,wEAAwE;IACxE,uEAAuE;IACvE,oEAAoE;IACpE,uEAAuE;IACvE,2EAA2E;IAC3E,kEAAkE;IAClE,yEAAyE;IACzE,gEAAgE;IAChE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,aAAa,EAAE,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,WAAW,CAAC;IAChE,CAAC;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE/B,MAAM,QAAQ,GAAG,cAAc,CAC7B,GAAG,EACH,QAAQ,EACR,WAAW,EACX,GAAG,EACH,cAAc,CACf,CAAC;IACF,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAClD,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,WAAW,CAAC;IAEpD,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,cAAc,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpE,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACvC,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,WAAW,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC1E,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAEtC,MAAM,YAAY,GAAG,oBAAoB,CACvC,gBAAgB,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,MAAM,CAC7C,CAAC;IACF,MAAM,WAAW,GAAG,oBAAoB,CACtC,gBAAgB,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,MAAM,CAC5C,CAAC;IACF,0EAA0E;IAC1E,oEAAoE;IACpE,OAAO,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,SAAS,cAAc,CACrB,GAAW,EACX,QAAgB,EAChB,SAAiB,EACjB,GAAW,EACX,cAAsB;IAKtB,MAAM,UAAU,GAAG,GAAG,CACpB;QACE,WAAW;QACX,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,eAAe;QACf,gBAAgB;QAChB,SAAS;QACT,GAAG;KACJ,EACD,QAAQ,CACT,CAAC;IACF,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAEpD,wEAAwE;IACxE,wEAAwE;IACxE,wEAAwE;IACxE,uEAAuE;IACvE,sEAAsE;IACtE,yEAAyE;IACzE,uEAAuE;IACvE,yEAAyE;IACzE,gDAAgD;IAChD,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9D,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACrD,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9B,CAAC,IAAI,CAAC,CAAC;YACP,IAAI,OAAO,KAAK,cAAc,EAAE,CAAC;gBAC/B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;YAC5C,CAAC;YACD,SAAS;QACX,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,CAAC,IAAI,CAAC,CAAC;QACP,IAAI,CAAC,KAAK,cAAc;YAAE,SAAS;QACnC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;AAChD,CAAC"}
package/dist/types.d.ts CHANGED
@@ -44,6 +44,24 @@ export interface RequireAnchorsOptions {
44
44
  */
45
45
  allow: string[];
46
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
+ }
47
65
  export interface BundleContext {
48
66
  bundleDir: string;
49
67
  repoRoot?: string;
@@ -52,6 +70,15 @@ export interface BundleContext {
52
70
  runGit?: RunGit;
53
71
  /** See `RequireAnchorsOptions`. Undefined when `--require-anchors` was not passed. */
54
72
  requireAnchors?: RequireAnchorsOptions;
73
+ /** See `ProseLineReferencesOptions`. Undefined when `--prose-line-references` was not passed. */
74
+ proseLineReferences?: ProseLineReferencesOptions;
75
+ /**
76
+ * Clock-skew allowance (seconds) for `sources-fresh-future` (see
77
+ * `--future-skew-minutes` in `src/cli.ts` and `DEFAULT_FUTURE_SKEW_SECONDS`
78
+ * in `src/rules/sources-fresh.ts`). Undefined when `--future-skew-minutes`
79
+ * was not passed, in which case the rule applies its own default.
80
+ */
81
+ freshnessFutureSkewSeconds?: number;
55
82
  }
56
83
  export interface Rule {
57
84
  id: string;
package/dist/util.d.ts CHANGED
@@ -21,3 +21,55 @@ export declare function getValidSources(parsed: unknown): string[] | undefined;
21
21
  * degrade to the no-valid-timestamp notice.
22
22
  */
23
23
  export declare function getTimestampEpoch(parsed: unknown): number | undefined;
24
+ /**
25
+ * The frontmatter `timestamp`'s raw string form, or undefined when it is
26
+ * absent, blank, or not a string -- notably, a native `Date` instance (see
27
+ * `getTimestampEpoch`'s doc comment) returns undefined here too, since a
28
+ * `Date`'s `getTime()` is always UTC-unambiguous and carries none of the
29
+ * local-timezone risk `hasUtcDesignator` exists to catch. Used only by
30
+ * `sources-fresh-future`'s UTC-designator gate; `sources-fresh` itself has
31
+ * no need for the raw string, only the resolved epoch.
32
+ */
33
+ export declare function getRawTimestampString(parsed: unknown): string | undefined;
34
+ /**
35
+ * Whether an ISO-ish timestamp string carries an explicit UTC designator
36
+ * (`Z`) or a numeric UTC offset (`+02:00`, `-0500`) at its end. A timestamp
37
+ * with neither ("2026-01-01T00:00:00", "2026-01-01 00:00:00") parses in
38
+ * the machine's LOCAL timezone under `Date.parse`, which would make
39
+ * `sources-fresh-future`'s clock-skew comparison swing by hours depending
40
+ * on which machine runs the check -- not usable for a check whose default
41
+ * allowance is only 10 minutes. A numeric offset, unlike a bare local
42
+ * time, is unambiguous: `Date.parse`/`Date#getTime()` already normalizes
43
+ * it to a real UTC instant, so no extra conversion is needed here beyond
44
+ * recognizing it as present. `sources-fresh`'s own thresholds are in
45
+ * days, wide enough that this ambiguity doesn't practically matter there,
46
+ * so this helper is deliberately NOT applied to that rule's comparison.
47
+ */
48
+ export declare function hasUtcDesignator(raw: string): boolean;
49
+ /**
50
+ * A canonical identity string for the frontmatter `timestamp` VALUE, or
51
+ * undefined when the key is absent, blank, or not a scalar this tool
52
+ * understands. Two docs (or two revisions of one doc) are "stamped the same"
53
+ * iff this returns the same value for both.
54
+ *
55
+ * Used by `sources-fresh` to decide whether a commit actually re-stamped a
56
+ * doc, by comparing the doc's frontmatter at that commit against its
57
+ * frontmatter in the commit's first parent. Comparing VALUES, not diff text,
58
+ * is what makes the test immune to the three shapes a diff-text scan gets
59
+ * wrong: a `timestamp:` line inside a fenced YAML example in the doc BODY (it
60
+ * is not the frontmatter key, so it never reaches this function at all), a
61
+ * rename (the two revisions are read by path, not from a diff header), and a
62
+ * merge commit (whose combined-diff output is empty while its trees are
63
+ * perfectly readable).
64
+ *
65
+ * The `date:`/`string:` prefixes keep the two YAML shapes distinguishable: a
66
+ * `!!timestamp`-tagged scalar resolving to a native `Date` and a plain string
67
+ * are different frontmatter, so rewriting one into the other counts as a
68
+ * re-stamp rather than silently comparing equal. Deliberately NOT normalized
69
+ * to an epoch: this is an identity test ("did the value change"), not a
70
+ * chronological one, so re-writing `2026-01-01T00:00:00Z` as
71
+ * `2026-01-01T00:00:00+00:00` counts as a re-stamp -- the author touched the
72
+ * stamp. Whether the new value is CORRECT is a separate question this
73
+ * function deliberately does not answer (see the README's known limitations).
74
+ */
75
+ export declare function getTimestampIdentity(parsed: unknown): string | undefined;