@sledorze/cairn 0.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +77950 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config.d.ts +20 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +168 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/core/Config.d.ts +80 -0
  12. package/dist/core/Config.d.ts.map +1 -0
  13. package/dist/core/Config.js +135 -0
  14. package/dist/core/Config.js.map +1 -0
  15. package/dist/core/DocSummaries.d.ts +42 -0
  16. package/dist/core/DocSummaries.d.ts.map +1 -0
  17. package/dist/core/DocSummaries.js +74 -0
  18. package/dist/core/DocSummaries.js.map +1 -0
  19. package/dist/core/MarkdownLinks.bench.d.ts +2 -0
  20. package/dist/core/MarkdownLinks.bench.d.ts.map +1 -0
  21. package/dist/core/MarkdownLinks.bench.js +81 -0
  22. package/dist/core/MarkdownLinks.bench.js.map +1 -0
  23. package/dist/core/MarkdownLinks.d.ts +51 -0
  24. package/dist/core/MarkdownLinks.d.ts.map +1 -0
  25. package/dist/core/MarkdownLinks.js +115 -0
  26. package/dist/core/MarkdownLinks.js.map +1 -0
  27. package/dist/core/SummaryTree.bench.d.ts +2 -0
  28. package/dist/core/SummaryTree.bench.d.ts.map +1 -0
  29. package/dist/core/SummaryTree.bench.js +70 -0
  30. package/dist/core/SummaryTree.bench.js.map +1 -0
  31. package/dist/core/SummaryTree.d.ts +45 -0
  32. package/dist/core/SummaryTree.d.ts.map +1 -0
  33. package/dist/core/SummaryTree.js +196 -0
  34. package/dist/core/SummaryTree.js.map +1 -0
  35. package/dist/core/glob.bench.d.ts +2 -0
  36. package/dist/core/glob.bench.d.ts.map +1 -0
  37. package/dist/core/glob.bench.js +62 -0
  38. package/dist/core/glob.bench.js.map +1 -0
  39. package/dist/core/glob.d.ts +7 -0
  40. package/dist/core/glob.d.ts.map +1 -0
  41. package/dist/core/glob.js +52 -0
  42. package/dist/core/glob.js.map +1 -0
  43. package/dist/core/paths.d.ts +3 -0
  44. package/dist/core/paths.d.ts.map +1 -0
  45. package/dist/core/paths.js +6 -0
  46. package/dist/core/paths.js.map +1 -0
  47. package/dist/index.d.ts +12 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +13 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/init/content.d.ts +3 -0
  52. package/dist/init/content.d.ts.map +1 -0
  53. package/dist/init/content.js +127 -0
  54. package/dist/init/content.js.map +1 -0
  55. package/dist/init/generate.d.ts +14 -0
  56. package/dist/init/generate.d.ts.map +1 -0
  57. package/dist/init/generate.js +121 -0
  58. package/dist/init/generate.js.map +1 -0
  59. package/dist/io/DocsFs.d.ts +30 -0
  60. package/dist/io/DocsFs.d.ts.map +1 -0
  61. package/dist/io/DocsFs.js +95 -0
  62. package/dist/io/DocsFs.js.map +1 -0
  63. package/dist/program/CheckLinks.d.ts +27 -0
  64. package/dist/program/CheckLinks.d.ts.map +1 -0
  65. package/dist/program/CheckLinks.js +122 -0
  66. package/dist/program/CheckLinks.js.map +1 -0
  67. package/dist/program/CheckSummaries.d.ts +38 -0
  68. package/dist/program/CheckSummaries.d.ts.map +1 -0
  69. package/dist/program/CheckSummaries.js +198 -0
  70. package/dist/program/CheckSummaries.js.map +1 -0
  71. package/dist/program/JsonReport.d.ts +14 -0
  72. package/dist/program/JsonReport.d.ts.map +1 -0
  73. package/dist/program/JsonReport.js +12 -0
  74. package/dist/program/JsonReport.js.map +1 -0
  75. package/dist/program/locale.d.ts +13 -0
  76. package/dist/program/locale.d.ts.map +1 -0
  77. package/dist/program/locale.js +8 -0
  78. package/dist/program/locale.js.map +1 -0
  79. package/package.json +78 -0
  80. package/schema/cairn.schema.json +77 -0
@@ -0,0 +1,62 @@
1
+ import { bench, describe } from 'vitest';
2
+ import { globToRegExp, matchesAny, matchesGlob } from "./glob.js";
3
+ const DIRS = ['src', 'docs', 'lib', 'packages', 'apps', 'test', 'scripts', 'vendor'];
4
+ const SUBDIRS = ['core', 'utils', 'components', 'services', 'models', 'fixtures'];
5
+ const EXTS = ['ts', 'tsx', 'md', 'json', 'css', 'tmp'];
6
+ const makePaths = (count) => Array.from({ length: count }, (_, i) => {
7
+ const dir = DIRS[i % DIRS.length];
8
+ const sub = SUBDIRS[i % SUBDIRS.length];
9
+ const ext = EXTS[i % EXTS.length];
10
+ if (i % 11 === 0) {
11
+ return `${dir}/node_modules/${sub}/pkg-${i}/index.${ext}`;
12
+ }
13
+ if (i % 13 === 0) {
14
+ return `${dir}/.git/objects/${i}.pack`;
15
+ }
16
+ if (i % 17 === 0) {
17
+ return `dist/${sub}/bundle-${i}.${ext}`;
18
+ }
19
+ return `${dir}/${sub}/deep/nested/path/file-${i}.${ext}`;
20
+ });
21
+ const IGNORE_PATTERNS = ['**/node_modules/**', '**/.git/**', 'dist/**', '**/*.tmp'];
22
+ const SIMPLE_PATTERN = 'docs/*.md';
23
+ const STAR_STAR_PATTERN = 'docs/**/a.md';
24
+ const ENCLOSURE_PATTERN = '**/node_modules/**';
25
+ const COMPLEX_PATTERN = '**/src/**/*.spec.?s?(x)';
26
+ const paths500 = makePaths(500);
27
+ const paths5000 = makePaths(5000);
28
+ describe('globToRegExp() — compilation cost by pattern complexity', () => {
29
+ bench('simple single-segment pattern', () => {
30
+ globToRegExp(SIMPLE_PATTERN);
31
+ });
32
+ bench('`**` pattern', () => {
33
+ globToRegExp(STAR_STAR_PATTERN);
34
+ });
35
+ bench('`**/dir/**` enclosure pattern', () => {
36
+ globToRegExp(ENCLOSURE_PATTERN);
37
+ });
38
+ bench('longer pattern with mixed wildcards', () => {
39
+ globToRegExp(COMPLEX_PATTERN);
40
+ });
41
+ });
42
+ describe('matchesGlob() — single call, since it recompiles the regex every time', () => {
43
+ bench('match against a `**/node_modules/**` pattern', () => {
44
+ matchesGlob('src/core/node_modules/pkg/index.ts', ENCLOSURE_PATTERN);
45
+ });
46
+ bench('non-match against a `**/node_modules/**` pattern', () => {
47
+ matchesGlob('src/core/deep/nested/path/file.ts', ENCLOSURE_PATTERN);
48
+ });
49
+ });
50
+ describe('matchesAny() — scanning cost across candidate-path-list scale', () => {
51
+ bench('~500 paths against the realistic ignore list', () => {
52
+ for (const path of paths500) {
53
+ matchesAny(path, IGNORE_PATTERNS);
54
+ }
55
+ });
56
+ bench('~5000 paths against the realistic ignore list', () => {
57
+ for (const path of paths5000) {
58
+ matchesAny(path, IGNORE_PATTERNS);
59
+ }
60
+ });
61
+ });
62
+ //# sourceMappingURL=glob.bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.bench.js","sourceRoot":"","sources":["../../src/core/glob.bench.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** Compile a glob into an anchored RegExp. */
2
+ export declare const globToRegExp: (pattern: string) => RegExp;
3
+ /** True when `path` matches the glob `pattern`. */
4
+ export declare const matchesGlob: (path: string, pattern: string) => boolean;
5
+ /** True when `path` matches any of the glob `patterns`. */
6
+ export declare const matchesAny: (path: string, patterns: readonly string[]) => boolean;
7
+ //# sourceMappingURL=glob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/core/glob.ts"],"names":[],"mappings":"AAWA,8CAA8C;AAC9C,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAQ9C,CAAA;AA4BD,mDAAmD;AACnD,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,OAA2C,CAAA;AAEvG,2DAA2D;AAC3D,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,EAAE,UAAU,SAAS,MAAM,EAAE,KAAG,OACf,CAAA"}
@@ -0,0 +1,52 @@
1
+ // Tiny dependency-free glob matcher for `ignore` patterns and `roots` expansion.
2
+ // Supports `**` (any run, including `/`), `*` (any run except `/`) and `?` (one
3
+ // char except `/`). Patterns and paths use `/` separators. Pure and unit-tested.
4
+ const SPECIAL = /[.+^${}()|[\]\\]/g;
5
+ // Ignore/root pattern lists are small and reused across every path they're tested
6
+ // against (an O(paths) x O(patterns) product), so compiling once per distinct
7
+ // pattern — instead of once per call — turns that product into O(patterns).
8
+ const compiled = new Map();
9
+ /** Compile a glob into an anchored RegExp. */
10
+ export const globToRegExp = (pattern) => {
11
+ const cached = compiled.get(pattern);
12
+ if (cached) {
13
+ return cached;
14
+ }
15
+ const re = compile(pattern);
16
+ compiled.set(pattern, re);
17
+ return re;
18
+ };
19
+ const compile = (pattern) => {
20
+ let re = '';
21
+ for (let i = 0; i < pattern.length; i += 1) {
22
+ const c = pattern[i];
23
+ if (c === '*') {
24
+ if (pattern[i + 1] === '*') {
25
+ // `**` — any run including path separators. Absorb a following `/`.
26
+ i += 1;
27
+ if (pattern[i + 1] === '/') {
28
+ i += 1;
29
+ re += '(?:.*/)?';
30
+ }
31
+ else {
32
+ re += '.*';
33
+ }
34
+ }
35
+ else {
36
+ re += '[^/]*';
37
+ }
38
+ }
39
+ else if (c === '?') {
40
+ re += '[^/]';
41
+ }
42
+ else {
43
+ re += (c ?? '').replace(SPECIAL, '\\$&');
44
+ }
45
+ }
46
+ return new RegExp(`^${re}$`);
47
+ };
48
+ /** True when `path` matches the glob `pattern`. */
49
+ export const matchesGlob = (path, pattern) => globToRegExp(pattern).test(path);
50
+ /** True when `path` matches any of the glob `patterns`. */
51
+ export const matchesAny = (path, patterns) => patterns.some((pattern) => matchesGlob(path, pattern));
52
+ //# sourceMappingURL=glob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["../../src/core/glob.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,gFAAgF;AAChF,iFAAiF;AAEjF,MAAM,OAAO,GAAG,mBAAmB,CAAA;AAEnC,kFAAkF;AAClF,8EAA8E;AAC9E,4EAA4E;AAC5E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;AAE1C,8CAA8C;AAC9C,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAU,EAAE;IACtD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACpC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAA;IACf,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3B,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IACzB,OAAO,EAAE,CAAA;AACX,CAAC,CAAA;AAED,MAAM,OAAO,GAAG,CAAC,OAAe,EAAU,EAAE;IAC1C,IAAI,EAAE,GAAG,EAAE,CAAA;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,oEAAoE;gBACpE,CAAC,IAAI,CAAC,CAAA;gBACN,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC3B,CAAC,IAAI,CAAC,CAAA;oBACN,EAAE,IAAI,UAAU,CAAA;gBAClB,CAAC;qBAAM,CAAC;oBACN,EAAE,IAAI,IAAI,CAAA;gBACZ,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,EAAE,IAAI,OAAO,CAAA;YACf,CAAC;QACH,CAAC;aAAM,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACrB,EAAE,IAAI,MAAM,CAAA;QACd,CAAC;aAAM,CAAC;YACN,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;AAC9B,CAAC,CAAA;AAED,mDAAmD;AACnD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,OAAe,EAAW,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEvG,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,QAA2B,EAAW,EAAE,CAC/E,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ /** Convert an OS path to POSIX form (`\` -> `/`). */
2
+ export declare const toPosix: (p: string) => string;
3
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAIA,qDAAqD;AACrD,eAAO,MAAM,OAAO,GAAI,GAAG,MAAM,KAAG,MAAiC,CAAA"}
@@ -0,0 +1,6 @@
1
+ // Path normalisation. The pure planners reason in POSIX (`/`) paths so behaviour
2
+ // is identical on every OS; the IO layer normalises real filesystem paths (which
3
+ // may use `\` on Windows) to POSIX before they reach the core.
4
+ /** Convert an OS path to POSIX form (`\` -> `/`). */
5
+ export const toPosix = (p) => p.replaceAll('\\', '/');
6
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,iFAAiF;AACjF,+DAA+D;AAE/D,qDAAqD;AACrD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA"}
@@ -0,0 +1,12 @@
1
+ export * from './core/DocSummaries.ts';
2
+ export * from './core/glob.ts';
3
+ export * from './core/MarkdownLinks.ts';
4
+ export * from './core/SummaryTree.ts';
5
+ export * from './io/DocsFs.ts';
6
+ export * from './program/CheckLinks.ts';
7
+ export * from './program/CheckSummaries.ts';
8
+ export * from './program/locale.ts';
9
+ export * from './config.ts';
10
+ export type { AgentTarget, InitArgs, InitResult } from './init/generate.ts';
11
+ export { runInit } from './init/generate.ts';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,wBAAwB,CAAA;AACtC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // Public API surface. The CLI (`cairn`) is the primary entrypoint, but the pure
2
+ // planners and Effect programs are exported for programmatic use and testing.
3
+ export * from "./core/DocSummaries.js";
4
+ export * from "./core/glob.js";
5
+ export * from "./core/MarkdownLinks.js";
6
+ export * from "./core/SummaryTree.js";
7
+ export * from "./io/DocsFs.js";
8
+ export * from "./program/CheckLinks.js";
9
+ export * from "./program/CheckSummaries.js";
10
+ export * from "./program/locale.js";
11
+ export * from "./config.js";
12
+ export { runInit } from "./init/generate.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8EAA8E;AAE9E,cAAc,wBAAwB,CAAA;AACtC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAE3B,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export declare const CONVENTION_BODY = "# Documentation summary convention\n\nThis repo enforces a **hierarchical, content-hashed documentation summary** tree.\nCI runs `cairn check` and **fails the merge** if any summary is missing,\nstale, or a link is broken. Treat green `check` as a hard requirement, not a nicety.\n\n## The invariant\n\n1. **File summaries** \u2014 every Markdown file longer than the threshold (default 30\n lines) has a sibling `X.summary.md`: a fast-to-read digest of the CURRENT content\n of `X.md`.\n2. **Directory summaries** \u2014 every in-scope directory has a `_SUMMARY.md` that\n aggregates its direct docs (each doc's `.summary.md` if the doc is big, else the\n doc itself) plus the `_SUMMARY.md` of each direct sub-directory. It links to\n **every** direct child file and sub-directory (link-completeness).\n3. **Freshness by content hash** \u2014 each summary's first line is\n `<!-- source-sha256: <64-hex> -->`. The checker recomputes the source hash;\n mismatch = stale, absent = missing. This survives git clone and CI (mtime does not).\n4. **Bottom-up in one pass** \u2014 a directory summary hashes a manifest of its children's\n hashes (a Merkle tree), so (re)write leaves-first: file summaries, then directories\n deepest-first, then stamp.\n\n## Workflow when you edit docs\n\nWhen you create or edit any doc:\n\n1. If the doc is longer than the threshold, create or update its `X.summary.md` to\n reflect the new content.\n2. Update the `_SUMMARY.md` of every affected directory, walking **up** the tree\n leaves-first, and keep a link to every child file and sub-directory.\n3. Run the stamp command to (re)write the `source-sha256` hashes bottom-up:\n `npx cairn check --summaries-only --stamp`.\n4. Run `npx cairn check` and ensure it exits 0 (green) before you finish.\n\n## Commands\n\n- `npx cairn check` \u2014 check summaries + links (exit 1 on any problem).\n- `npx cairn check --summaries-only` / `--links-only`.\n- `npx cairn check --links-only --fix` \u2014 auto-repair unambiguous dead links.\n- `npx cairn check --summaries-only --stamp` \u2014 write stamps of EXISTING\n summaries bottom-up. It does **not** author prose; you write the content, then stamp.\n\nYou author the prose. The tool only verifies and stamps.\n";
2
+ export declare const SKILL_BODY = "# Writing good documentation summaries\n\nUse this when you author or refresh the summary tree that `cairn`\nenforces. It covers *how* to write summaries that are worth reading, and the exact\nmechanical order to (re)generate them so `check` goes green in one pass.\n\n## Two kinds of summary \u2014 do not conflate them\n\n**A file summary (`X.summary.md`) condenses ONE document.**\nGoal: a reader grasps what `X.md` says in ~10 seconds.\n\n- Faithful to the source \u2014 never invent, never contradict, never add claims the doc\n doesn't make. It is a digest, not commentary.\n- Front-load the thesis and the hard numbers. Lead with the conclusion, the decision,\n the metric \u2014 not the background.\n- Bullet-dense, no fluff. Drop hedging, transitions, and restated headings.\n- If the source changes, the summary changes. A summary that no longer matches its\n source is a bug the checker will catch.\n\n**A directory summary (`_SUMMARY.md`) is a MAP, not a digest.**\nGoal: a reader knows what lives in this directory and where to go next.\n\n- A short orientation paragraph (1-3 sentences): what this directory is about.\n- Then one line per direct child \u2014 file **and** sub-directory \u2014 each with a Markdown\n link and a few-word hook describing what's behind it.\n- For a big doc, link and hook its `.summary.md`; for a small doc, link the doc itself.\n- For a sub-directory, link its `_SUMMARY.md`.\n- **Link-completeness:** every direct child must appear as a link. A missing link fails\n `check`. When you add or remove a child, update the parent's `_SUMMARY.md`.\n\n## Why leaves-first \u2014 the Merkle mental model\n\nEach summary is stamped with `<!-- source-sha256: <hex> -->` over its source. A\ndirectory summary's source is a **manifest of its children's hashes**, so a child's\nhash must be settled before its parent can be stamped. Think of it as a Merkle tree:\nchange a leaf and every hash on the path to the root must be recomputed. If you stamp\ntop-down, parents capture stale child hashes and `check` stays red.\n\n## The bottom-up procedure\n\n1. **Author leaves first.** For every doc over the threshold, write/refresh its\n `X.summary.md`. Get the prose right before touching any directory.\n2. **Author directories deepest-first.** Walk from the deepest directories up to the\n roots. For each, write its `_SUMMARY.md`: orientation paragraph, then a linked line\n for every direct child (child `.summary.md` or doc, and each sub-dir's `_SUMMARY.md`).\n3. **Stamp mechanically.** Run `npx cairn check --summaries-only --stamp`.\n It rewrites every `source-sha256` bottom-up. **Never hand-edit a sha256** \u2014 it is\n computed, not authored; a hand-typed hash is always wrong.\n4. **Verify.** Run `npx cairn check` and confirm exit 0.\n\n## Tiny examples\n\nA **file summary** (`guides/getting-started.summary.md`):\n\n```markdown\n<!-- source-sha256: 0000...(stamped by the tool) -->\n# Getting started \u2014 summary\n\n- Install as a dev dependency, then run the init command.\n- Configure via a single rc file; every option has a sensible default.\n- First run scaffolds an example and prints the next command to run.\n```\n\nA **directory summary** (`guides/_SUMMARY.md`):\n\n```markdown\n<!-- source-sha256: 0000...(stamped by the tool) -->\n# Guides\n\nHow-to guides for everyday tasks, in reading order.\n\n- [getting-started](./getting-started.summary.md) \u2014 install, configure, first run\n- [configuration](./configuration.md) \u2014 every rc option, with its default\n- [advanced/](./advanced/_SUMMARY.md) \u2014 recipes for larger setups\n```\n\nKeep summaries short, keep links complete, stamp last, verify green.\n";
3
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/init/content.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,eAAe,8tEA2C3B,CAAA;AAED,eAAO,MAAM,UAAU,gnHA6EtB,CAAA"}
@@ -0,0 +1,127 @@
1
+ // Prose surfaces injected by `cairn init`.
2
+ // These are agent-neutral bodies; the generator wraps each with the
3
+ // frontmatter appropriate to its target (Claude rule, Copilot instructions,
4
+ // AGENTS.md block, or SKILL.md).
5
+ export const CONVENTION_BODY = `# Documentation summary convention
6
+
7
+ This repo enforces a **hierarchical, content-hashed documentation summary** tree.
8
+ CI runs \`cairn check\` and **fails the merge** if any summary is missing,
9
+ stale, or a link is broken. Treat green \`check\` as a hard requirement, not a nicety.
10
+
11
+ ## The invariant
12
+
13
+ 1. **File summaries** — every Markdown file longer than the threshold (default 30
14
+ lines) has a sibling \`X.summary.md\`: a fast-to-read digest of the CURRENT content
15
+ of \`X.md\`.
16
+ 2. **Directory summaries** — every in-scope directory has a \`_SUMMARY.md\` that
17
+ aggregates its direct docs (each doc's \`.summary.md\` if the doc is big, else the
18
+ doc itself) plus the \`_SUMMARY.md\` of each direct sub-directory. It links to
19
+ **every** direct child file and sub-directory (link-completeness).
20
+ 3. **Freshness by content hash** — each summary's first line is
21
+ \`<!-- source-sha256: <64-hex> -->\`. The checker recomputes the source hash;
22
+ mismatch = stale, absent = missing. This survives git clone and CI (mtime does not).
23
+ 4. **Bottom-up in one pass** — a directory summary hashes a manifest of its children's
24
+ hashes (a Merkle tree), so (re)write leaves-first: file summaries, then directories
25
+ deepest-first, then stamp.
26
+
27
+ ## Workflow when you edit docs
28
+
29
+ When you create or edit any doc:
30
+
31
+ 1. If the doc is longer than the threshold, create or update its \`X.summary.md\` to
32
+ reflect the new content.
33
+ 2. Update the \`_SUMMARY.md\` of every affected directory, walking **up** the tree
34
+ leaves-first, and keep a link to every child file and sub-directory.
35
+ 3. Run the stamp command to (re)write the \`source-sha256\` hashes bottom-up:
36
+ \`npx cairn check --summaries-only --stamp\`.
37
+ 4. Run \`npx cairn check\` and ensure it exits 0 (green) before you finish.
38
+
39
+ ## Commands
40
+
41
+ - \`npx cairn check\` — check summaries + links (exit 1 on any problem).
42
+ - \`npx cairn check --summaries-only\` / \`--links-only\`.
43
+ - \`npx cairn check --links-only --fix\` — auto-repair unambiguous dead links.
44
+ - \`npx cairn check --summaries-only --stamp\` — write stamps of EXISTING
45
+ summaries bottom-up. It does **not** author prose; you write the content, then stamp.
46
+
47
+ You author the prose. The tool only verifies and stamps.
48
+ `;
49
+ export const SKILL_BODY = `# Writing good documentation summaries
50
+
51
+ Use this when you author or refresh the summary tree that \`cairn\`
52
+ enforces. It covers *how* to write summaries that are worth reading, and the exact
53
+ mechanical order to (re)generate them so \`check\` goes green in one pass.
54
+
55
+ ## Two kinds of summary — do not conflate them
56
+
57
+ **A file summary (\`X.summary.md\`) condenses ONE document.**
58
+ Goal: a reader grasps what \`X.md\` says in ~10 seconds.
59
+
60
+ - Faithful to the source — never invent, never contradict, never add claims the doc
61
+ doesn't make. It is a digest, not commentary.
62
+ - Front-load the thesis and the hard numbers. Lead with the conclusion, the decision,
63
+ the metric — not the background.
64
+ - Bullet-dense, no fluff. Drop hedging, transitions, and restated headings.
65
+ - If the source changes, the summary changes. A summary that no longer matches its
66
+ source is a bug the checker will catch.
67
+
68
+ **A directory summary (\`_SUMMARY.md\`) is a MAP, not a digest.**
69
+ Goal: a reader knows what lives in this directory and where to go next.
70
+
71
+ - A short orientation paragraph (1-3 sentences): what this directory is about.
72
+ - Then one line per direct child — file **and** sub-directory — each with a Markdown
73
+ link and a few-word hook describing what's behind it.
74
+ - For a big doc, link and hook its \`.summary.md\`; for a small doc, link the doc itself.
75
+ - For a sub-directory, link its \`_SUMMARY.md\`.
76
+ - **Link-completeness:** every direct child must appear as a link. A missing link fails
77
+ \`check\`. When you add or remove a child, update the parent's \`_SUMMARY.md\`.
78
+
79
+ ## Why leaves-first — the Merkle mental model
80
+
81
+ Each summary is stamped with \`<!-- source-sha256: <hex> -->\` over its source. A
82
+ directory summary's source is a **manifest of its children's hashes**, so a child's
83
+ hash must be settled before its parent can be stamped. Think of it as a Merkle tree:
84
+ change a leaf and every hash on the path to the root must be recomputed. If you stamp
85
+ top-down, parents capture stale child hashes and \`check\` stays red.
86
+
87
+ ## The bottom-up procedure
88
+
89
+ 1. **Author leaves first.** For every doc over the threshold, write/refresh its
90
+ \`X.summary.md\`. Get the prose right before touching any directory.
91
+ 2. **Author directories deepest-first.** Walk from the deepest directories up to the
92
+ roots. For each, write its \`_SUMMARY.md\`: orientation paragraph, then a linked line
93
+ for every direct child (child \`.summary.md\` or doc, and each sub-dir's \`_SUMMARY.md\`).
94
+ 3. **Stamp mechanically.** Run \`npx cairn check --summaries-only --stamp\`.
95
+ It rewrites every \`source-sha256\` bottom-up. **Never hand-edit a sha256** — it is
96
+ computed, not authored; a hand-typed hash is always wrong.
97
+ 4. **Verify.** Run \`npx cairn check\` and confirm exit 0.
98
+
99
+ ## Tiny examples
100
+
101
+ A **file summary** (\`guides/getting-started.summary.md\`):
102
+
103
+ \`\`\`markdown
104
+ <!-- source-sha256: 0000...(stamped by the tool) -->
105
+ # Getting started — summary
106
+
107
+ - Install as a dev dependency, then run the init command.
108
+ - Configure via a single rc file; every option has a sensible default.
109
+ - First run scaffolds an example and prints the next command to run.
110
+ \`\`\`
111
+
112
+ A **directory summary** (\`guides/_SUMMARY.md\`):
113
+
114
+ \`\`\`markdown
115
+ <!-- source-sha256: 0000...(stamped by the tool) -->
116
+ # Guides
117
+
118
+ How-to guides for everyday tasks, in reading order.
119
+
120
+ - [getting-started](./getting-started.summary.md) — install, configure, first run
121
+ - [configuration](./configuration.md) — every rc option, with its default
122
+ - [advanced/](./advanced/_SUMMARY.md) — recipes for larger setups
123
+ \`\`\`
124
+
125
+ Keep summaries short, keep links complete, stamp last, verify green.
126
+ `;
127
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/init/content.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,oEAAoE;AACpE,4EAA4E;AAC5E,iCAAiC;AAEjC,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2C9B,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6EzB,CAAA"}
@@ -0,0 +1,14 @@
1
+ export declare const AGENT_TARGETS: readonly ["agents", "all", "claude", "copilot", "opencode"];
2
+ export type AgentTarget = (typeof AGENT_TARGETS)[number];
3
+ export interface InitArgs {
4
+ readonly agent: AgentTarget;
5
+ readonly cwd: string;
6
+ readonly roots: readonly string[];
7
+ }
8
+ export interface InitResult {
9
+ readonly written: readonly string[];
10
+ readonly skipped: readonly string[];
11
+ }
12
+ /** Run the scaffold. Returns which files were written vs left untouched. */
13
+ export declare const runInit: ({ agent, cwd, roots }: InitArgs) => InitResult;
14
+ //# sourceMappingURL=generate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/init/generate.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,aAAa,6DAA8D,CAAA;AACxF,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AA2FD,4EAA4E;AAC5E,eAAO,MAAM,OAAO,GAAI,uBAAuB,QAAQ,KAAG,UA6BzD,CAAA"}
@@ -0,0 +1,121 @@
1
+ // `cairn init` — scaffold agent guidance from a single source of truth.
2
+ // The SAME convention body is rendered into the format each agent reads:
3
+ // - Claude Code: .claude/rules/docs-summaries.md (frontmatter `paths:`)
4
+ // - GitHub Copilot: .github/instructions/docs-summaries.instructions.md (`applyTo:`)
5
+ // - Cross-tool: a marked block in AGENTS.md — the file OpenCode (and Codex) read
6
+ // natively, so `--agent opencode` is a thin alias for `--agent agents`: no separate
7
+ // file format, just this block.
8
+ // - Claude/Codex: .claude/skills/cairn/SKILL.md (the writing methodology)
9
+ // - Claude Code: a marked `@AGENTS.md` import in CLAUDE.md — Claude Code auto-loads
10
+ // CLAUDE.md at session start but never reads AGENTS.md on its own, so without this
11
+ // pointer the cross-tool block in AGENTS.md is invisible to it.
12
+ // Plus a starter .cairnrc.json (only when absent).
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { CONVENTION_BODY, SKILL_BODY } from "./content.js";
16
+ // Single source of truth for valid `--agent` values, so the CLI's choice list
17
+ // (src/cli.ts) can never drift from what `runInit` actually understands.
18
+ export const AGENT_TARGETS = ['agents', 'all', 'claude', 'copilot', 'opencode'];
19
+ const AGENTS_START = '<!-- cairn:start -->';
20
+ const AGENTS_END = '<!-- cairn:end -->';
21
+ const rootsToGlobs = (roots) => roots.map((r) => `${r.replace(/\/+$/, '')}/**`);
22
+ const ensureDir = (file) => {
23
+ fs.mkdirSync(path.dirname(file), { recursive: true });
24
+ };
25
+ const write = (file, content, written) => {
26
+ ensureDir(file);
27
+ fs.writeFileSync(file, content.endsWith('\n') ? content : `${content}\n`);
28
+ written.push(file);
29
+ };
30
+ const claudeRule = (globs) => {
31
+ const paths = globs.map((g) => ` - '${g}'`).join('\n');
32
+ return `---\npaths:\n${paths}\n---\n\n${CONVENTION_BODY}`;
33
+ };
34
+ const copilotInstructions = (globs) => `---\napplyTo: '${globs.join(', ')}'\n---\n\n${CONVENTION_BODY}`;
35
+ const skillFile = () => [
36
+ '---',
37
+ 'name: cairn',
38
+ 'description: Methodology for writing and maintaining the hierarchical documentation summary tree enforced by cairn. Use when authoring or refreshing docs summaries (X.summary.md / _SUMMARY.md).',
39
+ '---',
40
+ '',
41
+ SKILL_BODY,
42
+ ].join('\n');
43
+ /** Insert or replace the cairn block in AGENTS.md, leaving other content intact. */
44
+ const upsertAgentsBlock = (cwd, written) => {
45
+ const file = path.join(cwd, 'AGENTS.md');
46
+ const block = `${AGENTS_START}\n\n${CONVENTION_BODY.trimEnd()}\n\n${AGENTS_END}`;
47
+ let next;
48
+ if (fs.existsSync(file)) {
49
+ const existing = fs.readFileSync(file, 'utf8');
50
+ if (existing.includes(AGENTS_START) && existing.includes(AGENTS_END)) {
51
+ next = existing.replace(new RegExp(`${AGENTS_START}[\\s\\S]*?${AGENTS_END}`), block);
52
+ }
53
+ else {
54
+ next = `${existing.trimEnd()}\n\n${block}\n`;
55
+ }
56
+ }
57
+ else {
58
+ next = `# AGENTS.md\n\n${block}\n`;
59
+ }
60
+ fs.writeFileSync(file, next.endsWith('\n') ? next : `${next}\n`);
61
+ written.push(file);
62
+ };
63
+ /** Ensure CLAUDE.md imports AGENTS.md. Claude Code auto-loads CLAUDE.md (not AGENTS.md)
64
+ * at session start, so without this the AGENTS.md block cairn writes is never read. Leaves
65
+ * other CLAUDE.md content intact, and no-ops if an `@AGENTS.md` import is already present
66
+ * (hand-written or from a previous run). */
67
+ const upsertClaudeMdImport = (cwd, written, skipped) => {
68
+ const file = path.join(cwd, 'CLAUDE.md');
69
+ const block = `${AGENTS_START}\n@AGENTS.md\n${AGENTS_END}`;
70
+ if (fs.existsSync(file)) {
71
+ const existing = fs.readFileSync(file, 'utf8');
72
+ if (existing.includes('@AGENTS.md')) {
73
+ skipped.push(file);
74
+ return;
75
+ }
76
+ const next = existing.includes(AGENTS_START) && existing.includes(AGENTS_END)
77
+ ? existing.replace(new RegExp(`${AGENTS_START}[\\s\\S]*?${AGENTS_END}`), block)
78
+ : `${existing.trimEnd()}\n\n${block}\n`;
79
+ fs.writeFileSync(file, next.endsWith('\n') ? next : `${next}\n`);
80
+ }
81
+ else {
82
+ fs.writeFileSync(file, `${block}\n`);
83
+ }
84
+ written.push(file);
85
+ };
86
+ const starterConfig = (roots) => `${JSON.stringify({
87
+ $schema: './node_modules/@sledorze/cairn/schema/cairn.schema.json',
88
+ ignore: ['**/node_modules/**'],
89
+ naming: { dirSummary: '_SUMMARY.md', fileSummarySuffix: '.summary.md' },
90
+ roots,
91
+ thresholdLines: 30,
92
+ }, null, 2)}\n`;
93
+ /** Run the scaffold. Returns which files were written vs left untouched. */
94
+ export const runInit = ({ agent, cwd, roots }) => {
95
+ const globs = rootsToGlobs(roots);
96
+ const written = [];
97
+ const skipped = [];
98
+ const doClaude = agent === 'claude' || agent === 'all';
99
+ const doCopilot = agent === 'copilot' || agent === 'all';
100
+ const doAgents = agent === 'agents' || agent === 'opencode' || agent === 'all';
101
+ if (doClaude) {
102
+ write(path.join(cwd, '.claude/rules/docs-summaries.md'), claudeRule(globs), written);
103
+ write(path.join(cwd, '.claude/skills/cairn/SKILL.md'), skillFile(), written);
104
+ upsertClaudeMdImport(cwd, written, skipped);
105
+ }
106
+ if (doCopilot) {
107
+ write(path.join(cwd, '.github/instructions/docs-summaries.instructions.md'), copilotInstructions(globs), written);
108
+ }
109
+ if (doAgents || doClaude || doCopilot) {
110
+ upsertAgentsBlock(cwd, written);
111
+ }
112
+ const rc = path.join(cwd, '.cairnrc.json');
113
+ if (fs.existsSync(rc)) {
114
+ skipped.push(rc);
115
+ }
116
+ else {
117
+ write(rc, starterConfig(roots), written);
118
+ }
119
+ return { skipped, written };
120
+ };
121
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate.js","sourceRoot":"","sources":["../../src/init/generate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { FileSystem, Path } from '@effect/platform';
2
+ import { Context, Effect, Layer } from 'effect';
3
+ export interface FileStat {
4
+ readonly mtimeMs: number;
5
+ readonly sizeBytes: number;
6
+ }
7
+ export interface DocsFsService {
8
+ readonly deleteFile: (abs: string) => Effect.Effect<void>;
9
+ readonly exists: (abs: string) => Effect.Effect<boolean>;
10
+ readonly listFiles: (roots: readonly string[]) => Effect.Effect<readonly string[]>;
11
+ readonly readFile: (abs: string) => Effect.Effect<string>;
12
+ readonly stat: (abs: string) => Effect.Effect<FileStat>;
13
+ readonly writeFile: (abs: string, content: string) => Effect.Effect<void>;
14
+ }
15
+ declare const DocsFs_base: Context.TagClass<DocsFs, "DocsFs", DocsFsService>;
16
+ export declare class DocsFs extends DocsFs_base {
17
+ }
18
+ /** Live implementation bound to the Node filesystem. */
19
+ export declare const DocsFsLive: Layer.Layer<DocsFs, never, FileSystem.FileSystem | Path.Path>;
20
+ export interface TestFile {
21
+ readonly content: string;
22
+ readonly mtimeMs: number;
23
+ }
24
+ /**
25
+ * In-memory DocsFs layer for tests. `files` maps absolute path -> file.
26
+ * `writeFile` mutates the backing map so fix behaviour can be asserted.
27
+ */
28
+ export declare const makeTestDocsFs: (files: Record<string, TestFile>) => Layer.Layer<DocsFs>;
29
+ export {};
30
+ //# sourceMappingURL=DocsFs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocsFs.d.ts","sourceRoot":"","sources":["../../src/io/DocsFs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAA;AAIvD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACzD,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACxD,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;IAClF,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACzD,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACvD,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC1E;;AAED,qBAAa,MAAO,SAAQ,WAA8C;CAAG;AAE7E,wDAAwD;AACxD,eAAO,MAAM,UAAU,+DA2DtB,CAAA;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CA2ClF,CAAA"}
@@ -0,0 +1,95 @@
1
+ // Filesystem capability for the docs checkers, expressed as an Effect service
2
+ // so programs stay testable. `DocsFsLive` binds it to the real Node platform
3
+ // (via @effect/platform-node); `makeTestDocsFs` provides an in-memory layer.
4
+ import { FileSystem, Path } from '@effect/platform';
5
+ import { Context, Effect, Layer, Option } from 'effect';
6
+ import { toPosix } from "../core/paths.js";
7
+ export class DocsFs extends Context.Tag('DocsFs')() {
8
+ }
9
+ /** Live implementation bound to the Node filesystem. */
10
+ export const DocsFsLive = Layer.effect(DocsFs, Effect.gen(function* () {
11
+ const fs = yield* FileSystem.FileSystem;
12
+ const path = yield* Path.Path;
13
+ // `FileSystem.readDirectory` returns bare names (no Dirent-style type info),
14
+ // so telling files from directories still costs one `stat` per entry — but
15
+ // those stats are independent, so bounding their concurrency (rather than
16
+ // awaiting them one at a time) lets the underlying I/O overlap instead of
17
+ // serializing on a tree with many entries. 32 caps outstanding file
18
+ // descriptors well clear of typical `ulimit -n` defaults.
19
+ const STAT_CONCURRENCY = 32;
20
+ const listFiles = (roots) => Effect.gen(function* () {
21
+ const out = [];
22
+ for (const root of roots) {
23
+ const present = yield* fs.exists(root);
24
+ if (!present) {
25
+ continue;
26
+ }
27
+ const entries = yield* fs.readDirectory(root, { recursive: true });
28
+ const abses = yield* Effect.forEach(entries, (entry) => {
29
+ const abs = path.join(root, entry);
30
+ return fs.stat(abs).pipe(Effect.map((info) => (info.type === 'File' ? abs : null)));
31
+ }, { concurrency: STAT_CONCURRENCY });
32
+ for (const abs of abses) {
33
+ if (abs !== null) {
34
+ // Normalise to POSIX so the pure planners see `/` paths on every OS.
35
+ out.push(toPosix(abs));
36
+ }
37
+ }
38
+ }
39
+ return out;
40
+ }).pipe(Effect.orDie);
41
+ const stat = (abs) => fs.stat(abs).pipe(Effect.map((info) => ({
42
+ mtimeMs: Option.match(info.mtime, { onNone: () => 0, onSome: (d) => d.getTime() }),
43
+ sizeBytes: Number(info.size),
44
+ })), Effect.orDie);
45
+ return {
46
+ deleteFile: (abs) => fs.remove(abs).pipe(Effect.orDie),
47
+ exists: (abs) => fs.exists(abs).pipe(Effect.orDie),
48
+ listFiles,
49
+ readFile: (abs) => fs.readFileString(abs).pipe(Effect.orDie),
50
+ stat,
51
+ writeFile: (abs, content) => fs.writeFileString(abs, content).pipe(Effect.orDie),
52
+ };
53
+ }));
54
+ /**
55
+ * In-memory DocsFs layer for tests. `files` maps absolute path -> file.
56
+ * `writeFile` mutates the backing map so fix behaviour can be asserted.
57
+ */
58
+ export const makeTestDocsFs = (files) => {
59
+ const store = new Map(Object.entries(files));
60
+ const dirsOf = () => {
61
+ const dirs = new Set();
62
+ for (const abs of store.keys()) {
63
+ let dir = abs.slice(0, abs.lastIndexOf('/'));
64
+ while (dir.length > 0 && !dirs.has(dir)) {
65
+ dirs.add(dir);
66
+ dir = dir.slice(0, dir.lastIndexOf('/'));
67
+ }
68
+ }
69
+ return dirs;
70
+ };
71
+ const service = {
72
+ deleteFile: (abs) => Effect.sync(() => void store.delete(abs)),
73
+ exists: (abs) => Effect.sync(() => store.has(abs) || dirsOf().has(abs)),
74
+ listFiles: (roots) => Effect.sync(() => [...store.keys()].filter((p) => roots.some((r) => p.startsWith(`${r}/`) || p === r))),
75
+ readFile: (abs) => Effect.sync(() => {
76
+ const f = store.get(abs);
77
+ if (!f) {
78
+ throw new Error(`ENOENT: ${abs}`);
79
+ }
80
+ return f.content;
81
+ }),
82
+ stat: (abs) => Effect.sync(() => {
83
+ const f = store.get(abs);
84
+ if (!f) {
85
+ throw new Error(`ENOENT: ${abs}`);
86
+ }
87
+ return { mtimeMs: f.mtimeMs, sizeBytes: Buffer.byteLength(f.content) };
88
+ }),
89
+ writeFile: (abs, content) => Effect.sync(() => {
90
+ store.set(abs, { content, mtimeMs: (store.get(abs)?.mtimeMs ?? 0) + 1 });
91
+ }),
92
+ };
93
+ return Layer.succeed(DocsFs, service);
94
+ };
95
+ //# sourceMappingURL=DocsFs.js.map