daftari 1.12.5 → 1.13.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 +38 -0
- package/README.md +55 -0
- package/dist/audit/checks/broken_refs.d.ts +3 -0
- package/dist/audit/checks/broken_refs.d.ts.map +1 -0
- package/dist/audit/checks/broken_refs.js +67 -0
- package/dist/audit/checks/broken_refs.js.map +1 -0
- package/dist/audit/checks/staleness.d.ts +3 -0
- package/dist/audit/checks/staleness.d.ts.map +1 -0
- package/dist/audit/checks/staleness.js +114 -0
- package/dist/audit/checks/staleness.js.map +1 -0
- package/dist/audit/collect.d.ts +4 -0
- package/dist/audit/collect.d.ts.map +1 -0
- package/dist/audit/collect.js +134 -0
- package/dist/audit/collect.js.map +1 -0
- package/dist/audit/config.d.ts +4 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +174 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/exit.d.ts +6 -0
- package/dist/audit/exit.d.ts.map +1 -0
- package/dist/audit/exit.js +8 -0
- package/dist/audit/exit.js.map +1 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +93 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/links.d.ts +4 -0
- package/dist/audit/links.d.ts.map +1 -0
- package/dist/audit/links.js +156 -0
- package/dist/audit/links.js.map +1 -0
- package/dist/audit/report.d.ts +4 -0
- package/dist/audit/report.d.ts.map +1 -0
- package/dist/audit/report.js +58 -0
- package/dist/audit/report.js.map +1 -0
- package/dist/audit/types.d.ts +94 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +11 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.13.0] - 2026-05-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `daftari audit` CLI subcommand. Scans N markdown repos and reports broken
|
|
15
|
+
cross-repo references and link-graph transitive staleness. Outputs markdown
|
|
16
|
+
(default: stdout) and optional JSON. Exit code 1 if `fail_on.broken_refs` or
|
|
17
|
+
`fail_on.transitive_staleness` thresholds are exceeded. Anonymous repos passed
|
|
18
|
+
via `--repo` get no URL patterns — URL-based cross-refs into them aren't
|
|
19
|
+
detected; use `--config` with an `urls:` block to enable them. See issue #85.
|
|
20
|
+
|
|
21
|
+
## [1.12.6] - 2026-05-27
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **`manifest.json` `description` and `long_description` rewritten to
|
|
26
|
+
lead with the cortex framing.** Brings the `.mcpb` install UI (which
|
|
27
|
+
Claude Desktop shows when a user installs the extension) into sync
|
|
28
|
+
with the Anthropic Connectors Directory listing copy. Previously,
|
|
29
|
+
the listing leads with "an external cortex for AI agents…" while the
|
|
30
|
+
install UI led with "an MCP server that exposes a curated markdown
|
|
31
|
+
vault" — same facts, different framing. Same product describing
|
|
32
|
+
itself two ways was a coherence cost worth paying down.
|
|
33
|
+
|
|
34
|
+
- `description` is now the tagline ("A persistent cortex Claude
|
|
35
|
+
reads, writes, and curates over time.") instead of the older
|
|
36
|
+
knowledge-vault opener.
|
|
37
|
+
- `long_description` is the 47-word cortex-led version used in the
|
|
38
|
+
directory listing form (which caps at 50 words). Trims the
|
|
39
|
+
`OPENAI_API_KEY` env-var hint and the `embeddings.provider:
|
|
40
|
+
openai-3-small` config path from the long copy — both still live
|
|
41
|
+
in `PRIVACY.md` and the README for anyone wiring up the OpenAI
|
|
42
|
+
embedding provider.
|
|
43
|
+
|
|
44
|
+
No functional change. The `.mcpb` artifact is repacked from this
|
|
45
|
+
commit so the bundled manifest matches what's submitted to the
|
|
46
|
+
directory.
|
|
47
|
+
|
|
10
48
|
## [1.12.5] - 2026-05-26
|
|
11
49
|
|
|
12
50
|
### Changed
|
package/README.md
CHANGED
|
@@ -164,6 +164,54 @@ Deliberately deferred to keep the surface tight:
|
|
|
164
164
|
|
|
165
165
|
Each is a clean increment on a surface that already works.
|
|
166
166
|
|
|
167
|
+
## Coherence audit
|
|
168
|
+
|
|
169
|
+
`daftari audit` runs a read-only, deterministic check across one or more
|
|
170
|
+
markdown repos for broken cross-repo references and link-graph transitive
|
|
171
|
+
staleness. It does **not** create a `.daftari/` vault on disk and does not
|
|
172
|
+
write to the audited repos.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Anonymous repos (no URL patterns):
|
|
176
|
+
daftari audit --repo ~/repos/service-a --repo ~/repos/service-b
|
|
177
|
+
|
|
178
|
+
# Or with a config file (recommended; see daftari audit --help for the schema):
|
|
179
|
+
daftari audit --config audit.yaml
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Anonymous repos passed via `--repo` do not get URL patterns, so cross-repo
|
|
183
|
+
references that take the form of GitHub URLs (e.g.
|
|
184
|
+
`https://github.com/org/service-a/blob/main/docs/api.md`) into them will be
|
|
185
|
+
silently treated as external. Declare repos in `audit.yaml` with their `urls`
|
|
186
|
+
field to detect these.
|
|
187
|
+
|
|
188
|
+
`audit.yaml` schema:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
repos:
|
|
192
|
+
- name: service-a
|
|
193
|
+
path: ~/repos/service-a
|
|
194
|
+
docs_glob: "docs/**/*.md" # default: "**/*.md"
|
|
195
|
+
urls: # optional; enables URL-pattern matching
|
|
196
|
+
- "github.com/org/service-a"
|
|
197
|
+
|
|
198
|
+
- name: service-b
|
|
199
|
+
path: ~/repos/service-b
|
|
200
|
+
urls:
|
|
201
|
+
- "github.com/org/service-b"
|
|
202
|
+
|
|
203
|
+
output:
|
|
204
|
+
markdown: coherence-report.md # default: stdout
|
|
205
|
+
json: coherence-report.json # default: not emitted
|
|
206
|
+
|
|
207
|
+
staleness:
|
|
208
|
+
threshold_days: 540 # default: 540 (18 months)
|
|
209
|
+
|
|
210
|
+
fail_on:
|
|
211
|
+
broken_refs: 1 # default: fail on any broken ref
|
|
212
|
+
transitive_staleness: 100 # default: generous; teams tune
|
|
213
|
+
```
|
|
214
|
+
|
|
167
215
|
## Development
|
|
168
216
|
|
|
169
217
|
```
|
|
@@ -181,6 +229,13 @@ Design tenets: functions and types, no classes; tool handlers return
|
|
|
181
229
|
- <docs/architecture.md> — layered design, request path, accumulation vs. generative domains
|
|
182
230
|
- <docs/file-format.md> — complete frontmatter reference
|
|
183
231
|
|
|
232
|
+
## Integrations
|
|
233
|
+
|
|
234
|
+
- [`integrations/langchain/`](integrations/langchain/) — `langchain-daftari`, a
|
|
235
|
+
Python package that exposes the 14 daftari tools as LangChain `BaseTool`s
|
|
236
|
+
for use with LangGraph / `create_react_agent`. Sync + async, schemas pulled
|
|
237
|
+
live from `tools/list`.
|
|
238
|
+
|
|
184
239
|
## Privacy
|
|
185
240
|
|
|
186
241
|
Daftari is a local MCP server. It runs on your machine, against vault files on
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broken_refs.d.ts","sourceRoot":"","sources":["../../../src/audit/checks/broken_refs.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE5E,wBAAgB,eAAe,CAAC,SAAS,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,gBAAgB,EAAE,CA0DhG"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/audit/checks/broken_refs.ts
|
|
2
|
+
// Pure function: given snapshots and classified edges, return every broken
|
|
3
|
+
// reference as a BrokenRefFinding. No throws. No classes.
|
|
4
|
+
//
|
|
5
|
+
// Resolution logic:
|
|
6
|
+
// 1. Look up target repo by name; if not found → missing_file.
|
|
7
|
+
// 2. Look up targetPath in that repo's docs Map.
|
|
8
|
+
// 3. If not found and targetPath lacks a ".md" extension, try targetPath +
|
|
9
|
+
// ".md" (spec §7 bare-path fallback).
|
|
10
|
+
// 4. If still not found → missing_file.
|
|
11
|
+
// 5. If found and targetAnchor is set: check DocSnapshot.headings. If absent
|
|
12
|
+
// → missing_anchor.
|
|
13
|
+
export function checkBrokenRefs(snapshots, edges) {
|
|
14
|
+
// Index snapshots by repo name for O(1) lookup.
|
|
15
|
+
const byRepo = new Map();
|
|
16
|
+
for (const snap of snapshots) {
|
|
17
|
+
byRepo.set(snap.config.name, snap);
|
|
18
|
+
}
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const edge of edges) {
|
|
21
|
+
const targetSnap = byRepo.get(edge.targetRepo);
|
|
22
|
+
if (!targetSnap) {
|
|
23
|
+
// Target repo itself is unknown — treat as missing file.
|
|
24
|
+
findings.push({
|
|
25
|
+
kind: "missing_file",
|
|
26
|
+
source: { repo: edge.sourceRepo, path: edge.sourcePath },
|
|
27
|
+
target: { repo: edge.targetRepo, path: edge.targetPath, anchor: edge.targetAnchor },
|
|
28
|
+
rawHref: edge.rawHref,
|
|
29
|
+
});
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Resolve the target doc: exact match, then .md fallback.
|
|
33
|
+
let resolvedPath = null;
|
|
34
|
+
if (targetSnap.docs.has(edge.targetPath)) {
|
|
35
|
+
resolvedPath = edge.targetPath;
|
|
36
|
+
}
|
|
37
|
+
else if (!edge.targetPath.endsWith(".md")) {
|
|
38
|
+
const withMd = `${edge.targetPath}.md`;
|
|
39
|
+
if (targetSnap.docs.has(withMd)) {
|
|
40
|
+
resolvedPath = withMd;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (resolvedPath === null) {
|
|
44
|
+
findings.push({
|
|
45
|
+
kind: "missing_file",
|
|
46
|
+
source: { repo: edge.sourceRepo, path: edge.sourcePath },
|
|
47
|
+
target: { repo: edge.targetRepo, path: edge.targetPath, anchor: edge.targetAnchor },
|
|
48
|
+
rawHref: edge.rawHref,
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// File found. Check anchor if present.
|
|
53
|
+
if (edge.targetAnchor !== null) {
|
|
54
|
+
const doc = targetSnap.docs.get(resolvedPath);
|
|
55
|
+
if (doc && !doc.headings.has(edge.targetAnchor)) {
|
|
56
|
+
findings.push({
|
|
57
|
+
kind: "missing_anchor",
|
|
58
|
+
source: { repo: edge.sourceRepo, path: edge.sourcePath },
|
|
59
|
+
target: { repo: edge.targetRepo, path: resolvedPath, anchor: edge.targetAnchor },
|
|
60
|
+
rawHref: edge.rawHref,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=broken_refs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broken_refs.js","sourceRoot":"","sources":["../../../src/audit/checks/broken_refs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staleness.d.ts","sourceRoot":"","sources":["../../../src/audit/checks/staleness.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAY5E,wBAAgB,cAAc,CAC5B,SAAS,EAAE,YAAY,EAAE,EACzB,KAAK,EAAE,QAAQ,EAAE,EACjB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,IAAI,GACR,gBAAgB,EAAE,CAsGpB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/audit/checks/staleness.ts
|
|
2
|
+
// Memoized BFS from each fresh doc to the nearest directly-stale leaf. BFS
|
|
3
|
+
// (not DFS) so the recorded chain is the shortest path. Cycles handled by
|
|
4
|
+
// the BFS visited set; verdicts memoized across roots.
|
|
5
|
+
const key = (repo, path) => `${repo} ${path}`;
|
|
6
|
+
function isDirectlyStale(mtimeIso, now, thresholdDays) {
|
|
7
|
+
const ms = now.getTime() - new Date(mtimeIso).getTime();
|
|
8
|
+
return ms > thresholdDays * 86_400_000;
|
|
9
|
+
}
|
|
10
|
+
export function checkStaleness(snapshots, edges, thresholdDays, now) {
|
|
11
|
+
const nodes = new Map();
|
|
12
|
+
for (const snap of snapshots) {
|
|
13
|
+
for (const d of snap.docs.values()) {
|
|
14
|
+
nodes.set(key(snap.config.name, d.relPath), {
|
|
15
|
+
repo: snap.config.name,
|
|
16
|
+
path: d.relPath,
|
|
17
|
+
mtime: d.mtime,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const adj = new Map();
|
|
22
|
+
for (const edge of edges) {
|
|
23
|
+
const src = key(edge.sourceRepo, edge.sourcePath);
|
|
24
|
+
const dst = key(edge.targetRepo, edge.targetPath);
|
|
25
|
+
// Resolve the dst against the .md-extension fallback so transitive edges
|
|
26
|
+
// see the same node identity broken-refs check does.
|
|
27
|
+
let dstKey = dst;
|
|
28
|
+
if (!nodes.has(dstKey)) {
|
|
29
|
+
const alt = key(edge.targetRepo, `${edge.targetPath}.md`);
|
|
30
|
+
if (nodes.has(alt))
|
|
31
|
+
dstKey = alt;
|
|
32
|
+
}
|
|
33
|
+
if (!nodes.has(dstKey))
|
|
34
|
+
continue; // dangling edge — broken_refs handles it
|
|
35
|
+
if (!nodes.has(src))
|
|
36
|
+
continue;
|
|
37
|
+
const list = adj.get(src) ?? [];
|
|
38
|
+
list.push(dstKey);
|
|
39
|
+
adj.set(src, list);
|
|
40
|
+
}
|
|
41
|
+
// Classify directly stale leaves.
|
|
42
|
+
const direct = new Set();
|
|
43
|
+
for (const [k, info] of nodes) {
|
|
44
|
+
if (isDirectlyStale(info.mtime, now, thresholdDays))
|
|
45
|
+
direct.add(k);
|
|
46
|
+
}
|
|
47
|
+
// BFS from each fresh root to find shortest path to a stale node.
|
|
48
|
+
// Memoize the shortest chain per node so a second root reaching it
|
|
49
|
+
// doesn't recompute.
|
|
50
|
+
const shortestChain = new Map(); // null = no stale path
|
|
51
|
+
function chainFor(root) {
|
|
52
|
+
if (direct.has(root))
|
|
53
|
+
return null; // direct, not transitive
|
|
54
|
+
if (shortestChain.has(root))
|
|
55
|
+
return shortestChain.get(root) ?? null;
|
|
56
|
+
const prev = new Map();
|
|
57
|
+
prev.set(root, null);
|
|
58
|
+
const queue = [root];
|
|
59
|
+
let target = null;
|
|
60
|
+
while (queue.length > 0) {
|
|
61
|
+
const cur = queue.shift();
|
|
62
|
+
if (cur !== root && direct.has(cur)) {
|
|
63
|
+
target = cur;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
for (const next of adj.get(cur) ?? []) {
|
|
67
|
+
if (prev.has(next))
|
|
68
|
+
continue;
|
|
69
|
+
prev.set(next, cur);
|
|
70
|
+
queue.push(next);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!target) {
|
|
74
|
+
shortestChain.set(root, null);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const path = [];
|
|
78
|
+
let n = target;
|
|
79
|
+
while (n) {
|
|
80
|
+
path.unshift(n);
|
|
81
|
+
n = prev.get(n) ?? null;
|
|
82
|
+
}
|
|
83
|
+
const chain = path.map((k) => {
|
|
84
|
+
const info = nodes.get(k);
|
|
85
|
+
return { repo: info.repo, path: info.path, mtime: info.mtime };
|
|
86
|
+
});
|
|
87
|
+
shortestChain.set(root, chain);
|
|
88
|
+
return chain;
|
|
89
|
+
}
|
|
90
|
+
const findings = [];
|
|
91
|
+
for (const [k, info] of nodes) {
|
|
92
|
+
if (direct.has(k)) {
|
|
93
|
+
findings.push({
|
|
94
|
+
kind: "direct",
|
|
95
|
+
repo: info.repo,
|
|
96
|
+
path: info.path,
|
|
97
|
+
mtime: info.mtime,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const chain = chainFor(k);
|
|
102
|
+
if (chain) {
|
|
103
|
+
findings.push({
|
|
104
|
+
kind: "transitive",
|
|
105
|
+
repo: info.repo,
|
|
106
|
+
path: info.path,
|
|
107
|
+
mtime: info.mtime,
|
|
108
|
+
staleChain: chain,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=staleness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staleness.js","sourceRoot":"","sources":["../../../src/audit/checks/staleness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Result } from "../frontmatter/types.js";
|
|
2
|
+
import type { AuditConfig, AuditError, RepoSnapshot } from "./types.js";
|
|
3
|
+
export declare function collectRepos(config: AuditConfig): Promise<Result<RepoSnapshot[], AuditError>>;
|
|
4
|
+
//# sourceMappingURL=collect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/audit/collect.ts"],"names":[],"mappings":"AAWA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAA2B,YAAY,EAAE,MAAM,YAAY,CAAC;AAoHjG,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,UAAU,CAAC,CAAC,CAa7C"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/audit/collect.ts
|
|
2
|
+
// The only IO stage of the audit pipeline. Per repo: glob docs, strip
|
|
3
|
+
// frontmatter, extract headings (GitHub-slugged) and links, then batch
|
|
4
|
+
// git log to populate mtimes; on any git failure, fall back to fs mtime.
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { statSync } from "node:fs";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { resolve as nodeResolve } from "node:path";
|
|
9
|
+
import { glob } from "glob";
|
|
10
|
+
import matter from "gray-matter";
|
|
11
|
+
import { err, ok } from "../frontmatter/types.js";
|
|
12
|
+
import { extractLinksFromBody } from "./links.js";
|
|
13
|
+
import { runtimeError } from "./types.js";
|
|
14
|
+
function slugify(heading) {
|
|
15
|
+
// GitHub slug: lowercase, strip non-alphanumeric (keep `-_`), whitespace -> `-`.
|
|
16
|
+
return heading
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/\s+/g, "-")
|
|
19
|
+
.replace(/[^a-z0-9\-_]/g, "")
|
|
20
|
+
.replace(/-+/g, "-")
|
|
21
|
+
.replace(/^-+|-+$/g, "");
|
|
22
|
+
}
|
|
23
|
+
function extractHeadings(body) {
|
|
24
|
+
const out = new Set();
|
|
25
|
+
for (const line of body.split(/\r?\n/)) {
|
|
26
|
+
const m = line.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
27
|
+
if (m)
|
|
28
|
+
out.add(slugify(m[1]));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function gitMtimes(repoPath, docsGlob) {
|
|
33
|
+
const opts = {
|
|
34
|
+
cwd: repoPath,
|
|
35
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
36
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], opts);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let out;
|
|
45
|
+
try {
|
|
46
|
+
// Use the :(glob) pathspec magic so git's internal glob matcher handles
|
|
47
|
+
// "**/*.md" correctly on all platforms and git configurations, including
|
|
48
|
+
// matching top-level files (bare "**/*.md" without the magic prefix skips
|
|
49
|
+
// files at depth 0 on some git versions).
|
|
50
|
+
const pathspec = docsGlob.startsWith(":(") ? docsGlob : `:(glob)${docsGlob}`;
|
|
51
|
+
out = execFileSync("git", ["log", "--all", "--name-only", `--pretty=format:COMMIT %aI`, "--", pathspec], opts).toString();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const mtimes = new Map();
|
|
57
|
+
let currentIso = null;
|
|
58
|
+
for (const line of out.split("\n")) {
|
|
59
|
+
if (line.startsWith("COMMIT ")) {
|
|
60
|
+
currentIso = line.slice("COMMIT ".length).trim() || null;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const path = line.trim();
|
|
64
|
+
if (!path || !currentIso)
|
|
65
|
+
continue;
|
|
66
|
+
// First time we see a path is its newest commit (git log is newest-first).
|
|
67
|
+
if (!mtimes.has(path))
|
|
68
|
+
mtimes.set(path, currentIso);
|
|
69
|
+
}
|
|
70
|
+
return mtimes;
|
|
71
|
+
}
|
|
72
|
+
async function loadDoc(repoPath, relPath, mtimeFromGit) {
|
|
73
|
+
const absPath = nodeResolve(repoPath, relPath);
|
|
74
|
+
const text = await readFile(absPath, "utf-8");
|
|
75
|
+
const parsed = matter(text);
|
|
76
|
+
const body = parsed.content;
|
|
77
|
+
let mtime;
|
|
78
|
+
let mtimeSource;
|
|
79
|
+
if (mtimeFromGit) {
|
|
80
|
+
mtime = mtimeFromGit;
|
|
81
|
+
mtimeSource = "git";
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
mtime = statSync(absPath).mtime.toISOString();
|
|
85
|
+
mtimeSource = "fs";
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
relPath: relPath.split(/[\\/]/).join("/"),
|
|
89
|
+
absPath,
|
|
90
|
+
mtime,
|
|
91
|
+
mtimeSource,
|
|
92
|
+
headings: extractHeadings(body),
|
|
93
|
+
links: extractLinksFromBody(body),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function collectOne(config) {
|
|
97
|
+
const files = await glob(config.docsGlob, {
|
|
98
|
+
cwd: config.path,
|
|
99
|
+
nodir: true,
|
|
100
|
+
posix: true,
|
|
101
|
+
dot: false,
|
|
102
|
+
});
|
|
103
|
+
const onlyMd = files.filter((f) => /\.(md|markdown)$/i.test(f));
|
|
104
|
+
const mtimes = gitMtimes(config.path, config.docsGlob);
|
|
105
|
+
const docs = new Map();
|
|
106
|
+
for (const rel of onlyMd) {
|
|
107
|
+
const posixRel = rel.split(/[\\/]/).join("/");
|
|
108
|
+
try {
|
|
109
|
+
const snap = await loadDoc(config.path, posixRel, mtimes?.get(posixRel));
|
|
110
|
+
docs.set(posixRel, snap);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
114
|
+
process.stderr.write(`daftari audit: warning: unreadable doc ${posixRel}: ${msg}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { config, docs };
|
|
118
|
+
}
|
|
119
|
+
export async function collectRepos(config) {
|
|
120
|
+
// Sequential per-repo; concurrency would help but is out of scope until the
|
|
121
|
+
// 30s budget gets squeezed (see plan §perf).
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const r of config.repos) {
|
|
124
|
+
try {
|
|
125
|
+
out.push(await collectOne(r));
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
129
|
+
return err(runtimeError(`collect failed for repo ${r.name}: ${reason}`));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ok(out);
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=collect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect.js","sourceRoot":"","sources":["../../src/audit/collect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Result } from "../frontmatter/types.js";
|
|
2
|
+
import { type AuditConfig, type AuditError } from "./types.js";
|
|
3
|
+
export declare function parseAuditConfig(argv: string[], yamlLoader?: (path: string) => string): Result<AuditConfig, AuditError>;
|
|
4
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/audit/config.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAgC,MAAM,YAAY,CAAC;AAsG7F,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACpC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAwFjC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/audit/config.ts
|
|
2
|
+
// Parses CLI argv + an optional audit.yaml into an AuditConfig. CLI wins on
|
|
3
|
+
// overlap with YAML; for --output flags only, a stderr warning is emitted so
|
|
4
|
+
// operators see when the file they expected to be written has been displaced.
|
|
5
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
import { err, ok } from "../frontmatter/types.js";
|
|
9
|
+
import { configError } from "./types.js";
|
|
10
|
+
// Inner helpers throw tagged AuditError objects (not class instances). The
|
|
11
|
+
// top-level parseAuditConfig wraps in try/catch and converts to Result.
|
|
12
|
+
function isAuditError(e) {
|
|
13
|
+
return (typeof e === "object" &&
|
|
14
|
+
e !== null &&
|
|
15
|
+
(e.kind === "config" || e.kind === "runtime"));
|
|
16
|
+
}
|
|
17
|
+
const DEFAULTS = {
|
|
18
|
+
docsGlob: "**/*.md",
|
|
19
|
+
thresholdDays: 540,
|
|
20
|
+
failOn: { brokenRefs: 1, transitiveStaleness: 100 },
|
|
21
|
+
};
|
|
22
|
+
function readArg(argv, flag) {
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
if (argv[i] === flag)
|
|
25
|
+
return argv[i + 1];
|
|
26
|
+
const prefix = `${flag}=`;
|
|
27
|
+
if (argv[i]?.startsWith(prefix))
|
|
28
|
+
return argv[i]?.slice(prefix.length);
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function readMulti(argv, flag) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
if (argv[i] === flag && argv[i + 1] !== undefined) {
|
|
36
|
+
out.push(argv[i + 1]);
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const prefix = `${flag}=`;
|
|
41
|
+
if (argv[i]?.startsWith(prefix))
|
|
42
|
+
out.push(argv[i].slice(prefix.length));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function validateRepoPath(p, label) {
|
|
48
|
+
if (!existsSync(p)) {
|
|
49
|
+
throw configError(`${label}: path does not exist: ${p}`);
|
|
50
|
+
}
|
|
51
|
+
if (!statSync(p).isDirectory()) {
|
|
52
|
+
throw configError(`${label}: not a directory: ${p}`);
|
|
53
|
+
}
|
|
54
|
+
return realpathSync(p);
|
|
55
|
+
}
|
|
56
|
+
function parseYamlRepos(raw) {
|
|
57
|
+
if (!raw)
|
|
58
|
+
return [];
|
|
59
|
+
return raw.map((r, i) => {
|
|
60
|
+
if (typeof r.name !== "string" || r.name.length === 0) {
|
|
61
|
+
throw configError(`repos[${i}]: missing name`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof r.path !== "string" || r.path.length === 0) {
|
|
64
|
+
throw configError(`repos[${i}] (${r.name}): missing path`);
|
|
65
|
+
}
|
|
66
|
+
const path = validateRepoPath(resolve(r.path), `repos[${i}] (${r.name})`);
|
|
67
|
+
const docsGlob = typeof r.docs_glob === "string" ? r.docs_glob : DEFAULTS.docsGlob;
|
|
68
|
+
const urls = Array.isArray(r.urls)
|
|
69
|
+
? r.urls.filter((u) => typeof u === "string")
|
|
70
|
+
: [];
|
|
71
|
+
return { name: r.name, path, docsGlob, urls };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function ensureUnique(repos) {
|
|
75
|
+
const seenName = new Set();
|
|
76
|
+
const seenPath = new Set();
|
|
77
|
+
for (const r of repos) {
|
|
78
|
+
if (seenName.has(r.name)) {
|
|
79
|
+
throw configError(`duplicate repo name: ${r.name}`);
|
|
80
|
+
}
|
|
81
|
+
if (seenPath.has(r.path)) {
|
|
82
|
+
throw configError(`duplicate repo path: ${r.path}`);
|
|
83
|
+
}
|
|
84
|
+
seenName.add(r.name);
|
|
85
|
+
seenPath.add(r.path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function warn(msg) {
|
|
89
|
+
process.stderr.write(`daftari audit: warning: ${msg}\n`);
|
|
90
|
+
}
|
|
91
|
+
export function parseAuditConfig(argv, yamlLoader) {
|
|
92
|
+
try {
|
|
93
|
+
const configPath = readArg(argv, "--config");
|
|
94
|
+
let yamlRaw = {};
|
|
95
|
+
if (configPath !== undefined) {
|
|
96
|
+
const load = yamlLoader ?? ((p) => readFileSync(p, "utf-8"));
|
|
97
|
+
let text;
|
|
98
|
+
try {
|
|
99
|
+
text = load(configPath);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
103
|
+
throw configError(`cannot read --config ${configPath}: ${reason}`);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const raw = yaml.load(text);
|
|
107
|
+
if (raw === null || raw === undefined) {
|
|
108
|
+
yamlRaw = {};
|
|
109
|
+
}
|
|
110
|
+
else if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
111
|
+
throw configError(`${configPath}: expected a YAML map at top level, got ${Array.isArray(raw) ? "array" : typeof raw}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
yamlRaw = raw;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (isAuditError(e))
|
|
119
|
+
throw e;
|
|
120
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
121
|
+
throw configError(`malformed YAML in ${configPath}: ${reason}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const yamlRepos = parseYamlRepos(yamlRaw.repos);
|
|
125
|
+
const cliRepoPaths = readMulti(argv, "--repo");
|
|
126
|
+
const cliRepos = cliRepoPaths.map((rawPath, i) => {
|
|
127
|
+
const path = validateRepoPath(resolve(rawPath), `--repo ${rawPath}`);
|
|
128
|
+
return {
|
|
129
|
+
name: `repo-${i}`,
|
|
130
|
+
path,
|
|
131
|
+
docsGlob: DEFAULTS.docsGlob,
|
|
132
|
+
urls: [],
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
const repos = [...yamlRepos, ...cliRepos];
|
|
136
|
+
if (repos.length === 0) {
|
|
137
|
+
throw configError("no repos configured: pass --repo or --config");
|
|
138
|
+
}
|
|
139
|
+
ensureUnique(repos);
|
|
140
|
+
// Output handling: CLI wins, warn if it displaces YAML.
|
|
141
|
+
const yamlMd = typeof yamlRaw.output?.markdown === "string" ? yamlRaw.output.markdown : undefined;
|
|
142
|
+
const yamlJson = typeof yamlRaw.output?.json === "string" ? yamlRaw.output.json : undefined;
|
|
143
|
+
const cliMd = readArg(argv, "--output");
|
|
144
|
+
const cliJson = readArg(argv, "--output-json");
|
|
145
|
+
if (cliMd && yamlMd && cliMd !== yamlMd) {
|
|
146
|
+
warn(`--output overrides output.markdown from config (${yamlMd} → ${cliMd})`);
|
|
147
|
+
}
|
|
148
|
+
if (cliJson && yamlJson && cliJson !== yamlJson) {
|
|
149
|
+
warn(`--output-json overrides output.json from config (${yamlJson} → ${cliJson})`);
|
|
150
|
+
}
|
|
151
|
+
const output = {
|
|
152
|
+
markdown: cliMd ?? yamlMd,
|
|
153
|
+
json: cliJson ?? yamlJson,
|
|
154
|
+
};
|
|
155
|
+
const thresholdDays = typeof yamlRaw.staleness?.threshold_days === "number"
|
|
156
|
+
? yamlRaw.staleness.threshold_days
|
|
157
|
+
: DEFAULTS.thresholdDays;
|
|
158
|
+
const failOn = {
|
|
159
|
+
brokenRefs: typeof yamlRaw.fail_on?.broken_refs === "number"
|
|
160
|
+
? yamlRaw.fail_on.broken_refs
|
|
161
|
+
: DEFAULTS.failOn.brokenRefs,
|
|
162
|
+
transitiveStaleness: typeof yamlRaw.fail_on?.transitive_staleness === "number"
|
|
163
|
+
? yamlRaw.fail_on.transitive_staleness
|
|
164
|
+
: DEFAULTS.failOn.transitiveStaleness,
|
|
165
|
+
};
|
|
166
|
+
return ok({ repos, output, staleness: { thresholdDays }, failOn });
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
if (isAuditError(e))
|
|
170
|
+
return err(e);
|
|
171
|
+
throw e; // not ours; let it propagate
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=config.js.map
|