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.
Files changed (42) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +55 -0
  3. package/dist/audit/checks/broken_refs.d.ts +3 -0
  4. package/dist/audit/checks/broken_refs.d.ts.map +1 -0
  5. package/dist/audit/checks/broken_refs.js +67 -0
  6. package/dist/audit/checks/broken_refs.js.map +1 -0
  7. package/dist/audit/checks/staleness.d.ts +3 -0
  8. package/dist/audit/checks/staleness.d.ts.map +1 -0
  9. package/dist/audit/checks/staleness.js +114 -0
  10. package/dist/audit/checks/staleness.js.map +1 -0
  11. package/dist/audit/collect.d.ts +4 -0
  12. package/dist/audit/collect.d.ts.map +1 -0
  13. package/dist/audit/collect.js +134 -0
  14. package/dist/audit/collect.js.map +1 -0
  15. package/dist/audit/config.d.ts +4 -0
  16. package/dist/audit/config.d.ts.map +1 -0
  17. package/dist/audit/config.js +174 -0
  18. package/dist/audit/config.js.map +1 -0
  19. package/dist/audit/exit.d.ts +6 -0
  20. package/dist/audit/exit.d.ts.map +1 -0
  21. package/dist/audit/exit.js +8 -0
  22. package/dist/audit/exit.js.map +1 -0
  23. package/dist/audit/index.d.ts +2 -0
  24. package/dist/audit/index.d.ts.map +1 -0
  25. package/dist/audit/index.js +93 -0
  26. package/dist/audit/index.js.map +1 -0
  27. package/dist/audit/links.d.ts +4 -0
  28. package/dist/audit/links.d.ts.map +1 -0
  29. package/dist/audit/links.js +156 -0
  30. package/dist/audit/links.js.map +1 -0
  31. package/dist/audit/report.d.ts +4 -0
  32. package/dist/audit/report.d.ts.map +1 -0
  33. package/dist/audit/report.js +58 -0
  34. package/dist/audit/report.js.map +1 -0
  35. package/dist/audit/types.d.ts +94 -0
  36. package/dist/audit/types.d.ts.map +1 -0
  37. package/dist/audit/types.js +11 -0
  38. package/dist/audit/types.js.map +1 -0
  39. package/dist/cli.d.ts.map +1 -1
  40. package/dist/cli.js +6 -0
  41. package/dist/cli.js.map +1 -1
  42. 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,3 @@
1
+ import type { BrokenRefFinding, LinkEdge, RepoSnapshot } from "../types.js";
2
+ export declare function checkBrokenRefs(snapshots: RepoSnapshot[], edges: LinkEdge[]): BrokenRefFinding[];
3
+ //# sourceMappingURL=broken_refs.d.ts.map
@@ -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":"AAAA,kCAAkC;AAClC,2EAA2E;AAC3E,0DAA0D;AAC1D,EAAE;AACF,oBAAoB;AACpB,iEAAiE;AACjE,mDAAmD;AACnD,6EAA6E;AAC7E,2CAA2C;AAC3C,0CAA0C;AAC1C,+EAA+E;AAC/E,yBAAyB;AAIzB,MAAM,UAAU,eAAe,CAAC,SAAyB,EAAE,KAAiB;IAC1E,gDAAgD;IAChD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,QAAQ,GAAuB,EAAE,CAAC;IAExC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,yDAAyD;YACzD,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,cAAc;gBACpB,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE;gBACxD,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE;gBACnF,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,0DAA0D;QAC1D,IAAI,YAAY,GAAkB,IAAI,CAAC;QACvC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACzC,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC;QACjC,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5C,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,KAAK,CAAC;YACvC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBAChC,YAAY,GAAG,MAAM,CAAC;YACxB,CAAC;QACH,CAAC;QAED,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,cAAc;gBACpB,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE;gBACxD,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE;gBACnF,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,uCAAuC;QACvC,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAC9C,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;gBAChD,QAAQ,CAAC,IAAI,CAAC;oBACZ,IAAI,EAAE,gBAAgB;oBACtB,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE;oBACxD,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE;oBAChF,OAAO,EAAE,IAAI,CAAC,OAAO;iBACtB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { LinkEdge, RepoSnapshot, StalenessFinding } from "../types.js";
2
+ export declare function checkStaleness(snapshots: RepoSnapshot[], edges: LinkEdge[], thresholdDays: number, now: Date): StalenessFinding[];
3
+ //# sourceMappingURL=staleness.d.ts.map
@@ -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":"AAAA,uBAAuB;AACvB,sEAAsE;AACtE,uEAAuE;AACvE,yEAAyE;AAEzE,OAAO,EAA4B,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,SAAS,OAAO,CAAC,OAAe;IAC9B,iFAAiF;IACjF,OAAO,OAAO;SACX,WAAW,EAAE;SACb,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;SAC5B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QACjD,IAAI,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,QAAgB;IACnD,MAAM,IAAI,GAAwB;QAChC,GAAG,EAAE,QAAQ;QACb,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;QAC5B,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;KACpC,CAAC;IACF,IAAI,CAAC;QACH,YAAY,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC,EAAE,IAAI,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,0CAA0C;QAC1C,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,QAAQ,EAAE,CAAC;QAC7E,GAAG,GAAG,YAAY,CAChB,KAAK,EACL,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,4BAA4B,EAAE,IAAI,EAAE,QAAQ,CAAC,EAC7E,IAAI,CACL,CAAC,QAAQ,EAAE,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/B,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;YACzD,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU;YAAE,SAAS;QACnC,2EAA2E;QAC3E,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,OAAO,CACpB,QAAgB,EAChB,OAAe,EACf,YAAgC;IAEhC,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC;IAE5B,IAAI,KAAa,CAAC;IAClB,IAAI,WAAyB,CAAC;IAC9B,IAAI,YAAY,EAAE,CAAC;QACjB,KAAK,GAAG,YAAY,CAAC;QACrB,WAAW,GAAG,KAAK,CAAC;IACtB,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAC9C,WAAW,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACzC,OAAO;QACP,KAAK;QACL,WAAW;QACX,QAAQ,EAAE,eAAe,CAAC,IAAI,CAAC;QAC/B,KAAK,EAAE,oBAAoB,CAAC,IAAI,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,MAAkB;IAC1C,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE;QACxC,GAAG,EAAE,MAAM,CAAC,IAAI;QAChB,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,GAAG,EAAE,KAAK;KACX,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC5C,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;YACzE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACvD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0CAA0C,QAAQ,KAAK,GAAG,IAAI,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAmB;IAEnB,4EAA4E;IAC5E,6CAA6C;IAC7C,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,MAAM,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC1D,OAAO,GAAG,CAAC,YAAY,CAAC,2BAA2B,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;AACjB,CAAC"}
@@ -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