ultimate-brain 0.4.1 → 0.5.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/skills/brain-apply/SKILL.md +35 -22
- package/.agents/skills/brain-catch-up/SKILL.md +28 -20
- package/.agents/skills/brain-howto/SKILL.md +1 -1
- package/.agents/skills/brain-howto/references/guide.md +67 -44
- package/.agents/skills/brain-ingest/SKILL.md +10 -6
- package/.agents/skills/brain-ingest/references/cli-inputs.md +19 -3
- package/.agents/skills/brain-ingest/references/extraction.md +23 -1
- package/.agents/skills/brain-ingest/references/notion.md +3 -3
- package/.agents/skills/brain-ingest/references/sources.md +25 -5
- package/.agents/skills/brain-notion-change/SKILL.md +2 -0
- package/.agents/skills/brain-query/SKILL.md +17 -7
- package/.agents/skills/brain-sync/SKILL.md +13 -8
- package/.agents/skills/brain-sync/references/formats.md +1 -1
- package/dist/cli.js +3 -3
- package/dist/commands/report.js +81 -4
- package/dist/commands/source.js +34 -0
- package/dist/commands/status.js +2 -2
- package/dist/lib/apply.js +237 -33
- package/dist/lib/diff.js +31 -0
- package/dist/lib/ids.js +2 -0
- package/dist/lib/index-builder.js +6 -1
- package/dist/lib/lint.js +39 -8
- package/dist/lib/note-new.js +21 -2
- package/dist/lib/paths.js +1 -0
- package/dist/lib/proposals.js +72 -18
- package/dist/lib/query.js +1 -1
- package/dist/lib/schemas.js +1 -1
- package/dist/lib/sync.js +101 -50
- package/package.json +1 -1
- package/templates/brain/brain.json +2 -1
- package/templates/brain/knowledge/references/.gitkeep +0 -0
package/dist/lib/note-new.js
CHANGED
|
@@ -7,7 +7,7 @@ import { rangesProblem, splitParts } from "./parts.js";
|
|
|
7
7
|
import { newNotePath } from "./paths.js";
|
|
8
8
|
import { formatDate, formatTimestamp } from "./time.js";
|
|
9
9
|
const ITEM_KEYS = ["type", "id", "title", "file_date", "frontmatter", "body"];
|
|
10
|
-
const KNOWLEDGE_TYPES = new Set(["area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "memory"]);
|
|
10
|
+
const KNOWLEDGE_TYPES = new Set(["area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "reference", "memory"]);
|
|
11
11
|
const NEW_NOTE_TYPES = new Set([...KNOWLEDGE_TYPES, "source"]);
|
|
12
12
|
const FORBIDDEN_KEYS = ["id", "type", "title", "created", "updated", "lifecycle"];
|
|
13
13
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -100,6 +100,24 @@ export function stageNotes(workspace, items, pointerOf, options) {
|
|
|
100
100
|
if (problem !== undefined)
|
|
101
101
|
parents.push({ pointer: `${pointerOf(index)}/frontmatter/attached_to`, message: problem });
|
|
102
102
|
});
|
|
103
|
+
// replaces names a source, existing or earlier in the batch, that no other source replaces (spec/cli.md §5.5, 0045).
|
|
104
|
+
const replaced = new Map();
|
|
105
|
+
for (const note of workspace.notes)
|
|
106
|
+
if (note.type === "source" && typeof note.attributes.replaces === "string")
|
|
107
|
+
replaced.set(note.attributes.replaces, note.id);
|
|
108
|
+
items.forEach((item, index) => {
|
|
109
|
+
const earlier = item.type === "source" ? item.frontmatter.replaces : undefined;
|
|
110
|
+
if (earlier === undefined)
|
|
111
|
+
return;
|
|
112
|
+
const type = workspace.get(String(earlier))?.type ?? items.slice(0, index).find((candidate) => candidate.id === earlier)?.type;
|
|
113
|
+
const problem = type === undefined ? `${String(earlier)} is not an existing source or one earlier in the batch`
|
|
114
|
+
: type !== "source" ? `${String(earlier)} is a ${type}, not a source`
|
|
115
|
+
: replaced.has(String(earlier)) ? `${String(earlier)} is already replaced by ${replaced.get(String(earlier))}`
|
|
116
|
+
: undefined;
|
|
117
|
+
if (problem !== undefined)
|
|
118
|
+
parents.push({ pointer: `${pointerOf(index)}/frontmatter/replaces`, message: problem });
|
|
119
|
+
replaced.set(String(earlier), item.id ?? `/notes/${index}`);
|
|
120
|
+
});
|
|
103
121
|
// A repo file names a linked repo of brain.json (0042).
|
|
104
122
|
const repoNames = new Set(Array.isArray(workspace.config.repos) ? workspace.config.repos.filter(isRecord).map((repo) => repo.name) : []);
|
|
105
123
|
items.forEach((item, index) => {
|
|
@@ -120,7 +138,8 @@ export function stageNotes(workspace, items, pointerOf, options) {
|
|
|
120
138
|
if (item.type === "source") {
|
|
121
139
|
const hash = `sha256:${createHash("sha256").update(body, "utf8").digest("hex")}`;
|
|
122
140
|
const sourceKey = attributes.source_key;
|
|
123
|
-
|
|
141
|
+
// A new version (replaces) is compared by its text only, so a corrected input can be read again (0045).
|
|
142
|
+
const duplicateByKey = attributes.replaces === undefined && typeof sourceKey === "string" && (sourceKey.startsWith("file:") || attributes.kind === "capture");
|
|
124
143
|
const duplicate = workspace.notes.find((note) => note.type === "source" && (note.attributes.content_hash === hash || (duplicateByKey && note.attributes.source_key === sourceKey)));
|
|
125
144
|
if (duplicate !== undefined) {
|
|
126
145
|
result.skipped.push({ input_index: index, reason: "DUPLICATE_SOURCE", existing: duplicate.id });
|
package/dist/lib/paths.js
CHANGED
|
@@ -49,6 +49,7 @@ function layout(type, slug, date, sourceKey) {
|
|
|
49
49
|
case "org": return file(`knowledge/orgs/${slug}.md`);
|
|
50
50
|
case "question": return file(`knowledge/questions/${slug}.md`);
|
|
51
51
|
case "concept": return file(`knowledge/concepts/${slug}.md`);
|
|
52
|
+
case "reference": return file(`knowledge/references/${slug}.md`);
|
|
52
53
|
case "meeting": return file(`knowledge/meetings/${date}-${slug}.md`);
|
|
53
54
|
case "decision": return file(`knowledge/decisions/${date}-${slug}.md`);
|
|
54
55
|
case "source": return file(`sources/${sourceFolder(sourceKey)}/${date}-${slug}.md`);
|
package/dist/lib/proposals.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { BrainError } from "./errors.js";
|
|
3
4
|
import { serializeNewFile } from "./frontmatter.js";
|
|
@@ -8,13 +9,15 @@ import { compareCodePoints } from "./notes.js";
|
|
|
8
9
|
import { newNotePath } from "./paths.js";
|
|
9
10
|
import { loadSchemas } from "./schemas.js";
|
|
10
11
|
import { formatDate } from "./time.js";
|
|
11
|
-
export const PROPOSAL_KINDS = ["update_fact", "add_to_note", "add_link", "supersede", "contradiction", "merge", "new_item", "review_notes", "answer"];
|
|
12
|
+
export const PROPOSAL_KINDS = ["update_fact", "add_to_note", "update_body", "add_link", "supersede", "contradiction", "merge", "new_item", "review_notes", "answer"];
|
|
12
13
|
export const PROPOSAL_STATUSES = ["pending", "approved", "rejected", "edited", "applied", "closed"];
|
|
13
14
|
/** Input keys of `proposal new`: the schema §11 fields the CLI does not set (spec/cli.md §5.6). */
|
|
14
15
|
const INPUT_KEYS = ["title", "kind", "target", "field", "notes", "answer", "answer_note", "before", "after", "from", "content", "note", "supersedes", "merge_into", "why", "source"];
|
|
15
16
|
const ID = /^[a-z]{2,4}-[0-9a-hjkmnp-tv-z]{8}$/;
|
|
16
|
-
const KNOWLEDGE_TYPES = new Set(["area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "memory"]);
|
|
17
|
+
const KNOWLEDGE_TYPES = new Set(["area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "reference", "memory"]);
|
|
17
18
|
const ADD_LINK_FIELDS = new Set(["aliases", "tags", "projects", "people", "attendees", "deciders"]);
|
|
19
|
+
/** Kinds a review_notes takes in as `changes`, in the order they are stored (spec/schema.md §11.3, 0045). */
|
|
20
|
+
export const CHANGE_KINDS = ["update_fact", "update_body", "add_to_note"];
|
|
18
21
|
/** Fields that make two pending proposals the same change (dedupe, spec/cli.md §5.6). */
|
|
19
22
|
const DEDUPE_KEYS = ["kind", "target", "field", "after", "content", "note", "supersedes", "merge_into", "answer", "answer_note"];
|
|
20
23
|
/**
|
|
@@ -40,7 +43,7 @@ export function stageProposals(workspace, items, now) {
|
|
|
40
43
|
if (item.kind === "new_item")
|
|
41
44
|
checkNewItem(workspace, item.note, `/proposals/${index}/note`, today);
|
|
42
45
|
});
|
|
43
|
-
// One review_notes per source; it takes in that source's add_link items (spec/cli.md §5.6, 0043).
|
|
46
|
+
// One review_notes per source; it takes in that source's add_link items and changes (spec/cli.md §5.6, 0043, 0045).
|
|
44
47
|
const reviews = new Map();
|
|
45
48
|
list.forEach((item, index) => {
|
|
46
49
|
if (item.kind !== "review_notes")
|
|
@@ -49,7 +52,7 @@ export function stageProposals(workspace, items, now) {
|
|
|
49
52
|
if (reviews.has(source))
|
|
50
53
|
assertNoProblems([{ pointer: `/proposals/${index}/source`, message: `a second review_notes for ${source} in one input` }]);
|
|
51
54
|
checkReviewNotes(workspace, item, `/proposals/${index}`);
|
|
52
|
-
reviews.set(source, { index, item, links: [] });
|
|
55
|
+
reviews.set(source, { index, item, links: [], changes: [] });
|
|
53
56
|
});
|
|
54
57
|
const start = workspace.checkpoint();
|
|
55
58
|
try {
|
|
@@ -62,7 +65,12 @@ export function stageProposals(workspace, items, now) {
|
|
|
62
65
|
skipped.push({ input_index: index, reason: "ALREADY_ANSWERED" });
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
65
|
-
|
|
68
|
+
if (item.kind === "update_body" && bodyOf(String(fields.content)) === workspace.get(String(fields.target)).body) {
|
|
69
|
+
skipped.push({ input_index: index, reason: "NO_CHANGE" });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const folds = item.kind === "add_link" || CHANGE_KINDS.includes(String(item.kind));
|
|
73
|
+
const review = folds ? reviews.get(String(fields.source)) : undefined;
|
|
66
74
|
if (item.kind === "add_link" && listField(workspace.get(String(fields.target)), String(fields.field)).some((value) => isDeepStrictEqual(value, fields.after))) {
|
|
67
75
|
skipped.push({ input_index: index, reason: "ALREADY_LINKED" });
|
|
68
76
|
return;
|
|
@@ -72,11 +80,20 @@ export function stageProposals(workspace, items, now) {
|
|
|
72
80
|
? (note.attributes.kind === "review_notes"
|
|
73
81
|
? review !== undefined && reviewLinks(note.attributes).some((link) => link.target === fields.target && link.field === fields.field && isDeepStrictEqual(link.value, fields.after))
|
|
74
82
|
: note.attributes.target === fields.target && proposalLinks(note.attributes).some((link) => link.field === fields.field && isDeepStrictEqual(link.value, fields.after)))
|
|
75
|
-
: DEDUPE_KEYS.every((key) => isDeepStrictEqual(note.attributes[key], fields[key]))
|
|
83
|
+
: DEDUPE_KEYS.every((key) => isDeepStrictEqual(note.attributes[key], fields[key]))
|
|
84
|
+
|| (CHANGE_KINDS.includes(String(item.kind)) && note.attributes.kind === "review_notes" && listOf(note.attributes.changes).some((change) => sameChange(change, fields)))));
|
|
76
85
|
if (duplicate !== undefined) {
|
|
77
86
|
skipped.push({ input_index: index, reason: "DUPLICATE_PENDING", existing: duplicate.id });
|
|
78
87
|
return;
|
|
79
88
|
}
|
|
89
|
+
if (review !== undefined && CHANGE_KINDS.includes(String(item.kind))) {
|
|
90
|
+
if (review.changes.some(({ change }) => sameChange(change, fields))) {
|
|
91
|
+
skipped.push({ input_index: index, reason: "DUPLICATE_PENDING" });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
review.changes.push({ index, change: changeEntry(fields) });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
80
97
|
if (item.kind === "add_link") {
|
|
81
98
|
const key = JSON.stringify([fields.target, fields.source, fields.field, fields.after]);
|
|
82
99
|
if (linkKeys.has(key)) {
|
|
@@ -111,20 +128,24 @@ export function stageProposals(workspace, items, now) {
|
|
|
111
128
|
}
|
|
112
129
|
for (const review of reviews.values()) {
|
|
113
130
|
const notes = review.item.notes;
|
|
114
|
-
if (notes.length === 0 && review.links.length === 0) {
|
|
131
|
+
if (notes.length === 0 && review.links.length === 0 && review.changes.length === 0) {
|
|
115
132
|
skipped.push({ input_index: review.index, reason: "NOTHING_TO_REVIEW" });
|
|
116
133
|
continue;
|
|
117
134
|
}
|
|
118
135
|
const source = String(review.item.source);
|
|
136
|
+
// Changes are stored in render order: every update_fact, then update_body, then add_to_note (spec/schema.md §11.3).
|
|
137
|
+
const changes = [...review.changes].sort((a, b) => CHANGE_KINDS.indexOf(String(a.change.kind)) - CHANGE_KINDS.indexOf(String(b.change.kind)) || a.index - b.index);
|
|
119
138
|
const fields = {
|
|
120
139
|
kind: "review_notes",
|
|
121
|
-
title: review.item.title ?? reviewTitle(workspace, source, notes.length, review.links.length),
|
|
140
|
+
title: review.item.title ?? reviewTitle(workspace, source, changes.length, notes.length, review.links.length),
|
|
122
141
|
notes,
|
|
123
142
|
links: review.links.map(({ fields }) => ({ target: fields.target, field: fields.field, value: fields.after })),
|
|
124
|
-
|
|
143
|
+
...(changes.length === 0 ? {} : { changes: changes.map(({ change }) => change) }),
|
|
144
|
+
why: review.item.why ?? `What came in from ${sourceTitle(workspace, source)}`,
|
|
125
145
|
source,
|
|
126
146
|
};
|
|
127
|
-
const
|
|
147
|
+
const held = [review.index, ...review.links.map(({ index }) => index), ...changes.map(({ index }) => index)].sort((a, b) => a - b);
|
|
148
|
+
const proposal = createProposal(workspace, fields, held, today, `/proposals/${review.index}`, pointers, created);
|
|
128
149
|
for (const link of review.links)
|
|
129
150
|
createdLinks.set(JSON.stringify([link.fields.target, link.fields.source, link.fields.field, link.fields.after]), proposal.id);
|
|
130
151
|
}
|
|
@@ -148,6 +169,11 @@ function createProposal(workspace, fields, inputIndexes, today, pointer, pointer
|
|
|
148
169
|
created.push(proposal);
|
|
149
170
|
return proposal;
|
|
150
171
|
}
|
|
172
|
+
/** Write a new pending proposal with these fields (apply's deferred row, spec/schema.md §11.3). Not validated. */
|
|
173
|
+
export function writeProposal(workspace, fields, today) {
|
|
174
|
+
const { id, path } = createProposal(workspace, fields, [], today, "", new Map(), []);
|
|
175
|
+
return { id, path };
|
|
176
|
+
}
|
|
151
177
|
/** The `{target, field, value}` links of a review_notes proposal. */
|
|
152
178
|
function reviewLinks(attributes) {
|
|
153
179
|
return Array.isArray(attributes.links) ? attributes.links.filter(isRecord).map((link) => ({ target: link.target, field: link.field, value: link.value })) : [];
|
|
@@ -165,17 +191,38 @@ function reviewedIds(workspace) {
|
|
|
165
191
|
return listed;
|
|
166
192
|
}
|
|
167
193
|
/** The meeting's title for a meeting id, else the source's (spec/cli.md §5.6). */
|
|
168
|
-
function sourceTitle(workspace, source) {
|
|
194
|
+
export function sourceTitle(workspace, source) {
|
|
169
195
|
return String(workspace.get(source)?.attributes.title ?? source);
|
|
170
196
|
}
|
|
171
|
-
|
|
197
|
+
/** `From <source title>: 1 change, 3 new notes and 2 links` (spec/cli.md §5.6, 0045); a deferred row starts `Deferred from`. */
|
|
198
|
+
export function reviewTitle(workspace, source, changes, notes, links, lead = "From") {
|
|
172
199
|
const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
200
|
+
const parts = [[changes, "change", "changes"], [notes, "new note", "new notes"], [links, "link", "links"]];
|
|
201
|
+
const counts = parts.filter(([n]) => n > 0).map(([n, one, many]) => count(n, one, many));
|
|
202
|
+
const joined = counts.length <= 1 ? counts.join("") : `${counts.slice(0, -1).join(", ")} and ${counts.at(-1)}`;
|
|
203
|
+
const title = `${lead} ${sourceTitle(workspace, source)}: ${joined}`;
|
|
177
204
|
return title.length > 120 ? `${title.slice(0, 119)}…` : title;
|
|
178
205
|
}
|
|
206
|
+
/** A change entry of a review_notes: the item's kind, target, why and that kind's fields (spec/schema.md §11). */
|
|
207
|
+
function changeEntry(fields) {
|
|
208
|
+
const keys = ["kind", "target", "why", "field", "before", "after", "from", "content", "before_hash"];
|
|
209
|
+
return Object.fromEntries(keys.filter((key) => fields[key] !== undefined).map((key) => [key, fields[key]]));
|
|
210
|
+
}
|
|
211
|
+
function sameChange(change, fields) {
|
|
212
|
+
return isRecord(change) && ["kind", "target", "field", "after", "content"].every((key) => isDeepStrictEqual(change[key], fields[key]));
|
|
213
|
+
}
|
|
214
|
+
/** A reference's body as stored, for `content`: one blank line after the frontmatter, LF endings, a final newline. */
|
|
215
|
+
export function bodyOf(content) {
|
|
216
|
+
const text = content.replace(/\r\n?/g, "\n").replace(/^\n+/, "");
|
|
217
|
+
return `\n${text.endsWith("\n") ? text : `${text}\n`}`;
|
|
218
|
+
}
|
|
219
|
+
/** The body hash an update_body is checked against (the source hash rule, spec/schema.md §4). */
|
|
220
|
+
export function bodyHash(body) {
|
|
221
|
+
return `sha256:${createHash("sha256").update(body, "utf8").digest("hex")}`;
|
|
222
|
+
}
|
|
223
|
+
function listOf(value) {
|
|
224
|
+
return Array.isArray(value) ? value : [];
|
|
225
|
+
}
|
|
179
226
|
/** A review_notes item's notes: existing knowledge notes, listed once here and by no other review_notes. */
|
|
180
227
|
function checkReviewNotes(workspace, item, pointer) {
|
|
181
228
|
const source = workspace.get(String(item.source));
|
|
@@ -248,6 +295,8 @@ function checkShape(item, pointer) {
|
|
|
248
295
|
}
|
|
249
296
|
if (item.kind === "answer" && (typeof item.answer !== "string" || item.answer.trim() === ""))
|
|
250
297
|
at("answer", "must be a non-empty string");
|
|
298
|
+
if (item.kind === "update_body" && (typeof item.content !== "string" || item.content.trim() === ""))
|
|
299
|
+
at("content", "must be the whole new body");
|
|
251
300
|
if (item.kind !== "answer") {
|
|
252
301
|
for (const key of ["answer", "answer_note"])
|
|
253
302
|
if (item[key] !== undefined)
|
|
@@ -324,6 +373,11 @@ function resolveFields(workspace, item, pointer, today) {
|
|
|
324
373
|
fields.from = typeof meeting?.attributes.date === "string" ? meeting.attributes.date : today;
|
|
325
374
|
}
|
|
326
375
|
}
|
|
376
|
+
if (kind === "update_body") {
|
|
377
|
+
if (target.type !== "reference")
|
|
378
|
+
invalid("target", `is a ${target.type}; update_body replaces the body of a reference`);
|
|
379
|
+
fields.before_hash = bodyHash(target.body);
|
|
380
|
+
}
|
|
327
381
|
if (kind === "add_link") {
|
|
328
382
|
const field = typeof item.field === "string" ? item.field : "";
|
|
329
383
|
if (!ADD_LINK_FIELDS.has(field))
|
|
@@ -403,7 +457,7 @@ export function listProposals(workspace, statuses, target) {
|
|
|
403
457
|
.filter((note) => note.type === "proposal")
|
|
404
458
|
.filter((note) => statuses.length === 0 || statuses.includes(String(note.attributes.status)))
|
|
405
459
|
.filter((note) => target === undefined || note.attributes.target === target
|
|
406
|
-
|| (note.attributes.kind === "review_notes" && ((Array.isArray(note.attributes.notes) && note.attributes.notes.includes(target)) || reviewLinks(note.attributes).some((link) => link.target === target))))
|
|
460
|
+
|| (note.attributes.kind === "review_notes" && ((Array.isArray(note.attributes.notes) && note.attributes.notes.includes(target)) || reviewLinks(note.attributes).some((link) => link.target === target) || listOf(note.attributes.changes).some((change) => isRecord(change) && change.target === target))))
|
|
407
461
|
.map((note) => ({ ...note.attributes, path: note.path }))
|
|
408
462
|
.sort((a, b) => compareCodePoints(String(a.created), String(b.created)) || compareCodePoints(String(a.id), String(b.id)));
|
|
409
463
|
}
|
|
@@ -436,7 +490,7 @@ export function stageBacklog(workspace, now) {
|
|
|
436
490
|
claimed.add(note.id);
|
|
437
491
|
const fields = {
|
|
438
492
|
kind: "review_notes",
|
|
439
|
-
title: reviewTitle(workspace, provenance, notes.length, 0),
|
|
493
|
+
title: reviewTitle(workspace, provenance, 0, notes.length, 0),
|
|
440
494
|
notes: notes.map((note) => note.id),
|
|
441
495
|
links: [],
|
|
442
496
|
why: `Notes from ${sourceTitle(workspace, provenance)} waiting for your review`,
|
package/dist/lib/query.js
CHANGED
|
@@ -4,7 +4,7 @@ import { BrainError } from "./errors.js";
|
|
|
4
4
|
import { compareCodePoints } from "./notes.js";
|
|
5
5
|
/** V1 note types accepted by `brain query --types` (spec/cli.md §5.13). */
|
|
6
6
|
export const QUERY_TYPES = [
|
|
7
|
-
"area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "source", "view", "memory", "proposal",
|
|
7
|
+
"area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "reference", "source", "view", "memory", "proposal",
|
|
8
8
|
];
|
|
9
9
|
const DEFAULT_TYPES = new Set(QUERY_TYPES.filter((type) => !["view", "proposal", "memory"].includes(type)));
|
|
10
10
|
const STOPWORDS = new Set([
|
package/dist/lib/schemas.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Ajv2020 } from "ajv/dist/2020.js";
|
|
|
3
3
|
/** The JSON Schemas of spec/schemas/, the single source of truth for file shapes. */
|
|
4
4
|
const SCHEMA_DIR = new URL("../../spec/schemas/", import.meta.url);
|
|
5
5
|
const KNOWLEDGE_TYPES = new Set([
|
|
6
|
-
"area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "memory",
|
|
6
|
+
"area", "goal", "project", "person", "org", "meeting", "decision", "question", "concept", "reference", "memory",
|
|
7
7
|
]);
|
|
8
8
|
let cached;
|
|
9
9
|
export function loadSchemas() {
|