okf-kit 0.9.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 +146 -0
- package/README.md +44 -8
- package/dist/bundle.d.ts +18 -1
- package/dist/bundle.js +8 -1
- package/dist/bundle.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -1
- package/dist/git.d.ts +2 -2
- package/dist/git.js +16 -2
- package/dist/git.js.map +1 -1
- package/dist/rules/index.d.ts +2 -2
- package/dist/rules/index.js +8 -2
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/sources-fresh.d.ts +35 -0
- package/dist/rules/sources-fresh.js +398 -18
- package/dist/rules/sources-fresh.js.map +1 -1
- package/dist/types.d.ts +7 -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
|
@@ -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
|
|
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
|
|
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)
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
80
|
-
//
|
|
81
|
-
// frontmatter-only
|
|
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
|
-
|
|
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":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -72,6 +72,13 @@ export interface BundleContext {
|
|
|
72
72
|
requireAnchors?: RequireAnchorsOptions;
|
|
73
73
|
/** See `ProseLineReferencesOptions`. Undefined when `--prose-line-references` was not passed. */
|
|
74
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;
|
|
75
82
|
}
|
|
76
83
|
export interface Rule {
|
|
77
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;
|
package/dist/util.js
CHANGED
|
@@ -47,4 +47,76 @@ export function getTimestampEpoch(parsed) {
|
|
|
47
47
|
return undefined;
|
|
48
48
|
return Math.floor(ms / 1000);
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The frontmatter `timestamp`'s raw string form, or undefined when it is
|
|
52
|
+
* absent, blank, or not a string -- notably, a native `Date` instance (see
|
|
53
|
+
* `getTimestampEpoch`'s doc comment) returns undefined here too, since a
|
|
54
|
+
* `Date`'s `getTime()` is always UTC-unambiguous and carries none of the
|
|
55
|
+
* local-timezone risk `hasUtcDesignator` exists to catch. Used only by
|
|
56
|
+
* `sources-fresh-future`'s UTC-designator gate; `sources-fresh` itself has
|
|
57
|
+
* no need for the raw string, only the resolved epoch.
|
|
58
|
+
*/
|
|
59
|
+
export function getRawTimestampString(parsed) {
|
|
60
|
+
if (!isRecord(parsed))
|
|
61
|
+
return undefined;
|
|
62
|
+
const timestamp = parsed.timestamp;
|
|
63
|
+
if (typeof timestamp !== "string" || timestamp.trim() === "")
|
|
64
|
+
return undefined;
|
|
65
|
+
return timestamp;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether an ISO-ish timestamp string carries an explicit UTC designator
|
|
69
|
+
* (`Z`) or a numeric UTC offset (`+02:00`, `-0500`) at its end. A timestamp
|
|
70
|
+
* with neither ("2026-01-01T00:00:00", "2026-01-01 00:00:00") parses in
|
|
71
|
+
* the machine's LOCAL timezone under `Date.parse`, which would make
|
|
72
|
+
* `sources-fresh-future`'s clock-skew comparison swing by hours depending
|
|
73
|
+
* on which machine runs the check -- not usable for a check whose default
|
|
74
|
+
* allowance is only 10 minutes. A numeric offset, unlike a bare local
|
|
75
|
+
* time, is unambiguous: `Date.parse`/`Date#getTime()` already normalizes
|
|
76
|
+
* it to a real UTC instant, so no extra conversion is needed here beyond
|
|
77
|
+
* recognizing it as present. `sources-fresh`'s own thresholds are in
|
|
78
|
+
* days, wide enough that this ambiguity doesn't practically matter there,
|
|
79
|
+
* so this helper is deliberately NOT applied to that rule's comparison.
|
|
80
|
+
*/
|
|
81
|
+
export function hasUtcDesignator(raw) {
|
|
82
|
+
return /(?:Z|[+-]\d{2}:?\d{2})$/i.test(raw.trim());
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A canonical identity string for the frontmatter `timestamp` VALUE, or
|
|
86
|
+
* undefined when the key is absent, blank, or not a scalar this tool
|
|
87
|
+
* understands. Two docs (or two revisions of one doc) are "stamped the same"
|
|
88
|
+
* iff this returns the same value for both.
|
|
89
|
+
*
|
|
90
|
+
* Used by `sources-fresh` to decide whether a commit actually re-stamped a
|
|
91
|
+
* doc, by comparing the doc's frontmatter at that commit against its
|
|
92
|
+
* frontmatter in the commit's first parent. Comparing VALUES, not diff text,
|
|
93
|
+
* is what makes the test immune to the three shapes a diff-text scan gets
|
|
94
|
+
* wrong: a `timestamp:` line inside a fenced YAML example in the doc BODY (it
|
|
95
|
+
* is not the frontmatter key, so it never reaches this function at all), a
|
|
96
|
+
* rename (the two revisions are read by path, not from a diff header), and a
|
|
97
|
+
* merge commit (whose combined-diff output is empty while its trees are
|
|
98
|
+
* perfectly readable).
|
|
99
|
+
*
|
|
100
|
+
* The `date:`/`string:` prefixes keep the two YAML shapes distinguishable: a
|
|
101
|
+
* `!!timestamp`-tagged scalar resolving to a native `Date` and a plain string
|
|
102
|
+
* are different frontmatter, so rewriting one into the other counts as a
|
|
103
|
+
* re-stamp rather than silently comparing equal. Deliberately NOT normalized
|
|
104
|
+
* to an epoch: this is an identity test ("did the value change"), not a
|
|
105
|
+
* chronological one, so re-writing `2026-01-01T00:00:00Z` as
|
|
106
|
+
* `2026-01-01T00:00:00+00:00` counts as a re-stamp -- the author touched the
|
|
107
|
+
* stamp. Whether the new value is CORRECT is a separate question this
|
|
108
|
+
* function deliberately does not answer (see the README's known limitations).
|
|
109
|
+
*/
|
|
110
|
+
export function getTimestampIdentity(parsed) {
|
|
111
|
+
if (!isRecord(parsed))
|
|
112
|
+
return undefined;
|
|
113
|
+
const timestamp = parsed.timestamp;
|
|
114
|
+
if (timestamp instanceof Date) {
|
|
115
|
+
const ms = timestamp.getTime();
|
|
116
|
+
return Number.isNaN(ms) ? undefined : `date:${ms}`;
|
|
117
|
+
}
|
|
118
|
+
if (typeof timestamp !== "string" || timestamp.trim() === "")
|
|
119
|
+
return undefined;
|
|
120
|
+
return `string:${timestamp.trim()}`;
|
|
121
|
+
}
|
|
50
122
|
//# sourceMappingURL=util.js.map
|