@refrakt-md/content 0.36.0 → 0.37.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,69 @@
1
+ /**
2
+ * The git scan (SPEC-136 D5, WORK-594).
3
+ *
4
+ * One pass over the repository's history, producing for every path the commits
5
+ * that touched it. Adapted from `timestamps.ts`'s scan rather than rewritten —
6
+ * the shallow-clone and git-root handling there are the parts most easily got
7
+ * wrong.
8
+ *
9
+ * **`--full-history`, and no pathspec.** This is the single easiest way to ship
10
+ * this feature with quietly wrong numbers, and it was found by measuring
11
+ * SPEC-136's own base rate:
12
+ *
13
+ * ```
14
+ * git log --name-only -- site/content/docs/cli/cli-overview.md → 2 commits
15
+ * git log --full-history --name-only (no pathspec) → 5 commits
16
+ * ```
17
+ *
18
+ * A pathspec makes git apply history simplification. Both outputs are correct
19
+ * git answering different questions — but this command does *arithmetic* on
20
+ * those timestamps, since the referrer's last-changed time is one half of every
21
+ * score. A simplified history inflates every edge out of a file and deflates
22
+ * every edge into it, and the failure is invisible: the report still renders,
23
+ * still ranks, still looks plausible.
24
+ */
25
+ export interface Commit {
26
+ sha: string;
27
+ /** Unix seconds. */
28
+ at: number;
29
+ subject: string;
30
+ }
31
+ export interface GitHistory {
32
+ /** Path (relative to the git root) → commits touching it, newest first. */
33
+ byPath: Map<string, Commit[]>;
34
+ /** Prefix from the git root to the scanned directory, '' at the root. */
35
+ prefix: string;
36
+ }
37
+ /** Why the scan could not run. A refusal is never a zero — D5. */
38
+ export declare class GitScanRefusal extends Error {
39
+ constructor(message: string);
40
+ }
41
+ /** A shallow clone reports one commit per path, so every edge scores zero and
42
+ * the tool confidently reports a clean corpus. That is the silent-wrong class
43
+ * this feature exists to avoid, so it refuses instead. */
44
+ export declare function isShallowClone(cwd: string): boolean;
45
+ export declare function isGitRepository(cwd: string): boolean;
46
+ /** Path from the git root to `cwd`, '' when already at the root. */
47
+ export declare function gitRelativePrefix(cwd: string): string;
48
+ /**
49
+ * Scan the whole history once.
50
+ *
51
+ * @throws {GitScanRefusal} on a non-git tree or a shallow clone. Both are
52
+ * "could not measure", which is a different event from "measured, nothing
53
+ * wrong" and must not be reported as the latter.
54
+ */
55
+ export declare function scanHistory(cwd: string): GitHistory;
56
+ /** The most recent commit timestamp for a path, or undefined if untracked. */
57
+ export declare function lastChanged(history: GitHistory, path: string): number | undefined;
58
+ /** Commits touching `path` strictly after `since`, newest first. */
59
+ export declare function commitsSince(history: GitHistory, path: string, since: number): Commit[];
60
+ /**
61
+ * Paths changed since a git ref — the `since:` query's input.
62
+ *
63
+ * A PR-scoped sweep: resolve what the branch touched, then ask the same
64
+ * question `touching` asks. Returns an empty list rather than throwing when the
65
+ * ref cannot be resolved, because a missing ref is the caller's mistake to
66
+ * report, not a reason to fail the whole query.
67
+ */
68
+ export declare function changedSince(cwd: string, ref: string): string[];
69
+ //# sourceMappingURL=git-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-scan.d.ts","sourceRoot":"","sources":["../../src/edges/git-scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,MAAM,WAAW,MAAM;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,qBAAa,cAAe,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI3B;AAWD;;2DAE2D;AAC3D,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMnD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMrD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAyCnD;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKjF;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAIvF;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/D"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The git scan (SPEC-136 D5, WORK-594).
3
+ *
4
+ * One pass over the repository's history, producing for every path the commits
5
+ * that touched it. Adapted from `timestamps.ts`'s scan rather than rewritten —
6
+ * the shallow-clone and git-root handling there are the parts most easily got
7
+ * wrong.
8
+ *
9
+ * **`--full-history`, and no pathspec.** This is the single easiest way to ship
10
+ * this feature with quietly wrong numbers, and it was found by measuring
11
+ * SPEC-136's own base rate:
12
+ *
13
+ * ```
14
+ * git log --name-only -- site/content/docs/cli/cli-overview.md → 2 commits
15
+ * git log --full-history --name-only (no pathspec) → 5 commits
16
+ * ```
17
+ *
18
+ * A pathspec makes git apply history simplification. Both outputs are correct
19
+ * git answering different questions — but this command does *arithmetic* on
20
+ * those timestamps, since the referrer's last-changed time is one half of every
21
+ * score. A simplified history inflates every edge out of a file and deflates
22
+ * every edge into it, and the failure is invisible: the report still renders,
23
+ * still ranks, still looks plausible.
24
+ */
25
+ import { execSync } from 'node:child_process';
26
+ /** Why the scan could not run. A refusal is never a zero — D5. */
27
+ export class GitScanRefusal extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'GitScanRefusal';
31
+ }
32
+ }
33
+ function git(command, cwd) {
34
+ return execSync(command, {
35
+ cwd,
36
+ encoding: 'utf-8',
37
+ maxBuffer: 256 * 1024 * 1024,
38
+ stdio: ['pipe', 'pipe', 'pipe'],
39
+ });
40
+ }
41
+ /** A shallow clone reports one commit per path, so every edge scores zero and
42
+ * the tool confidently reports a clean corpus. That is the silent-wrong class
43
+ * this feature exists to avoid, so it refuses instead. */
44
+ export function isShallowClone(cwd) {
45
+ try {
46
+ return git('git rev-parse --is-shallow-repository', cwd).trim() === 'true';
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ export function isGitRepository(cwd) {
53
+ try {
54
+ return git('git rev-parse --is-inside-work-tree', cwd).trim() === 'true';
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /** Path from the git root to `cwd`, '' when already at the root. */
61
+ export function gitRelativePrefix(cwd) {
62
+ const root = git('git rev-parse --show-toplevel', cwd).trim();
63
+ const abs = git('pwd', cwd).trim();
64
+ if (abs === root)
65
+ return '';
66
+ const rel = abs.slice(root.length + 1);
67
+ return rel ? `${rel}/` : '';
68
+ }
69
+ /**
70
+ * Scan the whole history once.
71
+ *
72
+ * @throws {GitScanRefusal} on a non-git tree or a shallow clone. Both are
73
+ * "could not measure", which is a different event from "measured, nothing
74
+ * wrong" and must not be reported as the latter.
75
+ */
76
+ export function scanHistory(cwd) {
77
+ if (!isGitRepository(cwd)) {
78
+ throw new GitScanRefusal('not a git repository — `refrakt stale` measures commits against a file, and has no ' +
79
+ 'non-git fallback. Run it inside a checkout.');
80
+ }
81
+ if (isShallowClone(cwd)) {
82
+ throw new GitScanRefusal('shallow clone — every path would report a single commit and every edge would score ' +
83
+ 'zero, which is indistinguishable from a clean corpus. Set `fetch-depth: 0` on the ' +
84
+ 'checkout step (actions/checkout defaults to 1) and run again.');
85
+ }
86
+ // `%x00` separates the subject so a subject containing the field separator
87
+ // cannot split the record.
88
+ const output = git('git log --full-history --format="__C__%H%x00%at%x00%s" --name-only --diff-filter=ACMR HEAD', cwd);
89
+ const byPath = new Map();
90
+ let current;
91
+ for (const raw of output.split('\n')) {
92
+ const line = raw.replace(/\r$/, '');
93
+ if (line.startsWith('__C__')) {
94
+ const [sha, at, ...rest] = line.slice(5).split('\u0000');
95
+ current = { sha: sha.slice(0, 7), at: Number(at), subject: rest.join('\u0000') };
96
+ continue;
97
+ }
98
+ if (line.length === 0 || current === undefined)
99
+ continue;
100
+ const list = byPath.get(line);
101
+ if (list)
102
+ list.push(current);
103
+ else
104
+ byPath.set(line, [current]);
105
+ }
106
+ return { byPath, prefix: gitRelativePrefix(cwd) };
107
+ }
108
+ /** The most recent commit timestamp for a path, or undefined if untracked. */
109
+ export function lastChanged(history, path) {
110
+ const commits = history.byPath.get(path);
111
+ if (!commits || commits.length === 0)
112
+ return undefined;
113
+ // `git log` emits newest first, so the head is the most recent.
114
+ return commits[0].at;
115
+ }
116
+ /** Commits touching `path` strictly after `since`, newest first. */
117
+ export function commitsSince(history, path, since) {
118
+ const commits = history.byPath.get(path);
119
+ if (!commits)
120
+ return [];
121
+ return commits.filter((c) => c.at > since);
122
+ }
123
+ /**
124
+ * Paths changed since a git ref — the `since:` query's input.
125
+ *
126
+ * A PR-scoped sweep: resolve what the branch touched, then ask the same
127
+ * question `touching` asks. Returns an empty list rather than throwing when the
128
+ * ref cannot be resolved, because a missing ref is the caller's mistake to
129
+ * report, not a reason to fail the whole query.
130
+ */
131
+ export function changedSince(cwd, ref) {
132
+ const out = git(`git diff --name-only ${ref}...HEAD`, cwd);
133
+ return out
134
+ .split('\n')
135
+ .map((line) => line.trim())
136
+ .filter((line) => line.length > 0);
137
+ }
138
+ //# sourceMappingURL=git-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-scan.js","sourceRoot":"","sources":["../../src/edges/git-scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The edge index (SPEC-136 D12, WORK-594).
3
+ *
4
+ * **Built as an addressable index, with ranking as one reader of it.** The
5
+ * tempting shape is a function that walks content, scores as it goes, and
6
+ * prints. That shape cannot answer WORK-596's `touching` query, which runs in
7
+ * the opposite direction — from a changed file to the pages that document it —
8
+ * and without the git scan at all. The cost of doing it properly is an
9
+ * afternoon here and a rewrite avoided later.
10
+ */
11
+ import { type DeclaredEdgeError, type Edge, type EdgeClass } from './extract.js';
12
+ import { type Commit, type GitHistory } from './git-scan.js';
13
+ export type { Edge, EdgeClass, DeclaredEdgeError } from './extract.js';
14
+ export type { Commit, GitHistory } from './git-scan.js';
15
+ export { changedSince, GitScanRefusal, scanHistory } from './git-scan.js';
16
+ export { globMatcher, globToRegExp, resolveStaleSettings, StaleConfigRefusal, type StaleSettings, } from './exclude.js';
17
+ export interface EdgeIndex {
18
+ edges: Edge[];
19
+ /** Declared edges that resolve to nothing — D14 reports these loudly, where
20
+ * every inferred class stays silent. */
21
+ errors: DeclaredEdgeError[];
22
+ /** target path → edges pointing at it. The `touching` direction. */
23
+ byTarget: Map<string, Edge[]>;
24
+ /** referrer page → edges it declares. */
25
+ byReferrer: Map<string, Edge[]>;
26
+ /** What the project's `stale` config removed. Reported, never silent: an
27
+ * exclusion list that hides its own effect is how the report converges on
28
+ * empty without anyone deciding that it should. */
29
+ excluded: ExclusionCount;
30
+ }
31
+ export interface ExclusionCount {
32
+ /** Pages skipped whole, matching an `archival` glob. */
33
+ archivalPages: number;
34
+ /** Edges dropped because their target matched a `generated` glob. */
35
+ generatedTargets: number;
36
+ }
37
+ /**
38
+ * Build the index from a content directory.
39
+ *
40
+ * Needs no git and no build, so `touching` can answer for a file created in the
41
+ * working tree and never committed.
42
+ */
43
+ export declare function buildEdgeIndex(opts: {
44
+ repoRoot: string;
45
+ contentDirs: string[];
46
+ /** Referrer globs — matching pages contribute no edges at all. */
47
+ archival?: string[];
48
+ /** Target globs — edges pointing at a match are dropped. */
49
+ generated?: string[];
50
+ }): EdgeIndex;
51
+ /** Index a list of edges in both directions. */
52
+ export declare function indexEdges(edges: Edge[]): EdgeIndex;
53
+ /**
54
+ * The `touching` query — every page whose edges point at any of `paths`.
55
+ *
56
+ * Not a staleness measurement and not a filter over the ranked report: there is
57
+ * no commit count, because the change has not happened yet. It takes no git
58
+ * history, is unbounded by `--top`, and is the query that makes this feature
59
+ * preventive rather than retrospective.
60
+ */
61
+ export declare function edgesTouching(index: EdgeIndex, paths: string[]): Edge[];
62
+ export interface RankedEdge {
63
+ edge: Edge;
64
+ /** Commits touching the target since the referrer last changed. */
65
+ score: number;
66
+ /** Unix seconds; undefined when the referrer is untracked. */
67
+ referrerChanged?: number;
68
+ targetChanged?: number;
69
+ commits: Commit[];
70
+ }
71
+ export interface RankOptions {
72
+ /** Narrow to one edge class. */
73
+ edgeClass?: EdgeClass;
74
+ /** Floor on the commit count. */
75
+ min?: number;
76
+ }
77
+ /** Per-class base rate — how many edges of that class scored non-zero out of
78
+ * how many were scanned. Printed in the footer so degradation is visible
79
+ * rather than inferred. */
80
+ export interface BaseRate {
81
+ class: EdgeClass;
82
+ nonZero: number;
83
+ scanned: number;
84
+ }
85
+ export interface RankResult {
86
+ ranked: RankedEdge[];
87
+ baseRates: BaseRate[];
88
+ /** Total non-zero edges before `--top` truncation. */
89
+ totalNonZero: number;
90
+ }
91
+ /**
92
+ * Rank the index against a git history.
93
+ *
94
+ * One reader of the index among several — `touching` is another, and neither
95
+ * owns it.
96
+ *
97
+ * An edge whose referrer is newer than every change to its target scores zero
98
+ * and is omitted. A zero score is **the absence of evidence of staleness, not
99
+ * evidence of freshness**: the referrer side resets on any edit, including a
100
+ * typo fix (D6, a known and accepted false negative).
101
+ */
102
+ export declare function rankEdges(index: EdgeIndex, history: GitHistory, opts?: RankOptions): RankResult;
103
+ /** Build the index and rank it in one call — the report's entry point.
104
+ *
105
+ * `contentDirs`, `archival` and `generated` all come from the project's
106
+ * `refrakt.config.json` when not passed explicitly (tests pass fixtures). None
107
+ * of the three has a built-in value: a directory layout is a property of the
108
+ * project, not of the tool. */
109
+ export declare function runStale(opts: {
110
+ repoRoot: string;
111
+ contentDirs?: string[];
112
+ archival?: string[];
113
+ generated?: string[];
114
+ edgeClass?: EdgeClass;
115
+ min?: number;
116
+ }): RankResult & {
117
+ index: EdgeIndex;
118
+ };
119
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edges/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,264 @@
1
+ /**
2
+ * The edge index (SPEC-136 D12, WORK-594).
3
+ *
4
+ * **Built as an addressable index, with ranking as one reader of it.** The
5
+ * tempting shape is a function that walks content, scores as it goes, and
6
+ * prints. That shape cannot answer WORK-596's `touching` query, which runs in
7
+ * the opposite direction — from a changed file to the pages that document it —
8
+ * and without the git scan at all. The cost of doing it properly is an
9
+ * afternoon here and a rewrite avoided later.
10
+ */
11
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
12
+ import { join, relative, sep } from 'node:path';
13
+ import { compareMarker, parseMarker, sliceForInvocation } from '@refrakt-md/runes';
14
+ import { dedupeEdges, extractDeclaredEdges, extractDescribedLinkEdges, extractEmbeddedEdges, extractProseEdges, } from './extract.js';
15
+ import { parseFrontmatter } from '../frontmatter.js';
16
+ import { globMatcher, resolveStaleSettings } from './exclude.js';
17
+ import { commitsSince, lastChanged, scanHistory, } from './git-scan.js';
18
+ export { changedSince, GitScanRefusal, scanHistory } from './git-scan.js';
19
+ export { globMatcher, globToRegExp, resolveStaleSettings, StaleConfigRefusal, } from './exclude.js';
20
+ /** Collect content files under a directory, repo-root-relative POSIX. */
21
+ function collectPages(contentDir, repoRoot, out = []) {
22
+ let entries;
23
+ try {
24
+ entries = readdirSync(contentDir);
25
+ }
26
+ catch {
27
+ return out;
28
+ }
29
+ for (const entry of entries) {
30
+ const full = join(contentDir, entry);
31
+ if (statSync(full).isDirectory())
32
+ collectPages(full, repoRoot, out);
33
+ else if (entry.endsWith('.md'))
34
+ out.push(relative(repoRoot, full).split(sep).join('/'));
35
+ }
36
+ return out;
37
+ }
38
+ /**
39
+ * Build the index from a content directory.
40
+ *
41
+ * Needs no git and no build, so `touching` can answer for a file created in the
42
+ * working tree and never committed.
43
+ */
44
+ export function buildEdgeIndex(opts) {
45
+ const edges = [];
46
+ const errors = [];
47
+ const isArchival = globMatcher(opts.archival);
48
+ const isGenerated = globMatcher(opts.generated);
49
+ const excluded = { archivalPages: 0, generatedTargets: 0 };
50
+ const exists = (path) => {
51
+ try {
52
+ return statSync(join(opts.repoRoot, path)).isFile();
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ };
58
+ // Every content page, for resolving described-link targets. A link points
59
+ // at a route; the edge needs the file behind it.
60
+ const allPages = opts.contentDirs.flatMap((d) => collectPages(join(opts.repoRoot, d), opts.repoRoot));
61
+ const byRoute = new Map();
62
+ for (const page of allPages) {
63
+ for (const dir of opts.contentDirs) {
64
+ if (!page.startsWith(`${dir}/`))
65
+ continue;
66
+ const rel = page.slice(dir.length + 1);
67
+ const route = `/${rel.replace(/\.md$/, '').replace(/\/index$/, '')}`;
68
+ byRoute.set(route === '/index' ? '/' : route, page);
69
+ }
70
+ }
71
+ // Both `<path>.md` and `<path>/index.md` resolve. The anchor is already
72
+ // stripped by the extractor.
73
+ const resolvePage = (route) => byRoute.get(route) ?? byRoute.get(`${route}/index`);
74
+ for (const page of allPages) {
75
+ // An archival page is skipped before extraction, not filtered after it,
76
+ // so it disappears from `touching` too — "what documents this file"
77
+ // must not answer with a record of what was once true.
78
+ if (isArchival(page)) {
79
+ excluded.archivalPages++;
80
+ continue;
81
+ }
82
+ let source;
83
+ try {
84
+ source = readFileSync(join(opts.repoRoot, page), 'utf8');
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ const { frontmatter } = parseFrontmatter(source);
90
+ edges.push(...extractEmbeddedEdges(page, source));
91
+ // A `_layout.md` declares nothing on behalf of the pages beneath it
92
+ // (D13) — that claim would be attributed to every one of them.
93
+ if (!page.endsWith('/_layout.md')) {
94
+ const declared = extractDeclaredEdges(page, frontmatter, exists);
95
+ edges.push(...declared.edges);
96
+ errors.push(...declared.errors);
97
+ }
98
+ edges.push(...extractDescribedLinkEdges(page, source, resolvePage));
99
+ edges.push(...extractProseEdges(page, source, exists));
100
+ }
101
+ // A marker outranks a commit count, but only while it is still true. This
102
+ // is where the two are told apart — here rather than in `rankEdges`, which
103
+ // reads no files, and against the same slice `snippet review` hashes.
104
+ for (const edge of edges) {
105
+ if (!edge.reviewed || edge.attrs === undefined)
106
+ continue;
107
+ edge.markerStale = !markerIsCurrent(opts.repoRoot, edge.target, edge.attrs, edge.reviewed);
108
+ edge.attrs = undefined;
109
+ }
110
+ const deduped = dedupeEdges(edges);
111
+ const kept = deduped.filter((edge) => {
112
+ if (!isGenerated(edge.target))
113
+ return true;
114
+ excluded.generatedTargets++;
115
+ return false;
116
+ });
117
+ return { ...indexEdges(kept), errors, excluded };
118
+ }
119
+ /**
120
+ * Does an attached marker still certify the target's current content?
121
+ *
122
+ * A formatting-only change counts as current: SPEC-134 D3 already decided that
123
+ * a reformat does not cost a review, and re-deciding it here would make the two
124
+ * tools disagree.
125
+ *
126
+ * An anchor that refuses, or a target that cannot be read, answers `true`. The
127
+ * refusal is SPEC-131's to report, and treating "could not check" as "stale"
128
+ * would put every broken anchor at the top of a report about documentation.
129
+ */
130
+ function markerIsCurrent(repoRoot, target, attrs, reviewed) {
131
+ let source;
132
+ try {
133
+ source = readFileSync(join(repoRoot, target), 'utf8');
134
+ }
135
+ catch {
136
+ return true;
137
+ }
138
+ const current = sliceForInvocation(attrs, target, source);
139
+ if (current === undefined)
140
+ return true;
141
+ const stored = parseMarker(reviewed);
142
+ const verdict = compareMarker(stored.strict, current, stored.loose).verdict;
143
+ return verdict === 'current' || verdict === 'formatting-only';
144
+ }
145
+ /** Index a list of edges in both directions. */
146
+ export function indexEdges(edges) {
147
+ const byTarget = new Map();
148
+ const byReferrer = new Map();
149
+ for (const edge of edges) {
150
+ const t = byTarget.get(edge.target);
151
+ if (t)
152
+ t.push(edge);
153
+ else
154
+ byTarget.set(edge.target, [edge]);
155
+ const r = byReferrer.get(edge.referrer);
156
+ if (r)
157
+ r.push(edge);
158
+ else
159
+ byReferrer.set(edge.referrer, [edge]);
160
+ }
161
+ return {
162
+ edges,
163
+ errors: [],
164
+ byTarget,
165
+ byReferrer,
166
+ excluded: { archivalPages: 0, generatedTargets: 0 },
167
+ };
168
+ }
169
+ /**
170
+ * The `touching` query — every page whose edges point at any of `paths`.
171
+ *
172
+ * Not a staleness measurement and not a filter over the ranked report: there is
173
+ * no commit count, because the change has not happened yet. It takes no git
174
+ * history, is unbounded by `--top`, and is the query that makes this feature
175
+ * preventive rather than retrospective.
176
+ */
177
+ export function edgesTouching(index, paths) {
178
+ const out = [];
179
+ for (const path of paths) {
180
+ out.push(...(index.byTarget.get(path) ?? []));
181
+ }
182
+ return out;
183
+ }
184
+ /**
185
+ * Rank the index against a git history.
186
+ *
187
+ * One reader of the index among several — `touching` is another, and neither
188
+ * owns it.
189
+ *
190
+ * An edge whose referrer is newer than every change to its target scores zero
191
+ * and is omitted. A zero score is **the absence of evidence of staleness, not
192
+ * evidence of freshness**: the referrer side resets on any edit, including a
193
+ * typo fix (D6, a known and accepted false negative).
194
+ */
195
+ export function rankEdges(index, history, opts = {}) {
196
+ const ranked = [];
197
+ const rates = new Map();
198
+ for (const edge of index.edges) {
199
+ if (opts.edgeClass && edge.class !== opts.edgeClass)
200
+ continue;
201
+ let rate = rates.get(edge.class);
202
+ if (!rate) {
203
+ rate = { class: edge.class, nonZero: 0, scanned: 0 };
204
+ rates.set(edge.class, rate);
205
+ }
206
+ rate.scanned++;
207
+ const referrerChanged = lastChanged(history, `${history.prefix}${edge.referrer}`);
208
+ const targetChanged = lastChanged(history, `${history.prefix}${edge.target}`);
209
+ // An untracked referrer has no "since" to measure from. Reporting it as
210
+ // maximally stale would put every new page at the top of the report.
211
+ if (referrerChanged === undefined)
212
+ continue;
213
+ // A SPEC-134 marker is a stronger statement than a commit count, so
214
+ // where one exists it wins (D3) — but only while it is still true. A
215
+ // marker the target has outgrown has expired as a statement, so the
216
+ // commit count stands again.
217
+ if (edge.reviewed && !edge.markerStale)
218
+ continue;
219
+ const commits = commitsSince(history, `${history.prefix}${edge.target}`, referrerChanged);
220
+ if (commits.length === 0)
221
+ continue;
222
+ if (opts.min !== undefined && commits.length < opts.min)
223
+ continue;
224
+ rate.nonZero++;
225
+ ranked.push({
226
+ edge,
227
+ score: commits.length,
228
+ referrerChanged,
229
+ targetChanged,
230
+ commits,
231
+ });
232
+ }
233
+ ranked.sort((a, b) => b.score - a.score || a.edge.referrer.localeCompare(b.edge.referrer));
234
+ return {
235
+ ranked,
236
+ baseRates: [...rates.values()].sort((a, b) => a.class.localeCompare(b.class)),
237
+ totalNonZero: ranked.length,
238
+ };
239
+ }
240
+ /** Build the index and rank it in one call — the report's entry point.
241
+ *
242
+ * `contentDirs`, `archival` and `generated` all come from the project's
243
+ * `refrakt.config.json` when not passed explicitly (tests pass fixtures). None
244
+ * of the three has a built-in value: a directory layout is a property of the
245
+ * project, not of the tool. */
246
+ export function runStale(opts) {
247
+ const settings = opts.contentDirs === undefined
248
+ ? resolveStaleSettings(opts.repoRoot)
249
+ : {
250
+ contentDirs: opts.contentDirs,
251
+ archival: opts.archival ?? [],
252
+ generated: opts.generated ?? [],
253
+ };
254
+ const index = buildEdgeIndex({
255
+ repoRoot: opts.repoRoot,
256
+ contentDirs: settings.contentDirs,
257
+ archival: opts.archival ?? settings.archival,
258
+ generated: opts.generated ?? settings.generated,
259
+ });
260
+ const history = scanHistory(opts.repoRoot);
261
+ const result = rankEdges(index, history, { edgeClass: opts.edgeClass, min: opts.min });
262
+ return { ...result, index };
263
+ }
264
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edges/index.ts"],"names":[],"mappings":"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"}
@@ -40,6 +40,31 @@ export interface Frontmatter {
40
40
  /** Last-modified date (ISO 8601), overriding what git history or the
41
41
  * filesystem reports. Read by `timestamps.ts`. */
42
42
  modified?: string;
43
+ /**
44
+ * Repository paths this page is *about* (SPEC-136 D13).
45
+ *
46
+ * The highest-precision edge class `refrakt stale` and `refrakt_stale`
47
+ * read, because there is no extraction heuristic to be wrong: an author who
48
+ * lists three files has chosen three files the page actually describes.
49
+ *
50
+ * It exists because a page's subject is not always in its words — 209 of
51
+ * 237 pages in this repository name no file at all, and no extraction rule
52
+ * reaches them.
53
+ *
54
+ * Unlike a `reviewed` marker, this does not decay: it names what the page
55
+ * is about, which survives every edit to the code it describes. Written
56
+ * once, it keeps producing edges for years.
57
+ *
58
+ * Paths are repo-root-relative POSIX, resolved through the same containment
59
+ * as `snippet path=`. **A declared path that resolves to nothing is an
60
+ * error**, not a skip — the author asserted the relationship, so a miss
61
+ * means the file moved and the declaration did not. Declared edges fail
62
+ * loudly; inferred ones fail quietly.
63
+ *
64
+ * Deliberately does **not** cascade from a `_layout.md`: a layout declaring
65
+ * what it documents would attribute that claim to every page beneath it.
66
+ */
67
+ documents?: string[];
43
68
  /** Arbitrary author fields. Deliberately open: runes and pipeline hooks read
44
69
  * frontmatter keys refrakt itself knows nothing about, so the declared
45
70
  * members above describe what refrakt consumes rather than closing the set. */
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;qDACiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;yDAEqD;IACrD,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;wBAGoB;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC;;;+BAG2B;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IAMtB;;;8BAG0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;+CAC2C;IAC3C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;4CACwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;uDACmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;oFAEgF;IAChF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,WAAW,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAW3F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKtF"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;qDACiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;yDAEqD;IACrD,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;wBAGoB;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC;;;+BAG2B;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IAMtB;;;8BAG0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;+CAC2C;IAC3C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;4CACwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;uDACmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB;;oFAEgF;IAChF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,WAAW,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAW3F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKtF"}