chapterhouse 0.3.18 → 0.3.19

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.
@@ -0,0 +1,350 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, rmSync, utimesSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ const repoRoot = process.cwd();
6
+ const sandboxRoot = join(repoRoot, ".test-work", `wiki-fix-${process.pid}`);
7
+ process.env.CHAPTERHOUSE_HOME = sandboxRoot;
8
+ process.env.CHAPTERHOUSE_AGENT_NAME = "wiki-fix-test-agent";
9
+ async function loadModules() {
10
+ const nonce = `${Date.now()}-${Math.random()}`;
11
+ const fix = await import(new URL(`./fix.js?case=${nonce}`, import.meta.url).href);
12
+ const wikiFs = await import(new URL(`./fs.js?case=${nonce}`, import.meta.url).href);
13
+ return { fix, wikiFs };
14
+ }
15
+ function resetSandbox() {
16
+ mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
17
+ rmSync(sandboxRoot, { recursive: true, force: true });
18
+ }
19
+ function wikiPath(relativePath) {
20
+ return join(sandboxRoot, ".chapterhouse", "wiki", ...relativePath.split("/"));
21
+ }
22
+ function setMtime(relativePath, isoDate) {
23
+ const timestamp = new Date(`${isoDate}T12:00:00.000Z`);
24
+ utimesSync(wikiPath(relativePath), timestamp, timestamp);
25
+ }
26
+ test.beforeEach(() => {
27
+ resetSandbox();
28
+ });
29
+ test.after(() => {
30
+ rmSync(sandboxRoot, { recursive: true, force: true });
31
+ });
32
+ test("fixWiki dry-run previews frontmatter backfills, tag normalization, and autostub marking without writing files", async () => {
33
+ const { fix, wikiFs } = await loadModules();
34
+ wikiFs.ensureWikiStructure();
35
+ wikiFs.writePage("pages/projects/alpha/index.md", `# Alpha Project
36
+
37
+ Alpha project **launch** notes for the team.
38
+
39
+ ## Details
40
+
41
+ One
42
+ Two
43
+ Three
44
+ Four
45
+ Five
46
+ Six
47
+ Seven
48
+ Eight
49
+ `);
50
+ wikiFs.writePage("pages/projects/empty/index.md", `# Empty Page
51
+ `);
52
+ wikiFs.writePage("pages/projects/bravo/index.md", `---
53
+ title: Bravo
54
+ summary: Bravo deployment notes
55
+ updated: 2026-05-10
56
+ tags: [Run Book, Mystery]
57
+ ---
58
+
59
+ # Bravo
60
+
61
+ Deployment notes with enough body to avoid the stub marker.
62
+
63
+ ## Steps
64
+
65
+ One
66
+ Two
67
+ Three
68
+ Four
69
+ Five
70
+ Six
71
+ Seven
72
+ Eight
73
+ `);
74
+ wikiFs.writePage("pages/projects/stub/index.md", `---
75
+ title: Stub Page
76
+ summary: Tiny page
77
+ updated: 2026-05-10
78
+ ---
79
+
80
+ # Stub Page
81
+
82
+ Tiny.
83
+ `);
84
+ wikiFs.writePage("pages/_meta/taxonomy.md", `## Process
85
+ - runbook
86
+ `);
87
+ setMtime("pages/projects/alpha/index.md", "2026-01-02");
88
+ setMtime("pages/projects/empty/index.md", "2026-01-03");
89
+ const beforeAlpha = wikiFs.readPage("pages/projects/alpha/index.md");
90
+ const beforeEmpty = wikiFs.readPage("pages/projects/empty/index.md");
91
+ const beforeBravo = wikiFs.readPage("pages/projects/bravo/index.md");
92
+ const beforeStub = wikiFs.readPage("pages/projects/stub/index.md");
93
+ const report = fix.fixWiki({ dryRun: true });
94
+ assert.equal(report.dryRun, true);
95
+ assert.equal(report.changedFiles, 4);
96
+ const alpha = report.files.find((entry) => entry.path === "pages/projects/alpha/index.md");
97
+ const empty = report.files.find((entry) => entry.path === "pages/projects/empty/index.md");
98
+ const bravo = report.files.find((entry) => entry.path === "pages/projects/bravo/index.md");
99
+ const stub = report.files.find((entry) => entry.path === "pages/projects/stub/index.md");
100
+ assert.deepEqual(alpha?.changes, [
101
+ { rule: "frontmatter-backfill", details: ["title", "summary", "updated"] },
102
+ ]);
103
+ assert.deepEqual(empty?.changes, [
104
+ { rule: "frontmatter-backfill", details: ["title", "summary", "updated", "autostub"] },
105
+ ]);
106
+ assert.deepEqual(bravo?.changes, [
107
+ { rule: "tag-normalize", details: ["Run Book -> runbook"] },
108
+ ]);
109
+ assert.deepEqual(bravo?.unknownTags, ["Mystery"]);
110
+ assert.deepEqual(stub?.changes, [
111
+ { rule: "autostub-mark", details: ["autostub"] },
112
+ ]);
113
+ assert.match(report.diff, /--- a\/pages\/projects\/alpha\/index\.md/);
114
+ assert.match(report.diff, /title: Alpha Project/);
115
+ assert.match(report.diff, /summary: Alpha project launch notes for the team\./);
116
+ assert.match(report.diff, /updated: 2026-01-02/);
117
+ assert.match(report.diff, /summary: \(no summary yet\)/);
118
+ assert.match(report.diff, /tags: \[runbook, Mystery\]/);
119
+ assert.match(report.diff, /autostub: true/);
120
+ assert.equal(wikiFs.readPage("pages/projects/alpha/index.md"), beforeAlpha);
121
+ assert.equal(wikiFs.readPage("pages/projects/empty/index.md"), beforeEmpty);
122
+ assert.equal(wikiFs.readPage("pages/projects/bravo/index.md"), beforeBravo);
123
+ assert.equal(wikiFs.readPage("pages/projects/stub/index.md"), beforeStub);
124
+ });
125
+ test("fixWiki applies changes, logs each applied rule, and is idempotent on the second run", async () => {
126
+ const { fix, wikiFs } = await loadModules();
127
+ wikiFs.ensureWikiStructure();
128
+ wikiFs.writePage("pages/projects/alpha/index.md", `# Alpha Project
129
+
130
+ Alpha project launch notes for the team.
131
+
132
+ ## Details
133
+
134
+ One
135
+ Two
136
+ Three
137
+ Four
138
+ Five
139
+ Six
140
+ Seven
141
+ Eight
142
+ `);
143
+ wikiFs.writePage("pages/projects/bravo/index.md", `---
144
+ title: Bravo
145
+ summary: Bravo deployment notes
146
+ updated: 2026-05-10
147
+ tags: [Run Book]
148
+ ---
149
+
150
+ # Bravo
151
+
152
+ Deployment notes with enough body to avoid the stub marker.
153
+
154
+ ## Steps
155
+
156
+ One
157
+ Two
158
+ Three
159
+ Four
160
+ Five
161
+ Six
162
+ Seven
163
+ Eight
164
+ `);
165
+ wikiFs.writePage("pages/projects/stub/index.md", `---
166
+ title: Stub Page
167
+ summary: Tiny page
168
+ updated: 2026-05-10
169
+ ---
170
+
171
+ # Stub Page
172
+
173
+ Tiny.
174
+ `);
175
+ wikiFs.writePage("pages/_meta/taxonomy.md", `## Process
176
+ - runbook
177
+ `);
178
+ setMtime("pages/projects/alpha/index.md", "2026-01-02");
179
+ const logEntries = [];
180
+ const firstReport = fix.fixWiki({
181
+ dryRun: false,
182
+ logAction: (type, path) => logEntries.push(`${type}:${path}`),
183
+ });
184
+ assert.equal(firstReport.dryRun, false);
185
+ assert.equal(firstReport.changedFiles, 3);
186
+ assert.equal(firstReport.diff, "");
187
+ assert.match(wikiFs.readPage("pages/projects/alpha/index.md") ?? "", /title: Alpha Project/);
188
+ assert.match(wikiFs.readPage("pages/projects/alpha/index.md") ?? "", /updated: 2026-01-02/);
189
+ assert.match(wikiFs.readPage("pages/projects/bravo/index.md") ?? "", /tags: \[runbook\]/);
190
+ assert.match(wikiFs.readPage("pages/projects/stub/index.md") ?? "", /autostub: true/);
191
+ assert.deepEqual(logEntries, [
192
+ "fix-frontmatter:pages/projects/alpha/index.md",
193
+ "fix-tags:pages/projects/bravo/index.md",
194
+ "fix-autostub:pages/projects/stub/index.md",
195
+ ]);
196
+ const secondReport = fix.fixWiki({
197
+ dryRun: false,
198
+ logAction: (type, path) => logEntries.push(`${type}:${path}`),
199
+ });
200
+ assert.equal(secondReport.changedFiles, 0);
201
+ assert.equal(secondReport.diff, "");
202
+ assert.deepEqual(logEntries, [
203
+ "fix-frontmatter:pages/projects/alpha/index.md",
204
+ "fix-tags:pages/projects/bravo/index.md",
205
+ "fix-autostub:pages/projects/stub/index.md",
206
+ ]);
207
+ });
208
+ test("fixWiki respects fix toggles, path globs, autofix false, and pages/_meta skips", async () => {
209
+ const { fix, wikiFs } = await loadModules();
210
+ wikiFs.ensureWikiStructure();
211
+ wikiFs.writePage("pages/projects/match/index.md", `---
212
+ title: Match
213
+ summary: Match notes
214
+ updated: 2026-05-10
215
+ tags: [Run Book]
216
+ ---
217
+
218
+ # Match
219
+
220
+ Long enough body.
221
+
222
+ ## Details
223
+
224
+ One
225
+ Two
226
+ Three
227
+ Four
228
+ Five
229
+ Six
230
+ Seven
231
+ Eight
232
+ `);
233
+ wikiFs.writePage("pages/projects/skip/index.md", `---
234
+ title: Skip
235
+ summary: Skip notes
236
+ updated: 2026-05-10
237
+ tags: [Run Book]
238
+ autofix: false
239
+ ---
240
+
241
+ # Skip
242
+
243
+ Long enough body.
244
+
245
+ ## Details
246
+
247
+ One
248
+ Two
249
+ Three
250
+ Four
251
+ Five
252
+ Six
253
+ Seven
254
+ Eight
255
+ `);
256
+ wikiFs.writePage("pages/people/alice/index.md", `---
257
+ title: Alice
258
+ summary: Alice notes
259
+ updated: 2026-05-10
260
+ tags: [Run Book]
261
+ ---
262
+
263
+ # Alice
264
+
265
+ Long enough body.
266
+
267
+ ## Details
268
+
269
+ One
270
+ Two
271
+ Three
272
+ Four
273
+ Five
274
+ Six
275
+ Seven
276
+ Eight
277
+ `);
278
+ wikiFs.writePage("pages/_meta/manual.md", `---
279
+ title: Manual
280
+ summary: System page
281
+ updated: 2026-05-10
282
+ tags: [Run Book]
283
+ ---
284
+
285
+ # Manual
286
+ `);
287
+ wikiFs.writePage("pages/_meta/taxonomy.md", `## Process
288
+ - runbook
289
+ `);
290
+ const beforeSkip = wikiFs.readPage("pages/projects/skip/index.md");
291
+ const beforePeople = wikiFs.readPage("pages/people/alice/index.md");
292
+ const beforeMeta = wikiFs.readPage("pages/_meta/manual.md");
293
+ const report = fix.fixWiki({
294
+ dryRun: true,
295
+ fixes: ["tag-normalize"],
296
+ pathGlob: "pages/projects/**",
297
+ });
298
+ assert.equal(report.changedFiles, 1);
299
+ assert.deepEqual(report.files.map((entry) => entry.path), ["pages/projects/match/index.md"]);
300
+ assert.deepEqual(report.files[0]?.changes.map((change) => change.rule), ["tag-normalize"]);
301
+ assert.doesNotMatch(report.diff, /pages\/projects\/skip\/index\.md/);
302
+ assert.doesNotMatch(report.diff, /pages\/people\/alice\/index\.md/);
303
+ assert.doesNotMatch(report.diff, /pages\/_meta\/manual\.md/);
304
+ assert.equal(wikiFs.readPage("pages/projects/skip/index.md"), beforeSkip);
305
+ assert.equal(wikiFs.readPage("pages/people/alice/index.md"), beforePeople);
306
+ assert.equal(wikiFs.readPage("pages/_meta/manual.md"), beforeMeta);
307
+ });
308
+ test("fixWiki flags unmatched tags even when there is nothing to rewrite", async () => {
309
+ const { fix, wikiFs } = await loadModules();
310
+ wikiFs.ensureWikiStructure();
311
+ wikiFs.writePage("pages/projects/mystery/index.md", `---
312
+ title: Mystery
313
+ summary: Mystery notes
314
+ updated: 2026-05-10
315
+ tags: [Mystery]
316
+ ---
317
+
318
+ # Mystery
319
+
320
+ Long enough body.
321
+
322
+ ## Details
323
+
324
+ One
325
+ Two
326
+ Three
327
+ Four
328
+ Five
329
+ Six
330
+ Seven
331
+ Eight
332
+ `);
333
+ wikiFs.writePage("pages/_meta/taxonomy.md", `## Process
334
+ - runbook
335
+ `);
336
+ const report = fix.fixWiki({
337
+ dryRun: true,
338
+ fixes: ["tag-normalize"],
339
+ });
340
+ assert.equal(report.changedFiles, 0);
341
+ assert.equal(report.diff, "");
342
+ assert.deepEqual(report.files, [
343
+ {
344
+ path: "pages/projects/mystery/index.md",
345
+ changes: [],
346
+ unknownTags: ["Mystery"],
347
+ },
348
+ ]);
349
+ });
350
+ //# sourceMappingURL=fix.test.js.map
@@ -1,6 +1,40 @@
1
1
  const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
2
2
  const SUMMARY_MARKDOWN_RE = /(\*\*|__|[_*`~]|^\s*#+\s|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|^\s*>)/m;
3
3
  const FRONTMATTER_TEMPLATE = `---\ntitle: <title>\nsummary: <plain-text one-line summary, max 200 chars>\nupdated: YYYY-MM-DD\ntags: []\nrelated: []\n---`;
4
+ const PROJECT_RULES_HARD_DEFAULTS = {
5
+ auto_pr: true,
6
+ require_worktree: false,
7
+ pr_draft_default: false,
8
+ default_branch: "main",
9
+ commit_co_author: "Copilot <223556219+Copilot@users.noreply.github.com>",
10
+ test_command: "",
11
+ build_command: "",
12
+ lint_command: "",
13
+ require_clean_worktree: false,
14
+ };
15
+ const KNOWN_WIKI_FRONTMATTER_FIELDS = new Set([
16
+ "title",
17
+ "summary",
18
+ "updated",
19
+ "tags",
20
+ "autostub",
21
+ "confidence",
22
+ "contested",
23
+ "contradictions",
24
+ "related",
25
+ ]);
26
+ const PROJECT_RULE_HARD_FIELDS = [
27
+ "auto_pr",
28
+ "require_worktree",
29
+ "pr_draft_default",
30
+ "default_branch",
31
+ "commit_co_author",
32
+ "test_command",
33
+ "build_command",
34
+ "lint_command",
35
+ "require_clean_worktree",
36
+ ];
37
+ const KNOWN_PROJECT_RULE_FIELDS = new Set(PROJECT_RULE_HARD_FIELDS);
4
38
  export function parseWikiFrontmatter(content) {
5
39
  const match = content.match(FRONTMATTER_RE);
6
40
  if (!match) {
@@ -41,6 +75,15 @@ export function parseWikiFrontmatter(content) {
41
75
  else
42
76
  parsed.metadata[key] = value;
43
77
  break;
78
+ case "auto_pr":
79
+ case "require_worktree":
80
+ case "pr_draft_default":
81
+ case "require_clean_worktree":
82
+ if (typeof value === "boolean")
83
+ parsed[key] = value;
84
+ else
85
+ parsed.metadata[key] = value;
86
+ break;
44
87
  case "confidence":
45
88
  if (value === "high" || value === "medium" || value === "low") {
46
89
  parsed.confidence = value;
@@ -49,6 +92,16 @@ export function parseWikiFrontmatter(content) {
49
92
  parsed.metadata[key] = value;
50
93
  }
51
94
  break;
95
+ case "default_branch":
96
+ case "commit_co_author":
97
+ case "test_command":
98
+ case "build_command":
99
+ case "lint_command":
100
+ if (typeof value === "string")
101
+ parsed[key] = value;
102
+ else
103
+ parsed.metadata[key] = value;
104
+ break;
52
105
  default:
53
106
  parsed.metadata[key] = value;
54
107
  break;
@@ -59,6 +112,17 @@ export function parseWikiFrontmatter(content) {
59
112
  body: content.slice(match[0].length),
60
113
  };
61
114
  }
115
+ export function parseProjectRulesFrontmatter(content) {
116
+ const { parsed, body } = parseWikiFrontmatter(content);
117
+ return {
118
+ parsed: {
119
+ ...parsed,
120
+ hardRules: materializeProjectRulesHardFields(parsed),
121
+ },
122
+ body,
123
+ warnings: collectUnknownProjectRuleWarnings(parsed.metadata),
124
+ };
125
+ }
62
126
  export function hasWikiFrontmatter(content) {
63
127
  return FRONTMATTER_RE.test(content);
64
128
  }
@@ -125,6 +189,25 @@ export function validateWikiFrontmatter(content, options = {}) {
125
189
  errors,
126
190
  };
127
191
  }
192
+ export function validateProjectRulesFrontmatter(content, options = {}) {
193
+ const base = validateWikiFrontmatter(content, options);
194
+ const { parsed, warnings } = parseProjectRulesFrontmatter(content);
195
+ const errors = [...base.errors];
196
+ for (const field of PROJECT_RULE_HARD_FIELDS) {
197
+ if (field in parsed.metadata) {
198
+ errors.push({
199
+ rule: "invalid-field-type",
200
+ field,
201
+ message: formatFrontmatterMessage(`invalid '${field}' type`),
202
+ });
203
+ }
204
+ }
205
+ return {
206
+ valid: errors.length === 0,
207
+ errors,
208
+ warnings,
209
+ };
210
+ }
128
211
  function formatFrontmatterMessage(reason) {
129
212
  return `Wiki page frontmatter violates the required shape: ${reason}. Use:\n${FRONTMATTER_TEMPLATE}`;
130
213
  }
@@ -145,4 +228,26 @@ function parseValue(rawValue) {
145
228
  function stripQuotes(value) {
146
229
  return value.replace(/^['"]|['"]$/g, "");
147
230
  }
231
+ function materializeProjectRulesHardFields(parsed) {
232
+ return {
233
+ auto_pr: parsed.auto_pr ?? PROJECT_RULES_HARD_DEFAULTS.auto_pr,
234
+ require_worktree: parsed.require_worktree ?? PROJECT_RULES_HARD_DEFAULTS.require_worktree,
235
+ pr_draft_default: parsed.pr_draft_default ?? PROJECT_RULES_HARD_DEFAULTS.pr_draft_default,
236
+ default_branch: parsed.default_branch ?? PROJECT_RULES_HARD_DEFAULTS.default_branch,
237
+ commit_co_author: parsed.commit_co_author ?? PROJECT_RULES_HARD_DEFAULTS.commit_co_author,
238
+ test_command: parsed.test_command ?? PROJECT_RULES_HARD_DEFAULTS.test_command,
239
+ build_command: parsed.build_command ?? PROJECT_RULES_HARD_DEFAULTS.build_command,
240
+ lint_command: parsed.lint_command ?? PROJECT_RULES_HARD_DEFAULTS.lint_command,
241
+ require_clean_worktree: parsed.require_clean_worktree ?? PROJECT_RULES_HARD_DEFAULTS.require_clean_worktree,
242
+ };
243
+ }
244
+ function collectUnknownProjectRuleWarnings(metadata) {
245
+ return Object.keys(metadata)
246
+ .filter((field) => !KNOWN_WIKI_FRONTMATTER_FIELDS.has(field) && !KNOWN_PROJECT_RULE_FIELDS.has(field))
247
+ .map((field) => ({
248
+ rule: "unknown-project-rule-key",
249
+ field,
250
+ message: `Project rules frontmatter includes unknown key '${field}'.`,
251
+ }));
252
+ }
148
253
  //# sourceMappingURL=frontmatter.js.map
@@ -106,4 +106,124 @@ tags: [engineering, made-up-tag]
106
106
  assert.deepEqual(result.errors.map((error) => error.rule), ["unknown-tag"]);
107
107
  assert.match(result.errors[0]?.message ?? "", /Add it to `pages\/_meta\/taxonomy\.md` first\./);
108
108
  });
109
+ test("parseProjectRulesFrontmatter parses typed hard-rule fields and flags unknown keys", async () => {
110
+ const { parseProjectRulesFrontmatter } = await loadFrontmatterModule();
111
+ const result = parseProjectRulesFrontmatter(`---
112
+ title: Project rules for chapterhouse
113
+ summary: Project-specific operating rules for Chapterhouse itself.
114
+ updated: 2026-05-12
115
+ tags: [engineering, workflow]
116
+ related: []
117
+ auto_pr: false
118
+ require_worktree: true
119
+ pr_draft_default: true
120
+ default_branch: trunk
121
+ commit_co_author: Jane Doe <jane@example.com>
122
+ test_command: npm test
123
+ build_command: npm run build
124
+ lint_command: npm run lint
125
+ require_clean_worktree: true
126
+ custom_rule: preserve-me
127
+ ---
128
+
129
+ ## Soft Rules
130
+ `);
131
+ assert.deepEqual(result, {
132
+ parsed: {
133
+ title: "Project rules for chapterhouse",
134
+ summary: "Project-specific operating rules for Chapterhouse itself.",
135
+ updated: "2026-05-12",
136
+ tags: ["engineering", "workflow"],
137
+ related: [],
138
+ auto_pr: false,
139
+ require_worktree: true,
140
+ pr_draft_default: true,
141
+ default_branch: "trunk",
142
+ commit_co_author: "Jane Doe <jane@example.com>",
143
+ test_command: "npm test",
144
+ build_command: "npm run build",
145
+ lint_command: "npm run lint",
146
+ require_clean_worktree: true,
147
+ metadata: {
148
+ custom_rule: "preserve-me",
149
+ },
150
+ hardRules: {
151
+ auto_pr: false,
152
+ require_worktree: true,
153
+ pr_draft_default: true,
154
+ default_branch: "trunk",
155
+ commit_co_author: "Jane Doe <jane@example.com>",
156
+ test_command: "npm test",
157
+ build_command: "npm run build",
158
+ lint_command: "npm run lint",
159
+ require_clean_worktree: true,
160
+ },
161
+ },
162
+ body: "## Soft Rules\n",
163
+ warnings: [
164
+ {
165
+ rule: "unknown-project-rule-key",
166
+ field: "custom_rule",
167
+ message: "Project rules frontmatter includes unknown key 'custom_rule'.",
168
+ },
169
+ ],
170
+ });
171
+ });
172
+ test("parseProjectRulesFrontmatter materializes defaults for omitted hard-rule fields", async () => {
173
+ const { parseProjectRulesFrontmatter } = await loadFrontmatterModule();
174
+ const result = parseProjectRulesFrontmatter(`---
175
+ title: Project rules for chapterhouse
176
+ summary: Project-specific operating rules for Chapterhouse itself.
177
+ ---
178
+
179
+ ## Soft Rules
180
+ `);
181
+ assert.deepEqual(result.parsed.hardRules, {
182
+ auto_pr: true,
183
+ require_worktree: false,
184
+ pr_draft_default: false,
185
+ default_branch: "main",
186
+ commit_co_author: "Copilot <223556219+Copilot@users.noreply.github.com>",
187
+ test_command: "",
188
+ build_command: "",
189
+ lint_command: "",
190
+ require_clean_worktree: false,
191
+ });
192
+ assert.deepEqual(result.warnings, []);
193
+ });
194
+ test("validateProjectRulesFrontmatter rejects invalid hard-rule field types and warns on unknown keys", async () => {
195
+ const { validateProjectRulesFrontmatter } = await loadFrontmatterModule();
196
+ const result = validateProjectRulesFrontmatter(`---
197
+ title: Project rules for chapterhouse
198
+ summary: Project-specific operating rules for Chapterhouse itself.
199
+ auto_pr: nope
200
+ default_branch: [main]
201
+ test_command: [npm test]
202
+ require_clean_worktree: "yes"
203
+ custom_rule: preserve-me
204
+ ---
205
+
206
+ ## Soft Rules
207
+ `);
208
+ assert.equal(result.valid, false);
209
+ assert.deepEqual(result.errors.map((error) => error.rule), [
210
+ "invalid-field-type",
211
+ "invalid-field-type",
212
+ "invalid-field-type",
213
+ "invalid-field-type",
214
+ ]);
215
+ assert.deepEqual(result.errors.map((error) => error.field), [
216
+ "auto_pr",
217
+ "default_branch",
218
+ "test_command",
219
+ "require_clean_worktree",
220
+ ]);
221
+ assert.deepEqual(result.warnings, [
222
+ {
223
+ rule: "unknown-project-rule-key",
224
+ field: "custom_rule",
225
+ message: "Project rules frontmatter includes unknown key 'custom_rule'.",
226
+ },
227
+ ]);
228
+ });
109
229
  //# sourceMappingURL=frontmatter.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
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"