first-tree 0.0.2 → 0.0.3

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/README.md +73 -39
  2. package/dist/cli.js +27 -13
  3. package/dist/help-xEI-s9iN.js +25 -0
  4. package/dist/init-DtOjj0wc.js +253 -0
  5. package/dist/installer-rcZpGLnM.js +47 -0
  6. package/dist/onboarding-6Fr5Gkrk.js +2 -0
  7. package/dist/onboarding-B9zPGvvG.js +10 -0
  8. package/dist/repo-BTJG8BU1.js +187 -0
  9. package/dist/upgrade-COGgI7Rj.js +96 -0
  10. package/dist/{verify-CSRIkuoM.js → verify-CxN6JiV9.js} +53 -24
  11. package/package.json +33 -10
  12. package/skills/first-tree/SKILL.md +109 -0
  13. package/skills/first-tree/agents/openai.yaml +4 -0
  14. package/skills/first-tree/assets/framework/VERSION +1 -0
  15. package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
  16. package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
  17. package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
  18. package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
  19. package/skills/first-tree/assets/framework/helpers/run-review.ts +179 -0
  20. package/skills/first-tree/assets/framework/manifest.json +11 -0
  21. package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
  22. package/skills/first-tree/assets/framework/templates/agent.md.template +48 -0
  23. package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
  24. package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
  25. package/skills/first-tree/assets/framework/templates/root-node.md.template +38 -0
  26. package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
  27. package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
  28. package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
  29. package/skills/first-tree/engine/commands/help.ts +32 -0
  30. package/skills/first-tree/engine/commands/init.ts +1 -0
  31. package/skills/first-tree/engine/commands/upgrade.ts +1 -0
  32. package/skills/first-tree/engine/commands/verify.ts +1 -0
  33. package/skills/first-tree/engine/init.ts +145 -0
  34. package/skills/first-tree/engine/onboarding.ts +10 -0
  35. package/skills/first-tree/engine/repo.ts +184 -0
  36. package/skills/first-tree/engine/rules/agent-instructions.ts +37 -0
  37. package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
  38. package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
  39. package/skills/first-tree/engine/rules/framework.ts +13 -0
  40. package/skills/first-tree/engine/rules/index.ts +41 -0
  41. package/skills/first-tree/engine/rules/members.ts +21 -0
  42. package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
  43. package/skills/first-tree/engine/rules/root-node.ts +41 -0
  44. package/skills/first-tree/engine/runtime/adapters.ts +22 -0
  45. package/skills/first-tree/engine/runtime/asset-loader.ts +134 -0
  46. package/skills/first-tree/engine/runtime/installer.ts +82 -0
  47. package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
  48. package/skills/first-tree/engine/upgrade.ts +176 -0
  49. package/skills/first-tree/engine/validators/members.ts +215 -0
  50. package/skills/first-tree/engine/validators/nodes.ts +514 -0
  51. package/skills/first-tree/engine/verify.ts +97 -0
  52. package/skills/first-tree/references/about.md +36 -0
  53. package/skills/first-tree/references/maintainer-architecture.md +59 -0
  54. package/skills/first-tree/references/maintainer-build-and-distribution.md +56 -0
  55. package/skills/first-tree/references/maintainer-testing.md +58 -0
  56. package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
  57. package/skills/first-tree/references/onboarding.md +162 -0
  58. package/skills/first-tree/references/ownership-and-naming.md +94 -0
  59. package/skills/first-tree/references/principles.md +113 -0
  60. package/skills/first-tree/references/source-map.md +94 -0
  61. package/skills/first-tree/references/upgrade-contract.md +85 -0
  62. package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
  63. package/skills/first-tree/scripts/quick_validate.py +95 -0
  64. package/skills/first-tree/scripts/run-local-cli.sh +35 -0
  65. package/skills/first-tree/tests/asset-loader.test.ts +75 -0
  66. package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
  67. package/skills/first-tree/tests/helpers.ts +149 -0
  68. package/skills/first-tree/tests/init.test.ts +153 -0
  69. package/skills/first-tree/tests/repo.test.ts +362 -0
  70. package/skills/first-tree/tests/rules.test.ts +394 -0
  71. package/skills/first-tree/tests/run-review.test.ts +155 -0
  72. package/skills/first-tree/tests/skill-artifacts.test.ts +307 -0
  73. package/skills/first-tree/tests/thin-cli.test.ts +59 -0
  74. package/skills/first-tree/tests/upgrade.test.ts +89 -0
  75. package/skills/first-tree/tests/validate-members.test.ts +224 -0
  76. package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
  77. package/skills/first-tree/tests/verify.test.ts +142 -0
  78. package/dist/init-CE_944sb.js +0 -283
  79. package/dist/repo-BByc3VvM.js +0 -111
  80. package/dist/upgrade-Chr7z0CY.js +0 -82
@@ -0,0 +1,153 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { formatTaskList, writeProgress, runInit } from "#skill/engine/init.js";
5
+ import { Repo } from "#skill/engine/repo.js";
6
+ import {
7
+ FRAMEWORK_VERSION,
8
+ INSTALLED_PROGRESS,
9
+ LEGACY_PROGRESS,
10
+ } from "#skill/engine/runtime/asset-loader.js";
11
+ import {
12
+ useTmpDir,
13
+ makeFramework,
14
+ makeLegacyFramework,
15
+ makeSourceSkill,
16
+ } from "./helpers.js";
17
+
18
+ // --- formatTaskList ---
19
+
20
+ describe("formatTaskList", () => {
21
+ it("produces markdown heading", () => {
22
+ const groups = [
23
+ { group: "Framework", order: 1, tasks: ["Install framework"] },
24
+ ];
25
+ const output = formatTaskList(groups);
26
+ expect(output).toMatch(/^# Context Tree Init/);
27
+ });
28
+
29
+ it("includes group heading", () => {
30
+ const groups = [
31
+ { group: "Framework", order: 1, tasks: ["Install framework"] },
32
+ ];
33
+ const output = formatTaskList(groups);
34
+ expect(output).toContain("## Framework");
35
+ });
36
+
37
+ it("includes task as checkbox", () => {
38
+ const groups = [
39
+ { group: "Root Node", order: 2, tasks: ["Fix title"] },
40
+ ];
41
+ const output = formatTaskList(groups);
42
+ expect(output).toContain("- [ ] Fix title");
43
+ });
44
+
45
+ it("handles multiple groups", () => {
46
+ const groups = [
47
+ { group: "A", order: 1, tasks: ["task-a1", "task-a2"] },
48
+ { group: "B", order: 2, tasks: ["task-b1"] },
49
+ ];
50
+ const output = formatTaskList(groups);
51
+ expect(output).toContain("## A");
52
+ expect(output).toContain("## B");
53
+ expect(output).toContain("- [ ] task-a1");
54
+ expect(output).toContain("- [ ] task-a2");
55
+ expect(output).toContain("- [ ] task-b1");
56
+ });
57
+
58
+ it("includes verification section", () => {
59
+ const groups = [{ group: "G", order: 1, tasks: ["t"] }];
60
+ const output = formatTaskList(groups);
61
+ expect(output).toContain("## Verification");
62
+ expect(output).toContain("context-tree verify");
63
+ });
64
+
65
+ it("handles empty groups", () => {
66
+ const output = formatTaskList([]);
67
+ expect(output).toContain("# Context Tree Init");
68
+ expect(output).toContain("## Verification");
69
+ });
70
+ });
71
+
72
+ // --- writeProgress ---
73
+
74
+ describe("writeProgress", () => {
75
+ it("writes to correct path", () => {
76
+ const tmp = useTmpDir();
77
+ const repo = new Repo(tmp.path);
78
+ writeProgress(repo, "# hello\n");
79
+ const progress = join(tmp.path, INSTALLED_PROGRESS);
80
+ expect(readFileSync(progress, "utf-8")).toBe("# hello\n");
81
+ });
82
+
83
+ it("creates directory if missing", () => {
84
+ const tmp = useTmpDir();
85
+ const repo = new Repo(tmp.path);
86
+ writeProgress(repo, "content");
87
+ const progress = join(tmp.path, INSTALLED_PROGRESS);
88
+ expect(readFileSync(progress, "utf-8")).toBe("content");
89
+ });
90
+
91
+ it("overwrites existing file", () => {
92
+ const tmp = useTmpDir();
93
+ mkdirSync(join(tmp.path, "skills", "first-tree"), {
94
+ recursive: true,
95
+ });
96
+ writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "old");
97
+ const repo = new Repo(tmp.path);
98
+ writeProgress(repo, "new");
99
+ expect(readFileSync(join(tmp.path, INSTALLED_PROGRESS), "utf-8")).toBe("new");
100
+ });
101
+
102
+ it("keeps using the legacy progress path for legacy repos", () => {
103
+ const tmp = useTmpDir();
104
+ makeLegacyFramework(tmp.path);
105
+ const repo = new Repo(tmp.path);
106
+ writeProgress(repo, "legacy");
107
+ expect(readFileSync(join(tmp.path, LEGACY_PROGRESS), "utf-8")).toBe("legacy");
108
+ });
109
+ });
110
+
111
+ // --- runInit — guard logic (no network) ---
112
+
113
+ describe("runInit", () => {
114
+ it("errors when not a git repo", () => {
115
+ const tmp = useTmpDir();
116
+ const repo = new Repo(tmp.path);
117
+ const ret = runInit(repo);
118
+ expect(ret).toBe(1);
119
+ });
120
+
121
+ it("installs the bundled skill and scaffolding when framework is missing", () => {
122
+ const repoDir = useTmpDir();
123
+ const sourceDir = useTmpDir();
124
+ mkdirSync(join(repoDir.path, ".git"));
125
+ makeSourceSkill(sourceDir.path, "0.2.0");
126
+
127
+ const ret = runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path });
128
+
129
+ expect(ret).toBe(0);
130
+ expect(
131
+ existsSync(join(repoDir.path, "skills", "first-tree", "SKILL.md")),
132
+ ).toBe(true);
133
+ expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
134
+ expect(existsSync(join(repoDir.path, "NODE.md"))).toBe(true);
135
+ expect(existsSync(join(repoDir.path, "AGENT.md"))).toBe(true);
136
+ expect(existsSync(join(repoDir.path, "members", "NODE.md"))).toBe(true);
137
+ expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(true);
138
+ });
139
+
140
+ it("skips reinstall when framework exists", () => {
141
+ const tmp = useTmpDir();
142
+ const sourceDir = useTmpDir();
143
+ mkdirSync(join(tmp.path, ".git"));
144
+ makeFramework(tmp.path, "0.1.0");
145
+ makeSourceSkill(sourceDir.path, "0.2.0");
146
+
147
+ const repo = new Repo(tmp.path);
148
+ const ret = runInit(repo, { sourceRoot: sourceDir.path });
149
+
150
+ expect(ret).toBe(0);
151
+ expect(readFileSync(join(tmp.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.1.0");
152
+ });
153
+ });
@@ -0,0 +1,362 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { Repo } from "#skill/engine/repo.js";
5
+ import {
6
+ FRAMEWORK_VERSION,
7
+ INSTALLED_PROGRESS,
8
+ LEGACY_SKILL_PROGRESS,
9
+ LEGACY_SKILL_VERSION,
10
+ LEGACY_PROGRESS,
11
+ LEGACY_VERSION,
12
+ } from "#skill/engine/runtime/asset-loader.js";
13
+ import {
14
+ useTmpDir,
15
+ makeFramework,
16
+ makeLegacyFramework,
17
+ makeLegacyNamedFramework,
18
+ } from "./helpers.js";
19
+
20
+ // --- pathExists ---
21
+
22
+ describe("pathExists", () => {
23
+ it("returns true for existing file", () => {
24
+ const tmp = useTmpDir();
25
+ writeFileSync(join(tmp.path, "file.txt"), "hello");
26
+ const repo = new Repo(tmp.path);
27
+ expect(repo.pathExists("file.txt")).toBe(true);
28
+ });
29
+
30
+ it("returns false for missing file", () => {
31
+ const tmp = useTmpDir();
32
+ const repo = new Repo(tmp.path);
33
+ expect(repo.pathExists("no-such-file.txt")).toBe(false);
34
+ });
35
+ });
36
+
37
+ // --- fileContains ---
38
+
39
+ describe("fileContains", () => {
40
+ it("returns true when text is present", () => {
41
+ const tmp = useTmpDir();
42
+ writeFileSync(join(tmp.path, "f.md"), "hello world");
43
+ const repo = new Repo(tmp.path);
44
+ expect(repo.fileContains("f.md", "hello")).toBe(true);
45
+ });
46
+
47
+ it("returns false when text is missing", () => {
48
+ const tmp = useTmpDir();
49
+ writeFileSync(join(tmp.path, "f.md"), "hello world");
50
+ const repo = new Repo(tmp.path);
51
+ expect(repo.fileContains("f.md", "goodbye")).toBe(false);
52
+ });
53
+
54
+ it("returns false when file is missing", () => {
55
+ const tmp = useTmpDir();
56
+ const repo = new Repo(tmp.path);
57
+ expect(repo.fileContains("missing.md", "anything")).toBe(false);
58
+ });
59
+ });
60
+
61
+ // --- frontmatter ---
62
+
63
+ describe("frontmatter", () => {
64
+ it("parses valid title and owners", () => {
65
+ const tmp = useTmpDir();
66
+ writeFileSync(
67
+ join(tmp.path, "NODE.md"),
68
+ "---\ntitle: My Tree\nowners: [alice, bob]\n---\n# Content\n",
69
+ );
70
+ const repo = new Repo(tmp.path);
71
+ const fm = repo.frontmatter("NODE.md");
72
+ expect(fm).not.toBeNull();
73
+ expect(fm!.title).toBe("My Tree");
74
+ expect(fm!.owners).toEqual(["alice", "bob"]);
75
+ });
76
+
77
+ it("returns null for missing frontmatter", () => {
78
+ const tmp = useTmpDir();
79
+ writeFileSync(
80
+ join(tmp.path, "NODE.md"),
81
+ "# Just a heading\nNo frontmatter here.\n",
82
+ );
83
+ const repo = new Repo(tmp.path);
84
+ expect(repo.frontmatter("NODE.md")).toBeNull();
85
+ });
86
+
87
+ it("handles partial — title only", () => {
88
+ const tmp = useTmpDir();
89
+ writeFileSync(join(tmp.path, "NODE.md"), "---\ntitle: Partial\n---\n");
90
+ const repo = new Repo(tmp.path);
91
+ const fm = repo.frontmatter("NODE.md");
92
+ expect(fm).not.toBeNull();
93
+ expect(fm!.title).toBe("Partial");
94
+ expect(fm!.owners).toBeUndefined();
95
+ });
96
+
97
+ it("handles partial — owners only", () => {
98
+ const tmp = useTmpDir();
99
+ writeFileSync(join(tmp.path, "NODE.md"), "---\nowners: [alice]\n---\n");
100
+ const repo = new Repo(tmp.path);
101
+ const fm = repo.frontmatter("NODE.md");
102
+ expect(fm).not.toBeNull();
103
+ expect(fm!.owners).toEqual(["alice"]);
104
+ expect(fm!.title).toBeUndefined();
105
+ });
106
+
107
+ it("returns null for missing file", () => {
108
+ const tmp = useTmpDir();
109
+ const repo = new Repo(tmp.path);
110
+ expect(repo.frontmatter("NODE.md")).toBeNull();
111
+ });
112
+ });
113
+
114
+ // --- anyAgentConfig ---
115
+
116
+ describe("anyAgentConfig", () => {
117
+ it("returns true with claude settings", () => {
118
+ const tmp = useTmpDir();
119
+ mkdirSync(join(tmp.path, ".claude"));
120
+ writeFileSync(join(tmp.path, ".claude", "settings.json"), "{}");
121
+ const repo = new Repo(tmp.path);
122
+ expect(repo.anyAgentConfig()).toBe(true);
123
+ });
124
+
125
+ it("returns false without any config", () => {
126
+ const tmp = useTmpDir();
127
+ const repo = new Repo(tmp.path);
128
+ expect(repo.anyAgentConfig()).toBe(false);
129
+ });
130
+ });
131
+
132
+ // --- isGitRepo ---
133
+
134
+ describe("isGitRepo", () => {
135
+ it("returns true with .git dir", () => {
136
+ const tmp = useTmpDir();
137
+ mkdirSync(join(tmp.path, ".git"));
138
+ const repo = new Repo(tmp.path);
139
+ expect(repo.isGitRepo()).toBe(true);
140
+ });
141
+
142
+ it("returns false without .git dir", () => {
143
+ const tmp = useTmpDir();
144
+ const repo = new Repo(tmp.path);
145
+ expect(repo.isGitRepo()).toBe(false);
146
+ });
147
+ });
148
+
149
+ // --- hasFramework ---
150
+
151
+ describe("hasFramework", () => {
152
+ it("returns true with installed skill version file", () => {
153
+ const tmp = useTmpDir();
154
+ makeFramework(tmp.path);
155
+ const repo = new Repo(tmp.path);
156
+ expect(repo.hasFramework()).toBe(true);
157
+ });
158
+
159
+ it("returns true with legacy version file", () => {
160
+ const tmp = useTmpDir();
161
+ makeLegacyFramework(tmp.path);
162
+ const repo = new Repo(tmp.path);
163
+ expect(repo.hasFramework()).toBe(true);
164
+ });
165
+
166
+ it("returns true with the previous installed skill name", () => {
167
+ const tmp = useTmpDir();
168
+ makeLegacyNamedFramework(tmp.path);
169
+ const repo = new Repo(tmp.path);
170
+ expect(repo.hasFramework()).toBe(true);
171
+ });
172
+
173
+ it("returns false without VERSION file", () => {
174
+ const tmp = useTmpDir();
175
+ const repo = new Repo(tmp.path);
176
+ expect(repo.hasFramework()).toBe(false);
177
+ });
178
+ });
179
+
180
+ // --- readVersion ---
181
+
182
+ describe("readVersion", () => {
183
+ it("reads the installed skill version", () => {
184
+ const tmp = useTmpDir();
185
+ makeFramework(tmp.path, "0.2.0");
186
+ const repo = new Repo(tmp.path);
187
+ expect(repo.readVersion()).toBe("0.2.0");
188
+ });
189
+
190
+ it("falls back to the legacy version", () => {
191
+ const tmp = useTmpDir();
192
+ makeLegacyFramework(tmp.path, "0.3.0");
193
+ const repo = new Repo(tmp.path);
194
+ expect(repo.readVersion()).toBe("0.3.0");
195
+ });
196
+
197
+ it("reads the previous installed skill version", () => {
198
+ const tmp = useTmpDir();
199
+ makeLegacyNamedFramework(tmp.path, "0.2.5");
200
+ const repo = new Repo(tmp.path);
201
+ expect(repo.readVersion()).toBe("0.2.5");
202
+ });
203
+
204
+ it("returns null when missing", () => {
205
+ const tmp = useTmpDir();
206
+ const repo = new Repo(tmp.path);
207
+ expect(repo.readVersion()).toBeNull();
208
+ });
209
+ });
210
+
211
+ // --- preferredProgressPath / frameworkVersionPath ---
212
+
213
+ describe("path preferences", () => {
214
+ it("prefers the installed-skill paths by default", () => {
215
+ const tmp = useTmpDir();
216
+ const repo = new Repo(tmp.path);
217
+ expect(repo.preferredProgressPath()).toBe(INSTALLED_PROGRESS);
218
+ expect(repo.frameworkVersionPath()).toBe(FRAMEWORK_VERSION);
219
+ });
220
+
221
+ it("switches path preferences for legacy repos", () => {
222
+ const tmp = useTmpDir();
223
+ makeLegacyFramework(tmp.path);
224
+ const repo = new Repo(tmp.path);
225
+ expect(repo.preferredProgressPath()).toBe(LEGACY_PROGRESS);
226
+ expect(repo.frameworkVersionPath()).toBe(LEGACY_VERSION);
227
+ });
228
+
229
+ it("switches path preferences for repos using the previous skill name", () => {
230
+ const tmp = useTmpDir();
231
+ makeLegacyNamedFramework(tmp.path);
232
+ const repo = new Repo(tmp.path);
233
+ expect(repo.preferredProgressPath()).toBe(LEGACY_SKILL_PROGRESS);
234
+ expect(repo.frameworkVersionPath()).toBe(LEGACY_SKILL_VERSION);
235
+ });
236
+ });
237
+
238
+ // --- hasAgentMdMarkers ---
239
+
240
+ describe("hasAgentMdMarkers", () => {
241
+ it("returns true with markers", () => {
242
+ const tmp = useTmpDir();
243
+ writeFileSync(
244
+ join(tmp.path, "AGENT.md"),
245
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
246
+ );
247
+ const repo = new Repo(tmp.path);
248
+ expect(repo.hasAgentMdMarkers()).toBe(true);
249
+ });
250
+
251
+ it("returns false without markers", () => {
252
+ const tmp = useTmpDir();
253
+ writeFileSync(
254
+ join(tmp.path, "AGENT.md"),
255
+ "# Agent instructions\nNo markers here.\n",
256
+ );
257
+ const repo = new Repo(tmp.path);
258
+ expect(repo.hasAgentMdMarkers()).toBe(false);
259
+ });
260
+
261
+ it("returns false when file is missing", () => {
262
+ const tmp = useTmpDir();
263
+ const repo = new Repo(tmp.path);
264
+ expect(repo.hasAgentMdMarkers()).toBe(false);
265
+ });
266
+ });
267
+
268
+ // --- hasMembers ---
269
+
270
+ describe("hasMembers", () => {
271
+ it("returns true with members/NODE.md", () => {
272
+ const tmp = useTmpDir();
273
+ const members = join(tmp.path, "members");
274
+ mkdirSync(members);
275
+ writeFileSync(join(members, "NODE.md"), "---\ntitle: Members\n---\n");
276
+ const repo = new Repo(tmp.path);
277
+ expect(repo.hasMembers()).toBe(true);
278
+ });
279
+
280
+ it("returns false without members dir", () => {
281
+ const tmp = useTmpDir();
282
+ const repo = new Repo(tmp.path);
283
+ expect(repo.hasMembers()).toBe(false);
284
+ });
285
+
286
+ it("returns false with dir but no NODE.md", () => {
287
+ const tmp = useTmpDir();
288
+ mkdirSync(join(tmp.path, "members"));
289
+ const repo = new Repo(tmp.path);
290
+ expect(repo.hasMembers()).toBe(false);
291
+ });
292
+ });
293
+
294
+ // --- memberCount ---
295
+
296
+ describe("memberCount", () => {
297
+ it("returns 0 with no members dir", () => {
298
+ const tmp = useTmpDir();
299
+ const repo = new Repo(tmp.path);
300
+ expect(repo.memberCount()).toBe(0);
301
+ });
302
+
303
+ it("counts one member", () => {
304
+ const tmp = useTmpDir();
305
+ const members = join(tmp.path, "members");
306
+ mkdirSync(members);
307
+ const alice = join(members, "alice");
308
+ mkdirSync(alice);
309
+ writeFileSync(join(alice, "NODE.md"), "---\ntitle: Alice\n---\n");
310
+ const repo = new Repo(tmp.path);
311
+ expect(repo.memberCount()).toBe(1);
312
+ });
313
+
314
+ it("counts two members", () => {
315
+ const tmp = useTmpDir();
316
+ const members = join(tmp.path, "members");
317
+ mkdirSync(members);
318
+ for (const name of ["alice", "bob"]) {
319
+ const d = join(members, name);
320
+ mkdirSync(d);
321
+ writeFileSync(join(d, "NODE.md"), `---\ntitle: ${name}\n---\n`);
322
+ }
323
+ const repo = new Repo(tmp.path);
324
+ expect(repo.memberCount()).toBe(2);
325
+ });
326
+
327
+ it("ignores dirs without NODE.md", () => {
328
+ const tmp = useTmpDir();
329
+ const members = join(tmp.path, "members");
330
+ mkdirSync(members);
331
+ mkdirSync(join(members, "alice")); // no NODE.md
332
+ const bob = join(members, "bob");
333
+ mkdirSync(bob);
334
+ writeFileSync(join(bob, "NODE.md"), "---\ntitle: Bob\n---\n");
335
+ const repo = new Repo(tmp.path);
336
+ expect(repo.memberCount()).toBe(1);
337
+ });
338
+ });
339
+
340
+ // --- hasPlaceholderNode ---
341
+
342
+ describe("hasPlaceholderNode", () => {
343
+ it("returns true with placeholder", () => {
344
+ const tmp = useTmpDir();
345
+ writeFileSync(
346
+ join(tmp.path, "NODE.md"),
347
+ "---\ntitle: My Tree\n---\n<!-- PLACEHOLDER: fill in -->\n",
348
+ );
349
+ const repo = new Repo(tmp.path);
350
+ expect(repo.hasPlaceholderNode()).toBe(true);
351
+ });
352
+
353
+ it("returns false without placeholder", () => {
354
+ const tmp = useTmpDir();
355
+ writeFileSync(
356
+ join(tmp.path, "NODE.md"),
357
+ "---\ntitle: My Tree\n---\n# Real content\n",
358
+ );
359
+ const repo = new Repo(tmp.path);
360
+ expect(repo.hasPlaceholderNode()).toBe(false);
361
+ });
362
+ });