chapterhouse 0.7.0 → 0.8.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.
- package/agents/korg.agent.md +65 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +238 -2
- package/dist/api/server.test.js +199 -0
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agents.js +3 -4
- package/dist/copilot/agents.test.js +12 -1
- package/dist/copilot/orchestrator.js +12 -1
- package/dist/copilot/orchestrator.test.js +3 -7
- package/dist/copilot/system-message.js +11 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +184 -376
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +53 -59
- package/dist/daemon.js +9 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +100 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/store/db.js +119 -4
- package/dist/store/db.test.js +19 -1
- package/dist/test/setup-env.js +1 -0
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +140 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/index-manager.js +246 -330
- package/dist/wiki/index-manager.test.js +138 -145
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +1 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/{index-DuKYxMIR.css → index-5kz9aRU9.css} +1 -1
- package/web/dist/assets/{index-DytB69KC.js → index-BbX9RKf3.js} +91 -89
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-DytB69KC.js.map +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } 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-consolidation-${process.pid}`);
|
|
7
|
+
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
8
|
+
function resetSandbox() {
|
|
9
|
+
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
10
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
11
|
+
mkdirSync(chapterhouseHome, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
async function loadModules() {
|
|
14
|
+
const nonce = `${Date.now()}-${Math.random()}`;
|
|
15
|
+
const dbModule = await import(new URL(`../store/db.js?c=${nonce}`, import.meta.url).href);
|
|
16
|
+
const wikiFs = await import(new URL(`./fs.js?c=${nonce}`, import.meta.url).href);
|
|
17
|
+
const indexManager = await import(new URL(`./index-manager.js?c=${nonce}`, import.meta.url).href);
|
|
18
|
+
const consolidation = await import(new URL(`./consolidation.js?c=${nonce}`, import.meta.url).href);
|
|
19
|
+
const memory = await import(new URL(`../memory/index.js?c=${nonce}`, import.meta.url).href);
|
|
20
|
+
return { dbModule, wikiFs, indexManager, consolidation, memory };
|
|
21
|
+
}
|
|
22
|
+
function makePage(title, summary, updated, summaryBody, timelineBlocks, pinned = false) {
|
|
23
|
+
return `---\ntitle: ${title}\nsummary: ${summary}\nupdated: ${updated}\ntags: []\npinned: ${pinned ? "true" : "false"}\n---\n\n# ${title}\n\n## Summary\n\n${summaryBody}\n\n## Timeline\n\n${timelineBlocks.trim()}\n`;
|
|
24
|
+
}
|
|
25
|
+
test.beforeEach(async () => {
|
|
26
|
+
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
27
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
28
|
+
dbModule.closeDb();
|
|
29
|
+
resetSandbox();
|
|
30
|
+
});
|
|
31
|
+
test.after(async () => {
|
|
32
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
33
|
+
dbModule.closeDb();
|
|
34
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
test("runConsolidation rewrites stale compiled truth and skips pinned pages", async () => {
|
|
37
|
+
const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
|
|
38
|
+
const db = dbModule.getDb();
|
|
39
|
+
wikiFs.ensureWikiStructure();
|
|
40
|
+
wikiFs.writePage("pages/topics/rust/index.md", makePage("Rust", "Systems language", "2026-05-01", "Old summary.", `### 2026-05-02T10:00:00.000Z\n\nStabilized async patterns.\n\n### 2026-05-04T10:00:00.000Z\n\nAdopted by backend team.`));
|
|
41
|
+
wikiFs.writePage("pages/topics/pinned/index.md", makePage("Pinned", "Pinned page", "2026-05-01", "Keep this summary.", `### 2026-05-10T10:00:00.000Z\n\nNew fact that should not trigger rewrite.`, true));
|
|
42
|
+
indexManager.rebuildWikiIndex();
|
|
43
|
+
const result = await consolidation.runConsolidationWithDeps(db, {
|
|
44
|
+
now: () => new Date("2026-05-14T22:30:03.086Z"),
|
|
45
|
+
synthesizeTruth: async ({ pagePath, pendingEntries }) => `Synthesized ${pagePath} from ${pendingEntries.length} entries.`,
|
|
46
|
+
commitWikiChanges: async () => false,
|
|
47
|
+
});
|
|
48
|
+
const rewritten = wikiFs.readPage("pages/topics/rust/index.md") ?? "";
|
|
49
|
+
const pinned = wikiFs.readPage("pages/topics/pinned/index.md") ?? "";
|
|
50
|
+
const row = db.prepare(`SELECT compiled_truth_hash FROM wiki_pages WHERE path = ?`).get("pages/topics/rust/index.md");
|
|
51
|
+
assert.match(rewritten, /Synthesized pages\/topics\/rust\/index.md from 2 entries\./);
|
|
52
|
+
assert.match(rewritten, /last_updated:/);
|
|
53
|
+
assert.ok(row.compiled_truth_hash, "compiled truth hash should be stored");
|
|
54
|
+
assert.equal(result.truthRewrites, 1);
|
|
55
|
+
assert.equal(result.llmCallsUsed, 1);
|
|
56
|
+
assert.match(pinned, /Keep this summary\./);
|
|
57
|
+
assert.doesNotMatch(pinned, /Synthesized/);
|
|
58
|
+
});
|
|
59
|
+
test("runConsolidation merges fragment pages into a canonical page and repoints links", async () => {
|
|
60
|
+
const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
|
|
61
|
+
const db = dbModule.getDb();
|
|
62
|
+
wikiFs.ensureWikiStructure();
|
|
63
|
+
wikiFs.writePage("pages/topics/rust/index.md", makePage("Rust", "Main Rust page", "2026-05-10", "Canonical summary.", `### 2026-05-10T10:00:00.000Z\n\nCanonical fact.`));
|
|
64
|
+
wikiFs.writePage("pages/topics/ruts/index.md", makePage("Ruts", "Fragment Rust page", "2026-05-11", "Fragment summary.", `### 2026-05-11T10:00:00.000Z\n\nFragment fact.`));
|
|
65
|
+
wikiFs.writePage("pages/topics/async/index.md", makePage("Async", "Async topic", "2026-05-10", "Async summary with [[Ruts]].", `### 2026-05-10T08:00:00.000Z\n\nReferences [[Ruts]].`));
|
|
66
|
+
indexManager.rebuildWikiIndex();
|
|
67
|
+
const result = await consolidation.runConsolidationWithDeps(db, {
|
|
68
|
+
now: () => new Date("2026-05-14T22:30:03.086Z"),
|
|
69
|
+
synthesizeTruth: async () => "unused",
|
|
70
|
+
commitWikiChanges: async () => false,
|
|
71
|
+
});
|
|
72
|
+
const canonical = wikiFs.readPage("pages/topics/rust/index.md") ?? "";
|
|
73
|
+
const fragmentExists = wikiFs.pageExists("pages/topics/ruts/index.md");
|
|
74
|
+
const repointed = db.prepare(`SELECT COUNT(*) as c FROM wiki_links WHERE to_page = ?`).get("pages/topics/rust/index.md");
|
|
75
|
+
const fragmentRow = db.prepare(`SELECT path FROM wiki_pages WHERE path = ?`).get("pages/topics/ruts/index.md");
|
|
76
|
+
assert.equal(result.fragmentsMerged, 1);
|
|
77
|
+
assert.match(canonical, /Canonical fact\./);
|
|
78
|
+
assert.match(canonical, /Fragment fact\./);
|
|
79
|
+
assert.equal(fragmentExists, false);
|
|
80
|
+
assert.equal(fragmentRow, undefined);
|
|
81
|
+
assert.ok(repointed.c >= 1, "links should be repointed to the canonical page");
|
|
82
|
+
});
|
|
83
|
+
test("runConsolidation removes orphaned wiki_links rows", async () => {
|
|
84
|
+
const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
|
|
85
|
+
const db = dbModule.getDb();
|
|
86
|
+
wikiFs.ensureWikiStructure();
|
|
87
|
+
wikiFs.writePage("pages/topics/alpha/index.md", makePage("Alpha", "Alpha page", "2026-05-10", "Alpha summary.", `### 2026-05-10T10:00:00.000Z\n\nAlpha fact.`));
|
|
88
|
+
indexManager.rebuildWikiIndex();
|
|
89
|
+
db.prepare(`INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at) VALUES (?, ?, ?, ?)`)
|
|
90
|
+
.run("pages/topics/alpha/index.md", "pages/topics/missing/index.md", "references", "2026-05-12T00:00:00.000Z");
|
|
91
|
+
const result = await consolidation.runConsolidationWithDeps(db, {
|
|
92
|
+
now: () => new Date("2026-05-14T22:30:03.086Z"),
|
|
93
|
+
synthesizeTruth: async () => "unused",
|
|
94
|
+
commitWikiChanges: async () => false,
|
|
95
|
+
});
|
|
96
|
+
const orphanCount = db.prepare(`SELECT COUNT(*) as c FROM wiki_links WHERE to_page = ?`).get("pages/topics/missing/index.md");
|
|
97
|
+
assert.equal(result.linksRepaired, 1);
|
|
98
|
+
assert.equal(orphanCount.c, 0);
|
|
99
|
+
});
|
|
100
|
+
test("runConsolidation creates an action item for research sessions inactive for 7+ days", async () => {
|
|
101
|
+
const { dbModule, wikiFs, consolidation, memory } = await loadModules();
|
|
102
|
+
const db = dbModule.getDb();
|
|
103
|
+
wikiFs.ensureWikiStructure();
|
|
104
|
+
const globalScope = memory.getScope("global");
|
|
105
|
+
assert.ok(globalScope);
|
|
106
|
+
db.prepare(`
|
|
107
|
+
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
108
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
109
|
+
`).run("source-1", "url", "https://example.test/1", "Research note", "2026-05-01T10:00:00.000Z", "sources/source-1.md", "content", "[]", "active", "session-123", "Compiler research");
|
|
110
|
+
const result = await consolidation.runConsolidationWithDeps(db, {
|
|
111
|
+
now: () => new Date("2026-05-14T22:30:03.086Z"),
|
|
112
|
+
synthesizeTruth: async () => "unused",
|
|
113
|
+
commitWikiChanges: async () => false,
|
|
114
|
+
});
|
|
115
|
+
const actionItems = memory.listActionItems({ scope_id: globalScope.id, includeArchived: true });
|
|
116
|
+
assert.equal(result.staleSessionsNotified, 1);
|
|
117
|
+
assert.equal(actionItems.some((item) => item.title.includes("Research session 'Compiler research' has been inactive for 7+ days")), true);
|
|
118
|
+
});
|
|
119
|
+
test("runConsolidation caps truth rewrites before exceeding the LLM budget", async () => {
|
|
120
|
+
const { dbModule, wikiFs, indexManager, consolidation } = await loadModules();
|
|
121
|
+
const db = dbModule.getDb();
|
|
122
|
+
wikiFs.ensureWikiStructure();
|
|
123
|
+
for (let i = 0; i < 25; i++) {
|
|
124
|
+
wikiFs.writePage(`pages/topics/topic-${i}/index.md`, makePage(`Topic ${i}`, `Topic ${i}`, "2026-05-01", `Old topic ${i} summary.`, `### 2026-05-13T10:00:00.000Z\n\nFresh fact ${i}.`));
|
|
125
|
+
}
|
|
126
|
+
indexManager.rebuildWikiIndex();
|
|
127
|
+
let llmCalls = 0;
|
|
128
|
+
const result = await consolidation.runConsolidationWithDeps(db, {
|
|
129
|
+
now: () => new Date("2026-05-14T22:30:03.086Z"),
|
|
130
|
+
synthesizeTruth: async () => {
|
|
131
|
+
llmCalls += 1;
|
|
132
|
+
return `Synthesized call ${llmCalls}`;
|
|
133
|
+
},
|
|
134
|
+
commitWikiChanges: async () => false,
|
|
135
|
+
});
|
|
136
|
+
assert.equal(result.truthRewrites, 18);
|
|
137
|
+
assert.equal(result.llmCallsUsed, 18);
|
|
138
|
+
assert.equal(llmCalls, 18);
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=consolidation.test.js.map
|
package/dist/wiki/frontmatter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeWikiPath } from "./path-utils.js";
|
|
1
2
|
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
2
3
|
const SUMMARY_MARKDOWN_RE = /(\*\*|__|[_*`~]|^\s*#+\s|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|^\s*>)/m;
|
|
3
4
|
const FRONTMATTER_TEMPLATE = `---\ntitle: <title>\nsummary: <plain-text one-line summary, max 200 chars>\nupdated: YYYY-MM-DD\ntags: []\nrelated: []\n---`;
|
|
@@ -208,6 +209,53 @@ export function validateProjectRulesFrontmatter(content, options = {}) {
|
|
|
208
209
|
warnings,
|
|
209
210
|
};
|
|
210
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Validate frontmatter and backfill required fields that are missing.
|
|
214
|
+
* Required fields: title, entity_type (null default), last_updated, version.
|
|
215
|
+
* Returns updated content and whether any changes were made.
|
|
216
|
+
*/
|
|
217
|
+
export function validateAndBackfillFrontmatter(path, content) {
|
|
218
|
+
const { parsed: fm } = parseWikiFrontmatter(content);
|
|
219
|
+
// Also handle missing-frontmatter case: inject a minimal block
|
|
220
|
+
if (!hasWikiFrontmatter(content)) {
|
|
221
|
+
const title = deriveTitleFromPath(path);
|
|
222
|
+
const now = new Date().toISOString();
|
|
223
|
+
const injected = `---\ntitle: ${title}\nlast_updated: ${now}\nversion: 1\n---\n\n${content}`;
|
|
224
|
+
return { content: injected, changed: true };
|
|
225
|
+
}
|
|
226
|
+
let changed = false;
|
|
227
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
228
|
+
if (!fmMatch)
|
|
229
|
+
return { content, changed: false };
|
|
230
|
+
const body = content.slice(fmMatch[0].length);
|
|
231
|
+
const fmLines = fmMatch[1].split("\n");
|
|
232
|
+
const fmKeys = new Set(fmLines.map((l) => l.split(":")[0].trim()).filter(Boolean));
|
|
233
|
+
if (!fmKeys.has("title") || !fm.title?.trim()) {
|
|
234
|
+
fmLines.push(`title: ${deriveTitleFromPath(path)}`);
|
|
235
|
+
changed = true;
|
|
236
|
+
}
|
|
237
|
+
if (!fmKeys.has("last_updated")) {
|
|
238
|
+
fmLines.push(`last_updated: ${new Date().toISOString()}`);
|
|
239
|
+
changed = true;
|
|
240
|
+
}
|
|
241
|
+
if (!fmKeys.has("version")) {
|
|
242
|
+
fmLines.push("version: 1");
|
|
243
|
+
changed = true;
|
|
244
|
+
}
|
|
245
|
+
// entity_type: only add if not already present (null default is fine — don't inject "null" string)
|
|
246
|
+
if (!changed)
|
|
247
|
+
return { content, changed: false };
|
|
248
|
+
const newContent = `---\n${fmLines.join("\n")}\n---\n${body}`;
|
|
249
|
+
return { content: newContent, changed: true };
|
|
250
|
+
}
|
|
251
|
+
function deriveTitleFromPath(path) {
|
|
252
|
+
const normalizedPath = normalizeWikiPath(path);
|
|
253
|
+
const segs = normalizedPath.split("/").filter(Boolean);
|
|
254
|
+
const file = segs[segs.length - 1] || normalizedPath;
|
|
255
|
+
const base = file.replace(/\.md$/, "");
|
|
256
|
+
const titleBase = base === "index" && segs.length >= 2 ? segs[segs.length - 2] : base;
|
|
257
|
+
return titleBase.split(/[-_]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
258
|
+
}
|
|
211
259
|
function formatFrontmatterMessage(reason) {
|
|
212
260
|
return `Wiki page frontmatter violates the required shape: ${reason}. Use:\n${FRONTMATTER_TEMPLATE}`;
|
|
213
261
|
}
|
|
@@ -226,4 +226,46 @@ custom_rule: preserve-me
|
|
|
226
226
|
},
|
|
227
227
|
]);
|
|
228
228
|
});
|
|
229
|
+
test("validateAndBackfillFrontmatter backfills missing required fields", async () => {
|
|
230
|
+
const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
|
|
231
|
+
const result = validateAndBackfillFrontmatter("pages/topics/rust/index.md", "---\ntitle: Rust\n---\n\n# Rust\n\nContent.\n");
|
|
232
|
+
assert.equal(result.changed, true);
|
|
233
|
+
assert.match(result.content, /last_updated:/);
|
|
234
|
+
assert.match(result.content, /version:/);
|
|
235
|
+
});
|
|
236
|
+
test("validateAndBackfillFrontmatter passes valid frontmatter unchanged", async () => {
|
|
237
|
+
const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
|
|
238
|
+
const content = "---\ntitle: Rust\nlast_updated: 2026-05-14T00:00:00.000Z\nversion: 1\n---\n\n# Rust\n";
|
|
239
|
+
const result = validateAndBackfillFrontmatter("pages/topics/rust/index.md", content);
|
|
240
|
+
assert.equal(result.changed, false);
|
|
241
|
+
assert.equal(result.content, content);
|
|
242
|
+
});
|
|
243
|
+
test("validateAndBackfillFrontmatter backfills title from filename when missing", async () => {
|
|
244
|
+
const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
|
|
245
|
+
const result = validateAndBackfillFrontmatter("pages/topics/rust-async/index.md", "---\nlast_updated: 2026-05-14T00:00:00.000Z\nversion: 1\n---\n\n# Content\n");
|
|
246
|
+
assert.equal(result.changed, true);
|
|
247
|
+
assert.match(result.content, /title: Rust Async/);
|
|
248
|
+
});
|
|
249
|
+
test("validateAndBackfillFrontmatter injects frontmatter block when completely missing", async () => {
|
|
250
|
+
const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
|
|
251
|
+
const result = validateAndBackfillFrontmatter("pages/topics/new-page/index.md", "# New Page\n\nContent here.\n");
|
|
252
|
+
assert.equal(result.changed, true);
|
|
253
|
+
assert.match(result.content, /^---\n/);
|
|
254
|
+
assert.match(result.content, /title: /);
|
|
255
|
+
assert.match(result.content, /last_updated: /);
|
|
256
|
+
assert.match(result.content, /version: 1/);
|
|
257
|
+
// ISO timestamp
|
|
258
|
+
const match = result.content.match(/last_updated: (.+)/);
|
|
259
|
+
assert.ok(match, "Should have last_updated");
|
|
260
|
+
assert.doesNotThrow(() => new Date(match[1].trim()).toISOString(), "last_updated should be valid ISO timestamp");
|
|
261
|
+
});
|
|
262
|
+
test("validateAndBackfillFrontmatter backfilled last_updated is valid ISO timestamp", async () => {
|
|
263
|
+
const { validateAndBackfillFrontmatter } = await loadFrontmatterModule();
|
|
264
|
+
const result = validateAndBackfillFrontmatter("pages/people/ada/index.md", "---\ntitle: Ada\n---\n\n# Ada\n");
|
|
265
|
+
assert.equal(result.changed, true);
|
|
266
|
+
const match = result.content.match(/last_updated: (.+)/);
|
|
267
|
+
assert.ok(match, "Should have last_updated after backfill");
|
|
268
|
+
const ts = match[1].trim();
|
|
269
|
+
assert.doesNotThrow(() => new Date(ts).toISOString(), `${ts} should be valid ISO timestamp`);
|
|
270
|
+
});
|
|
229
271
|
//# sourceMappingURL=frontmatter.test.js.map
|