@dmthepm/commune 0.2.0 → 0.3.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/README.md +133 -1
- package/lib/cli/errors.d.ts +3 -2
- package/lib/cli/errors.js +2 -1
- package/lib/cli/main.js +40 -2
- package/lib/cli/query.d.ts +25 -1
- package/lib/cli/query.js +44 -4
- package/lib/cli/update.d.ts +16 -0
- package/lib/cli/update.js +91 -0
- package/lib/cli/usage.d.ts +1 -1
- package/lib/cli/usage.js +19 -5
- package/lib/integration.js +29 -4
- package/lib/lib/dates.d.ts +100 -0
- package/lib/lib/dates.js +182 -0
- package/lib/lib/graph.d.ts +87 -5
- package/lib/lib/graph.js +170 -16
- package/package.json +1 -1
- package/src/components/Updates.astro +114 -0
package/lib/integration.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { copyFile, writeFile, mkdir } from 'node:fs/promises';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { buildGraph, formatDiagnostic, loadContentEntries, toBacklinksJson, toMarkdownPath, } from "./lib/graph.js";
|
|
14
|
+
import { buildGraph, formatDiagnostic, loadContentEntries, summarizeSite, toBacklinksJson, toMarkdownPath, } from "./lib/graph.js";
|
|
15
15
|
function buildBacklinksGraph(entries, logger) {
|
|
16
16
|
const graph = buildGraph(entries);
|
|
17
17
|
logger.info(`📝 Found ${Object.keys(graph.nodes).length} public content entries`);
|
|
@@ -24,9 +24,24 @@ function buildBacklinksGraph(entries, logger) {
|
|
|
24
24
|
}
|
|
25
25
|
return graph;
|
|
26
26
|
}
|
|
27
|
-
async function
|
|
27
|
+
async function writeJsonFile(filePath, value) {
|
|
28
28
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
29
|
-
await writeFile(filePath, JSON.stringify(
|
|
29
|
+
await writeFile(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
30
|
+
}
|
|
31
|
+
async function writeBacklinksFile(filePath, graph) {
|
|
32
|
+
await writeJsonFile(filePath, graph);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The site-wide summary, beside the graph rather than inside it.
|
|
36
|
+
*
|
|
37
|
+
* `backlinks.json` is keyed by urlPath and two of its readers walk it with
|
|
38
|
+
* `Object.entries`, so a top-level `lastUpdated` there would be a malformed
|
|
39
|
+
* node rather than a new field. It is also committed, and this value changes
|
|
40
|
+
* on the same commit that changes it — a committed copy is stale exactly when
|
|
41
|
+
* it matters. So: a sibling file, generated, gitignored.
|
|
42
|
+
*/
|
|
43
|
+
async function writeSiteFile(filePath, summary) {
|
|
44
|
+
await writeJsonFile(filePath, summary);
|
|
30
45
|
}
|
|
31
46
|
/**
|
|
32
47
|
* Copy every entry's source file next to its rendered page, at `<url>.md`.
|
|
@@ -60,6 +75,7 @@ export default function commune(_options = {}) {
|
|
|
60
75
|
// one process do not overwrite each other's roots.
|
|
61
76
|
let root;
|
|
62
77
|
let publicBacklinks;
|
|
78
|
+
let publicSite;
|
|
63
79
|
return {
|
|
64
80
|
name: 'commune-backlinks',
|
|
65
81
|
hooks: {
|
|
@@ -74,9 +90,12 @@ export default function commune(_options = {}) {
|
|
|
74
90
|
// `fileURLToPath` is what Astro's own docs use.
|
|
75
91
|
root = fileURLToPath(config.root);
|
|
76
92
|
publicBacklinks = fileURLToPath(new URL('./backlinks.json', config.publicDir));
|
|
93
|
+
publicSite = fileURLToPath(new URL('./site.json', config.publicDir));
|
|
77
94
|
try {
|
|
78
|
-
const
|
|
95
|
+
const entries = await loadContentEntries({ root });
|
|
96
|
+
const graph = buildBacklinksGraph(entries, logger);
|
|
79
97
|
await writeBacklinksFile(publicBacklinks, toBacklinksJson(graph));
|
|
98
|
+
await writeSiteFile(publicSite, summarizeSite(entries));
|
|
80
99
|
logger.info(`✅ Backlinks index written to ${path.relative(root, publicBacklinks)}`);
|
|
81
100
|
logger.info(summarize(graph));
|
|
82
101
|
}
|
|
@@ -95,10 +114,16 @@ export default function commune(_options = {}) {
|
|
|
95
114
|
await writeBacklinksFile(fileURLToPath(new URL('./backlinks.json', dir)), json);
|
|
96
115
|
// Also written to the public directory, for dev server parity.
|
|
97
116
|
await writeBacklinksFile(publicBacklinks, json);
|
|
117
|
+
const site = summarizeSite(entries);
|
|
118
|
+
await writeSiteFile(fileURLToPath(new URL('./site.json', dir)), site);
|
|
119
|
+
await writeSiteFile(publicSite, site);
|
|
98
120
|
const written = await writeMarkdownFiles(entries, root, fileURLToPath(dir));
|
|
99
121
|
logger.info(`✅ Backlinks index written to /backlinks.json (dist + public)`);
|
|
100
122
|
logger.info(`📄 ${written} source files written as .md alongside their pages`);
|
|
101
123
|
logger.info(summarize(graph));
|
|
124
|
+
logger.info(site.lastUpdated
|
|
125
|
+
? `🕒 Site last updated ${site.lastUpdated} (${site.lastUpdatedPath}, from ${site.lastUpdatedSource})`
|
|
126
|
+
: '🕒 No entry carries a date, so site.json has no lastUpdated');
|
|
102
127
|
}
|
|
103
128
|
catch (error) {
|
|
104
129
|
logger.error('❌ Failed to build backlinks index:');
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a content entry's dates come from when nobody wrote them down.
|
|
3
|
+
*
|
|
4
|
+
* `updated:` in frontmatter is a hand-maintained field, and a hand-maintained
|
|
5
|
+
* field is only true while somebody remembers to maintain it. The repository
|
|
6
|
+
* already knows the answer — a file's last commit *is* the day it last
|
|
7
|
+
* changed — so this module reads it from there and the graph prefers
|
|
8
|
+
* frontmatter only when frontmatter exists.
|
|
9
|
+
*
|
|
10
|
+
* Three rules the rest of the engine depends on:
|
|
11
|
+
*
|
|
12
|
+
* - **One walk, not one process per file.** `git log --name-only` over the
|
|
13
|
+
* content directories yields every file's whole history in a single
|
|
14
|
+
* child process. A vault has hundreds of notes; spawning `git` for each
|
|
15
|
+
* one turns a build into a minute of process creation.
|
|
16
|
+
* - **A shallow clone has no dates to give.** `actions/checkout` fetches
|
|
17
|
+
* depth 1 by default, and in that tree every file's "first and last
|
|
18
|
+
* commit" is the same single commit — the day of the build. That is not a
|
|
19
|
+
* worse date, it is a false one, and it would be false on every entry at
|
|
20
|
+
* once. So a shallow checkout produces no derived dates at all, and says
|
|
21
|
+
* so once on stderr.
|
|
22
|
+
* - **Never fatal, and never a guess dressed as a fact.** A tree that is not
|
|
23
|
+
* in a repository at all falls back to file mtimes. A tree that *is* in one
|
|
24
|
+
* never does: inside a checkout an mtime is the day the files were written
|
|
25
|
+
* to disk, which on CI is the day of the build and on a fresh clone is
|
|
26
|
+
* today, so it would launder the same falsehood the shallow rule exists to
|
|
27
|
+
* refuse.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* How an entry's `updated` was decided, so a site can show the honest one.
|
|
31
|
+
*
|
|
32
|
+
* `frontmatter` is an author's claim, `git` is the repository's record,
|
|
33
|
+
* `mtime` is the filesystem's guess in a directory with no repository around
|
|
34
|
+
* it, and `none` means nothing could answer — an uncommitted file, or any file
|
|
35
|
+
* in a shallow checkout.
|
|
36
|
+
*/
|
|
37
|
+
export type DateSource = 'frontmatter' | 'git' | 'mtime' | 'none';
|
|
38
|
+
/** The first and last commit dates of one file, as `yyyy-mm-dd`. */
|
|
39
|
+
export interface FileHistory {
|
|
40
|
+
/** Date of the file's most recent commit. */
|
|
41
|
+
updated: string;
|
|
42
|
+
/** Date of the file's oldest commit. */
|
|
43
|
+
created: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What the project's history can and cannot answer.
|
|
47
|
+
*
|
|
48
|
+
* Three states, because the two failures are not the same failure and the
|
|
49
|
+
* graph has to treat them differently. `unversioned` means no repository, and
|
|
50
|
+
* an mtime is the best honest answer available. `shallow` means a repository
|
|
51
|
+
* whose history was truncated, where both a commit date and an mtime would say
|
|
52
|
+
* "today" about every file at once.
|
|
53
|
+
*/
|
|
54
|
+
export type HistoryKind = 'history' | 'shallow' | 'unversioned';
|
|
55
|
+
export interface ContentHistory {
|
|
56
|
+
kind: HistoryKind;
|
|
57
|
+
/** Keyed by root-relative file path. Empty unless `kind` is `history`. */
|
|
58
|
+
files: Map<string, FileHistory>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Said once, on stderr, when a build or a query runs in a truncated checkout.
|
|
62
|
+
*
|
|
63
|
+
* On stderr rather than in `Graph.diagnostics` because it is not a finding
|
|
64
|
+
* about anyone's content — it is a fact about the environment the command ran
|
|
65
|
+
* in, and the person who needs it is reading a build log. stderr also keeps it
|
|
66
|
+
* out of the way of `--json`, whose stdout carries one document and nothing
|
|
67
|
+
* else.
|
|
68
|
+
*/
|
|
69
|
+
export declare const SHALLOW_WARNING: string;
|
|
70
|
+
/** Test seam: forget which roots have been warned about. */
|
|
71
|
+
export declare function resetShallowWarnings(): void;
|
|
72
|
+
/** A local calendar day as `yyyy-mm-dd`.
|
|
73
|
+
*
|
|
74
|
+
* The one spelling of "what day is it" in the engine. Local rather than UTC on
|
|
75
|
+
* purpose, and shared rather than reimplemented: `git log --date=short` renders
|
|
76
|
+
* a commit in its own recorded timezone — the committer's local day — so an
|
|
77
|
+
* mtime, a `--recent 7d` cutoff and the date a scaffolded update is filed under
|
|
78
|
+
* all have to be local days too, or the four would disagree for the hours
|
|
79
|
+
* either side of midnight.
|
|
80
|
+
*/
|
|
81
|
+
export declare function toIsoDay(date: Date): string;
|
|
82
|
+
/**
|
|
83
|
+
* Every content file's first and last commit date, from one `git log` walk.
|
|
84
|
+
*
|
|
85
|
+
* `--relative` (with the child process's cwd set to `root`) is what keeps the
|
|
86
|
+
* returned paths in the same spelling `ContentEntry.file` uses: a project root
|
|
87
|
+
* nested inside a larger repository would otherwise get repository-relative
|
|
88
|
+
* paths back and match nothing. `core.quotePath=false` keeps non-ASCII
|
|
89
|
+
* filenames literal — notes are named for their titles, and titles have
|
|
90
|
+
* accents.
|
|
91
|
+
*
|
|
92
|
+
* `%D` rides along on each commit line as the second belt: `rev-parse` has
|
|
93
|
+
* already answered whether the repository is shallow, and a walk that reaches
|
|
94
|
+
* the grafted boundary anyway says the same thing from the other direction.
|
|
95
|
+
* Either one means the same commit stands in for every commit before it, so
|
|
96
|
+
* neither the dates nor a first-commit `created` can be trusted.
|
|
97
|
+
*/
|
|
98
|
+
export declare function readContentHistory(root: string, dirs: string[]): Promise<ContentHistory>;
|
|
99
|
+
/** A file's modification time as a local `yyyy-mm-dd`, or `undefined` if unreadable. */
|
|
100
|
+
export declare function readMtimeDate(root: string, file: string): Promise<string | undefined>;
|
package/lib/lib/dates.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a content entry's dates come from when nobody wrote them down.
|
|
3
|
+
*
|
|
4
|
+
* `updated:` in frontmatter is a hand-maintained field, and a hand-maintained
|
|
5
|
+
* field is only true while somebody remembers to maintain it. The repository
|
|
6
|
+
* already knows the answer — a file's last commit *is* the day it last
|
|
7
|
+
* changed — so this module reads it from there and the graph prefers
|
|
8
|
+
* frontmatter only when frontmatter exists.
|
|
9
|
+
*
|
|
10
|
+
* Three rules the rest of the engine depends on:
|
|
11
|
+
*
|
|
12
|
+
* - **One walk, not one process per file.** `git log --name-only` over the
|
|
13
|
+
* content directories yields every file's whole history in a single
|
|
14
|
+
* child process. A vault has hundreds of notes; spawning `git` for each
|
|
15
|
+
* one turns a build into a minute of process creation.
|
|
16
|
+
* - **A shallow clone has no dates to give.** `actions/checkout` fetches
|
|
17
|
+
* depth 1 by default, and in that tree every file's "first and last
|
|
18
|
+
* commit" is the same single commit — the day of the build. That is not a
|
|
19
|
+
* worse date, it is a false one, and it would be false on every entry at
|
|
20
|
+
* once. So a shallow checkout produces no derived dates at all, and says
|
|
21
|
+
* so once on stderr.
|
|
22
|
+
* - **Never fatal, and never a guess dressed as a fact.** A tree that is not
|
|
23
|
+
* in a repository at all falls back to file mtimes. A tree that *is* in one
|
|
24
|
+
* never does: inside a checkout an mtime is the day the files were written
|
|
25
|
+
* to disk, which on CI is the day of the build and on a fresh clone is
|
|
26
|
+
* today, so it would launder the same falsehood the shallow rule exists to
|
|
27
|
+
* refuse.
|
|
28
|
+
*/
|
|
29
|
+
import { execFile } from 'node:child_process';
|
|
30
|
+
import { stat } from 'node:fs/promises';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { promisify } from 'node:util';
|
|
33
|
+
const run = promisify(execFile);
|
|
34
|
+
/**
|
|
35
|
+
* Said once, on stderr, when a build or a query runs in a truncated checkout.
|
|
36
|
+
*
|
|
37
|
+
* On stderr rather than in `Graph.diagnostics` because it is not a finding
|
|
38
|
+
* about anyone's content — it is a fact about the environment the command ran
|
|
39
|
+
* in, and the person who needs it is reading a build log. stderr also keeps it
|
|
40
|
+
* out of the way of `--json`, whose stdout carries one document and nothing
|
|
41
|
+
* else.
|
|
42
|
+
*/
|
|
43
|
+
export const SHALLOW_WARNING = 'git history is shallow: dates come from frontmatter only. ' +
|
|
44
|
+
'Fetch full history (fetch-depth: 0 / unshallow) to derive dates from commits.';
|
|
45
|
+
/** One warning per project root per process, however many times the graph is loaded. */
|
|
46
|
+
const warned = new Set();
|
|
47
|
+
function warnShallow(root) {
|
|
48
|
+
const key = path.resolve(root);
|
|
49
|
+
if (warned.has(key))
|
|
50
|
+
return;
|
|
51
|
+
warned.add(key);
|
|
52
|
+
process.stderr.write(`${SHALLOW_WARNING}\n`);
|
|
53
|
+
}
|
|
54
|
+
/** Test seam: forget which roots have been warned about. */
|
|
55
|
+
export function resetShallowWarnings() {
|
|
56
|
+
warned.clear();
|
|
57
|
+
}
|
|
58
|
+
/** A local calendar day as `yyyy-mm-dd`.
|
|
59
|
+
*
|
|
60
|
+
* The one spelling of "what day is it" in the engine. Local rather than UTC on
|
|
61
|
+
* purpose, and shared rather than reimplemented: `git log --date=short` renders
|
|
62
|
+
* a commit in its own recorded timezone — the committer's local day — so an
|
|
63
|
+
* mtime, a `--recent 7d` cutoff and the date a scaffolded update is filed under
|
|
64
|
+
* all have to be local days too, or the four would disagree for the hours
|
|
65
|
+
* either side of midnight.
|
|
66
|
+
*/
|
|
67
|
+
export function toIsoDay(date) {
|
|
68
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
69
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
70
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
71
|
+
}
|
|
72
|
+
/** Is this root a shallow checkout, a full one, or not a repository at all? */
|
|
73
|
+
async function classify(root) {
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await run('git', ['rev-parse', '--is-shallow-repository'], { cwd: root });
|
|
76
|
+
return stdout.trim() === 'true' ? 'shallow' : 'history';
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Not a repository, or no `git` on the PATH. The flag itself is not in
|
|
80
|
+
// question: it has shipped since git 2.15 (2017).
|
|
81
|
+
return 'unversioned';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Every content file's first and last commit date, from one `git log` walk.
|
|
86
|
+
*
|
|
87
|
+
* `--relative` (with the child process's cwd set to `root`) is what keeps the
|
|
88
|
+
* returned paths in the same spelling `ContentEntry.file` uses: a project root
|
|
89
|
+
* nested inside a larger repository would otherwise get repository-relative
|
|
90
|
+
* paths back and match nothing. `core.quotePath=false` keeps non-ASCII
|
|
91
|
+
* filenames literal — notes are named for their titles, and titles have
|
|
92
|
+
* accents.
|
|
93
|
+
*
|
|
94
|
+
* `%D` rides along on each commit line as the second belt: `rev-parse` has
|
|
95
|
+
* already answered whether the repository is shallow, and a walk that reaches
|
|
96
|
+
* the grafted boundary anyway says the same thing from the other direction.
|
|
97
|
+
* Either one means the same commit stands in for every commit before it, so
|
|
98
|
+
* neither the dates nor a first-commit `created` can be trusted.
|
|
99
|
+
*/
|
|
100
|
+
export async function readContentHistory(root, dirs) {
|
|
101
|
+
const kind = await classify(root);
|
|
102
|
+
if (kind !== 'history') {
|
|
103
|
+
if (kind === 'shallow')
|
|
104
|
+
warnShallow(root);
|
|
105
|
+
return { kind, files: new Map() };
|
|
106
|
+
}
|
|
107
|
+
let stdout;
|
|
108
|
+
try {
|
|
109
|
+
({ stdout } = await run('git', [
|
|
110
|
+
'-c',
|
|
111
|
+
'core.quotePath=false',
|
|
112
|
+
'log',
|
|
113
|
+
// A NUL before each commit, then its date and its decoration: no
|
|
114
|
+
// line of this can be confused with a filename, whatever a file is
|
|
115
|
+
// called.
|
|
116
|
+
'--format=%x00%cd%x09%D',
|
|
117
|
+
'--date=short',
|
|
118
|
+
'--name-only',
|
|
119
|
+
'--relative',
|
|
120
|
+
'--',
|
|
121
|
+
...dirs,
|
|
122
|
+
], { cwd: root, maxBuffer: 64 * 1024 * 1024 }));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// A repository the walk cannot read — most often one with no commits yet.
|
|
126
|
+
// Still a repository, so still not a place to reach for an mtime: it has
|
|
127
|
+
// nothing to say, and `none` is what "nothing to say" looks like.
|
|
128
|
+
return { kind: 'history', files: new Map() };
|
|
129
|
+
}
|
|
130
|
+
const files = new Map();
|
|
131
|
+
// `git log` walks newest first, so the first sighting of a file is its last
|
|
132
|
+
// commit and every later sighting is an older one.
|
|
133
|
+
for (const commit of stdout.split('\u0000')) {
|
|
134
|
+
const [header, ...lines] = commit.split('\n');
|
|
135
|
+
const [date, decoration = ''] = header.split('\t');
|
|
136
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
|
|
137
|
+
continue;
|
|
138
|
+
if (decoration.includes('grafted')) {
|
|
139
|
+
warnShallow(root);
|
|
140
|
+
return { kind: 'shallow', files: new Map() };
|
|
141
|
+
}
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
if (!line)
|
|
144
|
+
continue;
|
|
145
|
+
const file = unquotePath(line);
|
|
146
|
+
const seen = files.get(file);
|
|
147
|
+
if (seen)
|
|
148
|
+
seen.created = date;
|
|
149
|
+
else
|
|
150
|
+
files.set(file, { updated: date, created: date });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { kind: 'history', files };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Undo git's C-style quoting of a path.
|
|
157
|
+
*
|
|
158
|
+
* With `core.quotePath=false` only paths containing a quote, a backslash or a
|
|
159
|
+
* control character are quoted at all, and JSON's escapes cover those. A path
|
|
160
|
+
* git quoted some other way is returned as git printed it: a filename that odd
|
|
161
|
+
* is better shown verbatim than silently mangled by a guess.
|
|
162
|
+
*/
|
|
163
|
+
function unquotePath(line) {
|
|
164
|
+
if (!line.startsWith('"'))
|
|
165
|
+
return line;
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(line);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return line;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** A file's modification time as a local `yyyy-mm-dd`, or `undefined` if unreadable. */
|
|
174
|
+
export async function readMtimeDate(root, file) {
|
|
175
|
+
try {
|
|
176
|
+
const stats = await stat(path.join(root, file));
|
|
177
|
+
return toIsoDay(stats.mtime);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/lib/lib/graph.d.ts
CHANGED
|
@@ -8,18 +8,27 @@
|
|
|
8
8
|
* rules, and three subtly different opinions about trailing slashes.
|
|
9
9
|
*
|
|
10
10
|
* The rules this module owns:
|
|
11
|
-
* - which collections participate (notes, research, pages)
|
|
12
|
-
* - visibility (notes opt in with `visibility: public`; research and
|
|
13
|
-
* are always public)
|
|
11
|
+
* - which collections participate (notes, research, pages, updates)
|
|
12
|
+
* - visibility (notes opt in with `visibility: public`; research, pages and
|
|
13
|
+
* updates are always public)
|
|
14
14
|
* - canonical URLs, always with a trailing slash, matching Astro's
|
|
15
15
|
* directory build format
|
|
16
16
|
* - the title/alias lookup used to resolve `[[WikiLinks]]`
|
|
17
17
|
* - which link forms count as an edge, and how each one resolves
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
import { type DateSource } from './dates.ts';
|
|
20
|
+
export type { ContentHistory, DateSource, HistoryKind } from './dates.ts';
|
|
21
|
+
export { SHALLOW_WARNING, toIsoDay } from './dates.ts';
|
|
22
|
+
export type CollectionName = 'notes' | 'research' | 'pages' | 'updates';
|
|
20
23
|
/** Where each collection's markdown lives, relative to the project root. */
|
|
21
24
|
export declare const CONTENT_DIRS: Record<CollectionName, string>;
|
|
22
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Collection scan order. Stable so derived artifacts are deterministic.
|
|
27
|
+
*
|
|
28
|
+
* `updates` is last because it was added last, and the order is the order
|
|
29
|
+
* `backlinks.json` is keyed in: putting it anywhere else would rewrite the
|
|
30
|
+
* whole committed artifact to say the same thing.
|
|
31
|
+
*/
|
|
23
32
|
export declare const COLLECTIONS: CollectionName[];
|
|
24
33
|
/** One piece of public content, as the graph sees it. */
|
|
25
34
|
export interface ContentEntry {
|
|
@@ -33,7 +42,28 @@ export interface ContentEntry {
|
|
|
33
42
|
tags: string[];
|
|
34
43
|
status: string;
|
|
35
44
|
summary?: string;
|
|
45
|
+
/**
|
|
46
|
+
* When this entry last changed, by the best answer available.
|
|
47
|
+
*
|
|
48
|
+
* Frontmatter wins when an author wrote one down; otherwise the file's
|
|
49
|
+
* last commit date, and in a tree with no history its mtime. `updatedSource`
|
|
50
|
+
* says which of those this is, so a site can decide whether to show it.
|
|
51
|
+
*/
|
|
36
52
|
updated?: string;
|
|
53
|
+
/** Which of the three answers `updated` is. */
|
|
54
|
+
updatedSource: DateSource;
|
|
55
|
+
/** When this entry first appeared, by the same precedence as `updated`. */
|
|
56
|
+
created?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The date of the file's last commit, whatever `updated` ended up being.
|
|
59
|
+
*
|
|
60
|
+
* Carried separately so a site can show both — "updated 2026-01-21, last
|
|
61
|
+
* touched 2026-09-03" is the sentence that catches a stale `updated:` field,
|
|
62
|
+
* and it cannot be written if the two dates have already been collapsed
|
|
63
|
+
* into one. Absent when the project is not a git checkout, or when the file
|
|
64
|
+
* has never been committed.
|
|
65
|
+
*/
|
|
66
|
+
modifiedInGit?: string;
|
|
37
67
|
/** Markdown body with frontmatter stripped. */
|
|
38
68
|
body: string;
|
|
39
69
|
/** Parsed frontmatter. Kept because links can live in it. */
|
|
@@ -93,6 +123,15 @@ export declare function toMarkdownPath(urlPath: string): string;
|
|
|
93
123
|
export declare function toMarkdownHref(urlPath: string): string;
|
|
94
124
|
/** Coerce a frontmatter date to `yyyy-mm-dd`, so build output has no timestamp drift. */
|
|
95
125
|
export declare function normalizeDate(value: unknown): string | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* The date an author wrote down, if they wrote one down.
|
|
128
|
+
*
|
|
129
|
+
* `updated` first, then `date` — the key the changelog-shaped collections use,
|
|
130
|
+
* where the entry *is* a dated thing rather than a page that happens to have
|
|
131
|
+
* been edited. Kept as one function so the graph, `--recent` and the site-wide
|
|
132
|
+
* last-updated value can never disagree about what an author claimed.
|
|
133
|
+
*/
|
|
134
|
+
export declare function claimedDate(data: Record<string, unknown>): string | undefined;
|
|
96
135
|
/** Where to read content from. */
|
|
97
136
|
export interface GraphOptions {
|
|
98
137
|
/**
|
|
@@ -251,6 +290,7 @@ export interface NoteMetadata {
|
|
|
251
290
|
tags: string[];
|
|
252
291
|
status: string;
|
|
253
292
|
summary?: string;
|
|
293
|
+
/** The author's claimed date. Not the resolved one — see `buildGraph`. */
|
|
254
294
|
updated?: string;
|
|
255
295
|
isStarred?: boolean;
|
|
256
296
|
}
|
|
@@ -311,6 +351,48 @@ export declare function buildGraph(entries: ContentEntry[]): Graph;
|
|
|
311
351
|
* as it was when the integration formatted it itself.
|
|
312
352
|
*/
|
|
313
353
|
export declare function formatDiagnostic(diagnostic: Diagnostic): string;
|
|
354
|
+
/**
|
|
355
|
+
* What the whole site says about itself, as one small object.
|
|
356
|
+
*
|
|
357
|
+
* Written to `site.json` beside `backlinks.json`, and deliberately not *into*
|
|
358
|
+
* it. Two reasons, and either alone would settle it:
|
|
359
|
+
*
|
|
360
|
+
* - Every top-level key of `backlinks.json` is a urlPath. Two of its readers
|
|
361
|
+
* walk it with `Object.entries` and treat what they find as a node, so a
|
|
362
|
+
* `lastUpdated` string at the top level is not a new field — it is a
|
|
363
|
+
* malformed entry in a runtime contract.
|
|
364
|
+
* - `backlinks.json` is committed, and this value moves with history: it
|
|
365
|
+
* changes on the commit that changes it, so a committed copy is stale the
|
|
366
|
+
* moment it is right. `site.json` is generated and ignored, like `dist/`.
|
|
367
|
+
*/
|
|
368
|
+
export interface SiteSummary {
|
|
369
|
+
/** The newest `updated` across every public entry. Absent if nothing has a date. */
|
|
370
|
+
lastUpdated?: string;
|
|
371
|
+
/** The entry that date belongs to, so a page can link to what changed. */
|
|
372
|
+
lastUpdatedPath?: string;
|
|
373
|
+
/** Where that date came from — an author's claim, git, or an mtime. */
|
|
374
|
+
lastUpdatedSource?: DateSource;
|
|
375
|
+
/**
|
|
376
|
+
* The newest commit date across every entry, whatever the entries claim.
|
|
377
|
+
*
|
|
378
|
+
* `lastUpdated` obeys frontmatter, because an author who writes a date down
|
|
379
|
+
* means it. This does not, and the pair is the point: when they disagree,
|
|
380
|
+
* the wiki changed on a day nobody wrote down, and a site can say so
|
|
381
|
+
* instead of quietly showing the older number.
|
|
382
|
+
*/
|
|
383
|
+
lastModifiedInGit?: string;
|
|
384
|
+
/** How many public entries the site has. */
|
|
385
|
+
entries: number;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The newest change across the whole site, and which entry it was.
|
|
389
|
+
*
|
|
390
|
+
* "When did this wiki last change" is not the same question as "when did this
|
|
391
|
+
* page last change", and a home page that answers the second while appearing
|
|
392
|
+
* to answer the first is the bug this ticket opened on. Ties go to the entry
|
|
393
|
+
* scanned first, which is stable because the scan is.
|
|
394
|
+
*/
|
|
395
|
+
export declare function summarizeSite(entries: ContentEntry[]): SiteSummary;
|
|
314
396
|
/**
|
|
315
397
|
* The public artifact, exactly as `public/backlinks.json` stores it.
|
|
316
398
|
*
|