chapterhouse 0.3.23 → 0.3.24

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.
@@ -68,6 +68,47 @@ test("formats named SSE status events", async () => {
68
68
  assert.equal(sse.formatSseEvent("status", { status: "dreaming", message: "Consolidating memories..." }), 'event: status\ndata: {"status":"dreaming","message":"Consolidating memories..."}\n\n');
69
69
  });
70
70
  const repoRoot = process.cwd();
71
+ function getProjectDbPath(testRoot) {
72
+ return join(testRoot, ".chapterhouse", "chapterhouse.db");
73
+ }
74
+ function seedProjectRegistry(testRoot, registry) {
75
+ mkdirSync(join(testRoot, ".chapterhouse"), { recursive: true });
76
+ const db = new Database(getProjectDbPath(testRoot));
77
+ try {
78
+ db.exec(`
79
+ CREATE TABLE IF NOT EXISTS projects (
80
+ slug TEXT PRIMARY KEY,
81
+ cwd TEXT NOT NULL,
82
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
83
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
84
+ )
85
+ `);
86
+ db.prepare("DELETE FROM projects").run();
87
+ const insert = db.prepare(`
88
+ INSERT INTO projects (slug, cwd, created_at, updated_at)
89
+ VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
90
+ `);
91
+ for (const [slug, cwd] of Object.entries(registry).sort(([left], [right]) => left.localeCompare(right))) {
92
+ insert.run(slug, cwd);
93
+ }
94
+ }
95
+ finally {
96
+ db.close();
97
+ }
98
+ }
99
+ function readProjectRegistryRows(testRoot) {
100
+ const dbPath = getProjectDbPath(testRoot);
101
+ if (!existsSync(dbPath)) {
102
+ return [];
103
+ }
104
+ const db = new Database(dbPath, { readonly: true });
105
+ try {
106
+ return db.prepare("SELECT slug, cwd FROM projects ORDER BY slug").all();
107
+ }
108
+ finally {
109
+ db.close();
110
+ }
111
+ }
71
112
  async function getFreePort() {
72
113
  const server = createServer();
73
114
  await new Promise((resolve) => {
@@ -402,19 +443,10 @@ test("server projects route returns an empty list when the registry is missing",
402
443
  });
403
444
  test("server projects route sorts by slug and summarizes rule counts", async () => {
404
445
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
405
- const projectsIndexPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "index.md");
406
- mkdirSync(join(projectsIndexPath, ".."), { recursive: true });
407
- writeFileSync(projectsIndexPath, "---\n"
408
- + "title: Projects\n"
409
- + "summary: Canonical project registry.\n"
410
- + "updated: 2026-05-12\n"
411
- + "---\n\n"
412
- + "# Projects\n\n"
413
- + "## Project Registry\n\n"
414
- + "```yaml\n"
415
- + "zeta: /srv/zeta\n"
416
- + "alpha: /srv/alpha\n"
417
- + "```\n", "utf-8");
446
+ seedProjectRegistry(testRoot, {
447
+ zeta: "/srv/zeta",
448
+ alpha: "/srv/alpha",
449
+ });
418
450
  const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
419
451
  mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
420
452
  writeFileSync(alphaRulesPath, "---\n"
@@ -448,18 +480,9 @@ test("server projects route sorts by slug and summarizes rule counts", async ()
448
480
  });
449
481
  test("server project detail route returns cwd plus hard and soft rules", async () => {
450
482
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
451
- const projectsIndexPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "index.md");
452
- mkdirSync(join(projectsIndexPath, ".."), { recursive: true });
453
- writeFileSync(projectsIndexPath, "---\n"
454
- + "title: Projects\n"
455
- + "summary: Canonical project registry.\n"
456
- + "updated: 2026-05-12\n"
457
- + "---\n\n"
458
- + "# Projects\n\n"
459
- + "## Project Registry\n\n"
460
- + "```yaml\n"
461
- + "alpha: /srv/alpha\n"
462
- + "```\n", "utf-8");
483
+ seedProjectRegistry(testRoot, {
484
+ alpha: "/srv/alpha",
485
+ });
463
486
  const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
464
487
  mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
465
488
  writeFileSync(alphaRulesPath, "---\n"
@@ -509,18 +532,9 @@ test("server project detail route returns 404 for an unknown slug", async () =>
509
532
  });
510
533
  test("server projects create route rejects a duplicate slug", async () => {
511
534
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
512
- const projectsIndexPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "index.md");
513
- mkdirSync(join(projectsIndexPath, ".."), { recursive: true });
514
- writeFileSync(projectsIndexPath, "---\n"
515
- + "title: Projects\n"
516
- + "summary: Canonical project registry.\n"
517
- + "updated: 2026-05-12\n"
518
- + "---\n\n"
519
- + "# Projects\n\n"
520
- + "## Project Registry\n\n"
521
- + "```yaml\n"
522
- + "alpha: /srv/original\n"
523
- + "```\n", "utf-8");
535
+ seedProjectRegistry(testRoot, {
536
+ alpha: "/srv/original",
537
+ });
524
538
  const response = await fetch(`${baseUrl}/api/projects`, {
525
539
  method: "POST",
526
540
  headers: {
@@ -534,33 +548,17 @@ test("server projects create route rejects a duplicate slug", async () => {
534
548
  });
535
549
  assert.equal(response.status, 400);
536
550
  assert.deepEqual(await response.json(), { error: "Project 'alpha' already exists" });
537
- assert.equal(readFileSync(projectsIndexPath, "utf-8"), "---\n"
538
- + "title: Projects\n"
539
- + "summary: Canonical project registry.\n"
540
- + "updated: 2026-05-12\n"
541
- + "---\n\n"
542
- + "# Projects\n\n"
543
- + "## Project Registry\n\n"
544
- + "```yaml\n"
545
- + "alpha: /srv/original\n"
546
- + "```\n");
551
+ assert.deepEqual(readProjectRegistryRows(testRoot), [
552
+ { slug: "alpha", cwd: "/srv/original" },
553
+ ]);
547
554
  }, {}, 60_000);
548
555
  });
549
556
  test("server projects delete route removes the registry entry and rules page", async () => {
550
557
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
551
- const projectsIndexPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "index.md");
552
- mkdirSync(join(projectsIndexPath, ".."), { recursive: true });
553
- writeFileSync(projectsIndexPath, "---\n"
554
- + "title: Projects\n"
555
- + "summary: Canonical project registry.\n"
556
- + "updated: 2026-05-12\n"
557
- + "---\n\n"
558
- + "# Projects\n\n"
559
- + "## Project Registry\n\n"
560
- + "```yaml\n"
561
- + "alpha: /srv/alpha\n"
562
- + "beta: /srv/beta\n"
563
- + "```\n", "utf-8");
558
+ seedProjectRegistry(testRoot, {
559
+ alpha: "/srv/alpha",
560
+ beta: "/srv/beta",
561
+ });
564
562
  const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
565
563
  mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
566
564
  writeFileSync(alphaRulesPath, "---\n"
@@ -574,16 +572,9 @@ test("server projects delete route removes the registry entry and rules page", a
574
572
  });
575
573
  assert.equal(response.status, 200);
576
574
  assert.deepEqual(await response.json(), { ok: true, slug: "alpha" });
577
- assert.equal(readFileSync(projectsIndexPath, "utf-8"), "---\n"
578
- + "title: Projects\n"
579
- + "summary: Canonical project registry.\n"
580
- + "updated: 2026-05-12\n"
581
- + "---\n\n"
582
- + "# Projects\n\n"
583
- + "## Project Registry\n\n"
584
- + "```yaml\n"
585
- + "beta: /srv/beta\n"
586
- + "```\n");
575
+ assert.deepEqual(readProjectRegistryRows(testRoot), [
576
+ { slug: "beta", cwd: "/srv/beta" },
577
+ ]);
587
578
  assert.equal(existsSync(alphaRulesPath), false);
588
579
  }, {}, 60_000);
589
580
  });
@@ -599,18 +590,9 @@ test("server projects delete route returns 404 for an unknown slug", async () =>
599
590
  });
600
591
  test("server project hard-rules update route rewrites only hard-rule frontmatter fields", async () => {
601
592
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
602
- const projectsIndexPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "index.md");
603
- mkdirSync(join(projectsIndexPath, ".."), { recursive: true });
604
- writeFileSync(projectsIndexPath, "---\n"
605
- + "title: Projects\n"
606
- + "summary: Canonical project registry.\n"
607
- + "updated: 2026-05-12\n"
608
- + "---\n\n"
609
- + "# Projects\n\n"
610
- + "## Project Registry\n\n"
611
- + "```yaml\n"
612
- + "alpha: /srv/alpha\n"
613
- + "```\n", "utf-8");
593
+ seedProjectRegistry(testRoot, {
594
+ alpha: "/srv/alpha",
595
+ });
614
596
  const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
615
597
  mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
616
598
  writeFileSync(alphaRulesPath, "---\n"
@@ -688,18 +670,9 @@ test("server project hard-rules update route rewrites only hard-rule frontmatter
688
670
  });
689
671
  test("server project soft-rules update route rewrites the body while preserving frontmatter", async () => {
690
672
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
691
- const projectsIndexPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "index.md");
692
- mkdirSync(join(projectsIndexPath, ".."), { recursive: true });
693
- writeFileSync(projectsIndexPath, "---\n"
694
- + "title: Projects\n"
695
- + "summary: Canonical project registry.\n"
696
- + "updated: 2026-05-12\n"
697
- + "---\n\n"
698
- + "# Projects\n\n"
699
- + "## Project Registry\n\n"
700
- + "```yaml\n"
701
- + "alpha: /srv/alpha\n"
702
- + "```\n", "utf-8");
673
+ seedProjectRegistry(testRoot, {
674
+ alpha: "/srv/alpha",
675
+ });
703
676
  const alphaRulesPath = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "alpha", "rules.md");
704
677
  mkdirSync(join(alphaRulesPath, ".."), { recursive: true });
705
678
  writeFileSync(alphaRulesPath, "---\n"
package/dist/store/db.js CHANGED
@@ -44,6 +44,14 @@ export function getDb() {
44
44
  started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
45
45
  completed_at DATETIME
46
46
  )
47
+ `);
48
+ db.exec(`
49
+ CREATE TABLE IF NOT EXISTS projects (
50
+ slug TEXT PRIMARY KEY,
51
+ cwd TEXT NOT NULL,
52
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
53
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
54
+ )
47
55
  `);
48
56
  db.exec(`
49
57
  CREATE TABLE IF NOT EXISTS max_state (
@@ -32,6 +32,7 @@ test("getDb initializes schema, state helpers, and conversation formatting", asy
32
32
  "worker_sessions",
33
33
  "agent_sessions",
34
34
  "agent_tasks",
35
+ "projects",
35
36
  "max_state",
36
37
  "conversation_log",
37
38
  "memories",
@@ -1,47 +1,91 @@
1
1
  import { isAbsolute } from "node:path";
2
- import { readPage, writePage } from "./fs.js";
3
- const PROJECTS_INDEX_PATH = "pages/projects/index.md";
2
+ import { getDb } from "../store/db.js";
3
+ import { childLogger } from "../util/logger.js";
4
+ import { deletePage, pageExists, readPage } from "./fs.js";
5
+ const log = childLogger("project-registry");
6
+ const LEGACY_PROJECTS_INDEX_PATH = "pages/projects/index.md";
4
7
  const REGISTRY_HEADING = "## Project Registry";
5
8
  const OPENING_FENCE = "```yaml";
6
9
  const CLOSING_FENCE = "```";
7
10
  const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
8
11
  export function loadRegistry() {
9
- const content = readPage(PROJECTS_INDEX_PATH);
10
- if (!content)
11
- return {};
12
- const section = parseRegistrySection(content);
13
- if (!section)
14
- return {};
15
- return parseRegistryBlock(section.blockLines);
12
+ ensureRegistryMigrated();
13
+ const rows = getDb()
14
+ .prepare("SELECT slug, cwd FROM projects ORDER BY slug")
15
+ .all();
16
+ return Object.fromEntries(rows.map(({ slug, cwd }) => [slug, cwd]));
16
17
  }
17
18
  export function saveRegistry(registry) {
18
19
  validateRegistry(registry);
19
- const renderedSection = renderRegistrySection(registry);
20
- const current = readPage(PROJECTS_INDEX_PATH);
21
- if (!current) {
22
- writePage(PROJECTS_INDEX_PATH, `${renderedSection}\n`);
20
+ ensureRegistryMigrated();
21
+ const entries = Object.entries(registry).sort(([left], [right]) => left.localeCompare(right));
22
+ const db = getDb();
23
+ const save = db.transaction(() => {
24
+ db.prepare("DELETE FROM projects").run();
25
+ const insert = db.prepare(`
26
+ INSERT INTO projects (slug, cwd, created_at, updated_at)
27
+ VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
28
+ `);
29
+ for (const [slug, cwd] of entries) {
30
+ insert.run(slug, cwd);
31
+ }
32
+ });
33
+ save();
34
+ removeLegacyRegistryFile("Removed legacy wiki registry after SQLite save");
35
+ }
36
+ export function assertValidProjectSlug(slug) {
37
+ if (!SLUG_RE.test(slug)) {
38
+ throw new Error(`Project registry has invalid project slug '${slug}'. Expected a lowercase slug.`);
39
+ }
40
+ }
41
+ function ensureRegistryMigrated() {
42
+ const db = getDb();
43
+ let migratedRegistry;
44
+ const migrate = db.transaction(() => {
45
+ const row = db.prepare("SELECT COUNT(*) AS count FROM projects").get();
46
+ if (row.count > 0 || !pageExists(LEGACY_PROJECTS_INDEX_PATH)) {
47
+ return;
48
+ }
49
+ const legacyContent = readPage(LEGACY_PROJECTS_INDEX_PATH);
50
+ if (!legacyContent) {
51
+ return;
52
+ }
53
+ const registry = parseLegacyRegistry(legacyContent);
54
+ if (!registry) {
55
+ log.warn({ path: LEGACY_PROJECTS_INDEX_PATH }, "Legacy projects page had no registry section; skipping SQLite migration");
56
+ return;
57
+ }
58
+ const insert = db.prepare(`
59
+ INSERT INTO projects (slug, cwd, created_at, updated_at)
60
+ VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
61
+ `);
62
+ for (const [slug, cwd] of Object.entries(registry).sort(([left], [right]) => left.localeCompare(right))) {
63
+ insert.run(slug, cwd);
64
+ }
65
+ migratedRegistry = registry;
66
+ });
67
+ migrate.immediate();
68
+ if (!migratedRegistry) {
23
69
  return;
24
70
  }
25
- const section = parseRegistrySection(current);
26
- if (!section) {
27
- const trimmed = stripTrailingBlankLines(normalizeLineEndings(current));
28
- const prefix = trimmed ? `${trimmed}\n\n` : "";
29
- writePage(PROJECTS_INDEX_PATH, `${prefix}${renderedSection}\n`);
71
+ removeLegacyRegistryFile("Migrated project registry from wiki to SQLite", Object.keys(migratedRegistry).length);
72
+ }
73
+ function removeLegacyRegistryFile(message, count) {
74
+ if (!deletePage(LEGACY_PROJECTS_INDEX_PATH)) {
30
75
  return;
31
76
  }
32
- const before = stripTrailingBlankLines(section.before.join("\n"));
33
- const after = stripTrailingBlankLines(stripLeadingBlankLines(section.after).join("\n"));
34
- const pieces = [];
35
- if (before) {
36
- pieces.push(before);
37
- pieces.push("");
77
+ if (typeof count === "number") {
78
+ log.info({ count, path: LEGACY_PROJECTS_INDEX_PATH }, message);
79
+ return;
38
80
  }
39
- pieces.push(renderedSection);
40
- if (after) {
41
- pieces.push("");
42
- pieces.push(after);
81
+ log.info({ path: LEGACY_PROJECTS_INDEX_PATH }, message);
82
+ }
83
+ function parseLegacyRegistry(content) {
84
+ const section = parseRegistrySection(content);
85
+ if (!section) {
86
+ return undefined;
43
87
  }
44
- writePage(PROJECTS_INDEX_PATH, `${pieces.join("\n")}\n`);
88
+ return parseRegistryBlock(section.blockLines);
45
89
  }
46
90
  function parseRegistrySection(content) {
47
91
  const normalized = normalizeLineEndings(content);
@@ -81,11 +125,7 @@ function parseRegistrySection(content) {
81
125
  throw new Error("Project registry is malformed: unexpected content after the fenced block.");
82
126
  }
83
127
  }
84
- return {
85
- before: lines.slice(0, headingIndex),
86
- blockLines,
87
- after: lines.slice(sectionEnd),
88
- };
128
+ return { blockLines };
89
129
  }
90
130
  function parseRegistryBlock(lines) {
91
131
  const registry = {};
@@ -114,28 +154,11 @@ function validateRegistry(registry) {
114
154
  validatePath(path);
115
155
  }
116
156
  }
117
- export function assertValidProjectSlug(slug) {
118
- if (!SLUG_RE.test(slug)) {
119
- throw new Error(`Project registry has invalid project slug '${slug}'. Expected a lowercase slug.`);
120
- }
121
- }
122
157
  function validatePath(path) {
123
158
  if (!path || !isAbsolute(path)) {
124
159
  throw new Error(`Project registry path '${path}' must be an absolute path.`);
125
160
  }
126
161
  }
127
- function renderRegistrySection(registry) {
128
- const lines = [
129
- REGISTRY_HEADING,
130
- "",
131
- OPENING_FENCE,
132
- ...Object.keys(registry)
133
- .sort()
134
- .map((slug) => `${slug}: ${registry[slug]}`),
135
- CLOSING_FENCE,
136
- ];
137
- return lines.join("\n");
138
- }
139
162
  function findNextHeading(lines, startIndex) {
140
163
  for (let index = startIndex; index < lines.length; index += 1) {
141
164
  if (/^##\s+/.test(lines[index])) {
@@ -147,14 +170,4 @@ function findNextHeading(lines, startIndex) {
147
170
  function normalizeLineEndings(content) {
148
171
  return content.replace(/\r\n/g, "\n");
149
172
  }
150
- function stripTrailingBlankLines(content) {
151
- return content.replace(/\n+$/g, "");
152
- }
153
- function stripLeadingBlankLines(lines) {
154
- let start = 0;
155
- while (start < lines.length && lines[start].trim() === "") {
156
- start += 1;
157
- }
158
- return lines.slice(start);
159
- }
160
173
  //# sourceMappingURL=project-registry.js.map
@@ -6,67 +6,115 @@ const repoRoot = process.cwd();
6
6
  const sandboxRoot = join(repoRoot, ".test-work", `wiki-project-registry-${process.pid}`);
7
7
  process.env.CHAPTERHOUSE_HOME = sandboxRoot;
8
8
  async function loadModules() {
9
- const nonce = `${Date.now()}-${Math.random()}`;
10
- const projectRegistry = await import(new URL(`./project-registry.js?case=${nonce}`, import.meta.url).href);
11
- const wikiFs = await import(new URL(`./fs.js?case=${nonce}`, import.meta.url).href);
12
- return { projectRegistry, wikiFs };
9
+ const projectRegistry = await import(new URL("./project-registry.js", import.meta.url).href);
10
+ const wikiFs = await import(new URL("./fs.js", import.meta.url).href);
11
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
12
+ return { projectRegistry, wikiFs, dbModule };
13
13
  }
14
14
  function resetSandbox() {
15
15
  mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
16
16
  rmSync(sandboxRoot, { recursive: true, force: true });
17
17
  }
18
- test.beforeEach(() => {
18
+ test.beforeEach(async () => {
19
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
20
+ dbModule.closeDb();
19
21
  resetSandbox();
20
22
  });
21
- test.after(() => {
23
+ test.after(async () => {
24
+ const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
25
+ dbModule.closeDb();
22
26
  rmSync(sandboxRoot, { recursive: true, force: true });
23
27
  });
24
- test("loadRegistry returns an empty object when pages/projects/index.md is absent", async () => {
25
- const { projectRegistry } = await loadModules();
26
- assert.deepEqual(projectRegistry.loadRegistry(), {});
28
+ test("loadRegistry returns an empty object when the projects table is empty", async () => {
29
+ const { projectRegistry, dbModule } = await loadModules();
30
+ try {
31
+ assert.deepEqual(projectRegistry.loadRegistry(), {});
32
+ }
33
+ finally {
34
+ dbModule.closeDb();
35
+ }
27
36
  });
28
- test("loadRegistry returns an empty object when the projects page has no registry section", async () => {
29
- const { projectRegistry, wikiFs } = await loadModules();
30
- wikiFs.writePage("pages/projects/index.md", "---\ntitle: Projects\nsummary: Project pages live here.\nupdated: 2026-05-12\n---\n\n# Projects\n\nTracked project pages.\n");
31
- assert.deepEqual(projectRegistry.loadRegistry(), {});
37
+ test("saveRegistry persists the registry in SQLite", async () => {
38
+ const { projectRegistry, dbModule, wikiFs } = await loadModules();
39
+ try {
40
+ projectRegistry.saveRegistry({
41
+ "docs-site": "/home/bjk/projects/docs-site",
42
+ chapterhouse: "/home/bjk/projects/chapterhouse",
43
+ });
44
+ assert.deepEqual(projectRegistry.loadRegistry(), {
45
+ chapterhouse: "/home/bjk/projects/chapterhouse",
46
+ "docs-site": "/home/bjk/projects/docs-site",
47
+ });
48
+ assert.equal(wikiFs.readPage("pages/projects/index.md"), undefined);
49
+ assert.deepEqual(dbModule.getDb().prepare("SELECT slug, cwd FROM projects ORDER BY slug").all(), [
50
+ { slug: "chapterhouse", cwd: "/home/bjk/projects/chapterhouse" },
51
+ { slug: "docs-site", cwd: "/home/bjk/projects/docs-site" },
52
+ ]);
53
+ }
54
+ finally {
55
+ dbModule.closeDb();
56
+ }
32
57
  });
33
- test("loadRegistry parses the fenced yaml registry block", async () => {
34
- const { projectRegistry, wikiFs } = await loadModules();
35
- wikiFs.writePage("pages/projects/index.md", "---\ntitle: Projects\nsummary: Canonical project registry.\nupdated: 2026-05-12\n---\n\n# Projects\n\n## Project Registry\n\n```yaml\nchapterhouse: /home/bjk/projects/chapterhouse\ndocs-site: /home/bjk/Documents/docs site\n```\n");
36
- assert.deepEqual(projectRegistry.loadRegistry(), {
37
- chapterhouse: "/home/bjk/projects/chapterhouse",
38
- "docs-site": "/home/bjk/Documents/docs site",
39
- });
58
+ test("saveRegistry replaces prior SQLite-backed registry contents", async () => {
59
+ const { projectRegistry, dbModule } = await loadModules();
60
+ try {
61
+ projectRegistry.saveRegistry({
62
+ alpha: "/srv/alpha",
63
+ zeta: "/srv/zeta",
64
+ });
65
+ projectRegistry.saveRegistry({
66
+ beta: "/srv/beta",
67
+ alpha: "/srv/alpha",
68
+ });
69
+ assert.deepEqual(projectRegistry.loadRegistry(), {
70
+ alpha: "/srv/alpha",
71
+ beta: "/srv/beta",
72
+ });
73
+ assert.deepEqual(dbModule.getDb().prepare("SELECT slug, cwd FROM projects ORDER BY slug").all(), [
74
+ { slug: "alpha", cwd: "/srv/alpha" },
75
+ { slug: "beta", cwd: "/srv/beta" },
76
+ ]);
77
+ }
78
+ finally {
79
+ dbModule.closeDb();
80
+ }
40
81
  });
41
- test("loadRegistry rejects malformed registry content", async () => {
42
- const { projectRegistry, wikiFs } = await loadModules();
43
- wikiFs.writePage("pages/projects/index.md", "# Projects\n\n## Project Registry\n\n```yaml\nChapterHouse: /home/bjk/projects/chapterhouse\nrelative: ./docs-site\nbroken line\nchapterhouse: /home/bjk/projects/chapterhouse\n```\n");
44
- assert.throws(() => projectRegistry.loadRegistry(), /invalid project slug|absolute path|malformed registry line|duplicate project slug/);
45
- });
46
- test("saveRegistry creates a new registry page with deterministic ordering", async () => {
47
- const { projectRegistry, wikiFs } = await loadModules();
48
- projectRegistry.saveRegistry({
49
- "docs-site": "/home/bjk/projects/docs-site",
50
- chapterhouse: "/home/bjk/projects/chapterhouse",
51
- });
52
- assert.equal(wikiFs.readPage("pages/projects/index.md"), "## Project Registry\n\n```yaml\nchapterhouse: /home/bjk/projects/chapterhouse\ndocs-site: /home/bjk/projects/docs-site\n```\n");
53
- });
54
- test("saveRegistry rewrites only the registry section and preserves surrounding content", async () => {
55
- const { projectRegistry, wikiFs } = await loadModules();
56
- wikiFs.writePage("pages/projects/index.md", "---\ntitle: Projects\nsummary: Canonical project registry.\nupdated: 2026-05-12\n---\n\n# Projects\n\nIntro paragraph.\n\n## Project Registry\n\n```yaml\nzeta: /srv/zeta\nalpha: /srv/alpha\n```\n\n## Notes\n\nKeep this section untouched.\n");
57
- projectRegistry.saveRegistry({
58
- beta: "/srv/beta",
59
- alpha: "/srv/alpha",
60
- });
61
- assert.equal(wikiFs.readPage("pages/projects/index.md"), "---\ntitle: Projects\nsummary: Canonical project registry.\nupdated: 2026-05-12\n---\n\n# Projects\n\nIntro paragraph.\n\n## Project Registry\n\n```yaml\nalpha: /srv/alpha\nbeta: /srv/beta\n```\n\n## Notes\n\nKeep this section untouched.\n");
62
- assert.deepEqual(projectRegistry.loadRegistry(), {
63
- alpha: "/srv/alpha",
64
- beta: "/srv/beta",
65
- });
82
+ test("loadRegistry migrates the legacy wiki registry into SQLite and removes the file", async () => {
83
+ const { projectRegistry, wikiFs, dbModule } = await loadModules();
84
+ try {
85
+ wikiFs.writePage("pages/projects/index.md", "---\n"
86
+ + "title: Projects\n"
87
+ + "summary: Canonical project registry.\n"
88
+ + "updated: 2026-05-12\n"
89
+ + "---\n\n"
90
+ + "# Projects\n\n"
91
+ + "## Project Registry\n\n"
92
+ + "```yaml\n"
93
+ + "chapterhouse: /home/bjk/projects/chapterhouse\n"
94
+ + "docs-site: /home/bjk/Documents/docs site\n"
95
+ + "```\n");
96
+ assert.deepEqual(projectRegistry.loadRegistry(), {
97
+ chapterhouse: "/home/bjk/projects/chapterhouse",
98
+ "docs-site": "/home/bjk/Documents/docs site",
99
+ });
100
+ assert.equal(wikiFs.pageExists("pages/projects/index.md"), false);
101
+ assert.deepEqual(dbModule.getDb().prepare("SELECT slug, cwd FROM projects ORDER BY slug").all(), [
102
+ { slug: "chapterhouse", cwd: "/home/bjk/projects/chapterhouse" },
103
+ { slug: "docs-site", cwd: "/home/bjk/Documents/docs site" },
104
+ ]);
105
+ }
106
+ finally {
107
+ dbModule.closeDb();
108
+ }
66
109
  });
67
110
  test("saveRegistry rejects invalid slugs and non-absolute paths", async () => {
68
- const { projectRegistry } = await loadModules();
69
- assert.throws(() => projectRegistry.saveRegistry({ ChapterHouse: "/home/bjk/projects/chapterhouse" }), /invalid project slug/);
70
- assert.throws(() => projectRegistry.saveRegistry({ chapterhouse: "./relative" }), /absolute path/);
111
+ const { projectRegistry, dbModule } = await loadModules();
112
+ try {
113
+ assert.throws(() => projectRegistry.saveRegistry({ ChapterHouse: "/home/bjk/projects/chapterhouse" }), /invalid project slug/);
114
+ assert.throws(() => projectRegistry.saveRegistry({ chapterhouse: "./relative" }), /absolute path/);
115
+ }
116
+ finally {
117
+ dbModule.closeDb();
118
+ }
71
119
  });
72
120
  //# sourceMappingURL=project-registry.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
4
4
  "description": "Chapterhouse — a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
5
5
  "bin": {
6
6
  "chapterhouse": "dist/cli.js"