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/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { applyAll
|
|
3
|
+
import { applyAll } from "./apply.js";
|
|
4
4
|
import { BrainError } from "./errors.js";
|
|
5
5
|
import { assertNoProblems, isRecord, unknownKeys } from "./input.js";
|
|
6
6
|
import { generateId, TYPE_PREFIX } from "./ids.js";
|
|
@@ -9,11 +9,13 @@ import { compareCodePoints } from "./notes.js";
|
|
|
9
9
|
import { fullText, splitParts } from "./parts.js";
|
|
10
10
|
import { loadSchemas } from "./schemas.js";
|
|
11
11
|
import { formatDate, formatTimestamp, normalizeTimestamp } from "./time.js";
|
|
12
|
-
|
|
12
|
+
import { lineDiff } from "./diff.js";
|
|
13
|
+
import { bodyHash, bodyOf } from "./proposals.js";
|
|
14
|
+
const DATABASES = ["areas", "goals", "key_results", "projects", "people", "orgs", "decisions", "questions", "concepts", "references", "review", "inbox"];
|
|
13
15
|
const CREATABLE = new Set(["areas", "goals", "projects", "people", "orgs"]);
|
|
14
|
-
const TYPE_FOR_DATABASE = { areas: "area", goals: "goal", projects: "project", people: "person", orgs: "org", decisions: "decision", questions: "question", concepts: "concept", review: "proposal" };
|
|
15
|
-
const DATABASE_FOR_TYPE = { area: "areas", goal: "goals", project: "projects", person: "people", org: "orgs", decision: "decisions", question: "questions", concept: "concepts", source: "inbox", meeting: "meetings", proposal: "review" };
|
|
16
|
-
const PUBLISHED_TYPES = new Set(["area", "goal", "project", "person", "org", "decision", "question", "concept", "source", "meeting", "proposal"]);
|
|
16
|
+
const TYPE_FOR_DATABASE = { areas: "area", goals: "goal", projects: "project", people: "person", orgs: "org", decisions: "decision", questions: "question", concepts: "concept", references: "reference", review: "proposal" };
|
|
17
|
+
const DATABASE_FOR_TYPE = { area: "areas", goal: "goals", project: "projects", person: "people", org: "orgs", decision: "decisions", question: "questions", concept: "concepts", reference: "references", source: "inbox", meeting: "meetings", proposal: "review" };
|
|
18
|
+
const PUBLISHED_TYPES = new Set(["area", "goal", "project", "person", "org", "decision", "question", "concept", "reference", "source", "meeting", "proposal"]);
|
|
17
19
|
function property(value, key) {
|
|
18
20
|
return Object.hasOwn(value, key) ? value[key] : undefined;
|
|
19
21
|
}
|
|
@@ -72,7 +74,7 @@ export function parseSnapshot(input) {
|
|
|
72
74
|
}
|
|
73
75
|
/** Compute the deterministic, no-network Notion pull plan (cli §5.14.2). */
|
|
74
76
|
export function buildPlan(workspace, snapshot) {
|
|
75
|
-
const result = { as_of: snapshot.as_of, create: [], updates: [], corrections: [], decisions: [], full_text_requests: [], conflicts: [], ignored: [] };
|
|
77
|
+
const result = { as_of: snapshot.as_of, create: [], updates: [], corrections: [], decisions: [], full_text_requests: [], refresh_requests: [], report_fixes: [], conflicts: [], ignored: [] };
|
|
76
78
|
const createsByPage = new Map();
|
|
77
79
|
for (const row of snapshot.rows)
|
|
78
80
|
if (CREATABLE.has(row.database) && blank(property(row.properties, "Brain ID"))) {
|
|
@@ -296,44 +298,56 @@ function planKeyFacts(row, note, published, updates, out) {
|
|
|
296
298
|
updates.published = { ...(isRecord(updates.published) ? updates.published : {}), "Key Facts": text };
|
|
297
299
|
}
|
|
298
300
|
function planDecision(row, proposal, out) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
+
// A snapshot taken before migration 0009 has Decision and Note; Edit there is read as Fix (cli §5.14.2, 0045).
|
|
302
|
+
const call = property(row.properties, "Your call") ?? property(row.properties, "Decision");
|
|
303
|
+
const label = Object.hasOwn(row.properties, "Your call") ? "Your call" : "Decision";
|
|
304
|
+
const textLabel = Object.hasOwn(row.properties, "What to fix") || !Object.hasOwn(row.properties, "Note") ? "What to fix" : "Note";
|
|
305
|
+
if (call === undefined || call === null || property(row.properties, "State") !== "Pending")
|
|
301
306
|
return;
|
|
302
307
|
if (["applied", "closed"].includes(String(proposal.attributes.status))) {
|
|
303
308
|
out.ignored.push({ database: row.database, page_id: row.page_id, brain_id: proposal.id, reason: "ALREADY_TERMINAL" });
|
|
304
309
|
return;
|
|
305
310
|
}
|
|
306
|
-
const action = String(
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
const action = String(call).toLowerCase() === "edit" ? "fix" : String(call).toLowerCase();
|
|
312
|
+
const text = property(row.properties, textLabel);
|
|
313
|
+
const written = typeof text === "string" && text.trim() !== "";
|
|
314
|
+
if (!["approve", "reject", "fix"].includes(action)) {
|
|
315
|
+
out.ignored.push({ database: row.database, page_id: row.page_id, brain_id: proposal.id, property: label, reason: "UNKNOWN_OPTION" });
|
|
310
316
|
return;
|
|
311
317
|
}
|
|
312
|
-
if (action === "
|
|
313
|
-
out.conflicts.push(conflict(row, "
|
|
318
|
+
if (action === "fix" && !written) {
|
|
319
|
+
out.conflicts.push(conflict(row, "FIX_WITHOUT_TEXT", "Fix needs What to fix: what to correct", "What to fix"));
|
|
314
320
|
return;
|
|
315
321
|
}
|
|
316
|
-
if (action === "
|
|
317
|
-
out.conflicts.push(conflict(row, "
|
|
322
|
+
if (action === "fix") {
|
|
323
|
+
out.conflicts.push(conflict(row, "FIX_TO_READ", "What to fix needs reading by the skill", "What to fix", text, "Fix"));
|
|
318
324
|
return;
|
|
319
325
|
}
|
|
320
|
-
if (
|
|
321
|
-
out.conflicts.push(conflict(row, "NOTE_TO_READ", "
|
|
326
|
+
if (written) {
|
|
327
|
+
out.conflicts.push(conflict(row, "NOTE_TO_READ", "What to fix needs reading by the skill", "What to fix", text, action === "approve" ? "Approve" : "Reject"));
|
|
322
328
|
return;
|
|
323
329
|
}
|
|
324
|
-
out.decisions.push({ proposal: proposal.id, action: action,
|
|
330
|
+
out.decisions.push({ proposal: proposal.id, action: action, decided_at: row.last_edited, decided_by: "notion" });
|
|
325
331
|
}
|
|
326
|
-
/** An Inbox row is read
|
|
332
|
+
/** An Inbox row is read for its Full text box (0043) and, on a Report row, Refresh and What to fix (cli §5.14.2, 0045). */
|
|
327
333
|
function planInbox(workspace, row, out) {
|
|
334
|
+
const brainId = property(row.properties, "Brain ID");
|
|
335
|
+
const note = typeof brainId === "string" && brainId !== "" ? workspace.get(brainId) : undefined;
|
|
336
|
+
if (note !== undefined && isReport(note)) {
|
|
337
|
+
if (property(row.properties, "Refresh") === true)
|
|
338
|
+
out.refresh_requests.push({ page_id: row.page_id, report: note.id });
|
|
339
|
+
const text = property(row.properties, "What to fix");
|
|
340
|
+
const last = (Array.isArray(note.attributes.fixes) ? note.attributes.fixes : []).filter(isRecord).at(-1);
|
|
341
|
+
if (typeof text === "string" && text.trim() !== "" && text !== last?.text)
|
|
342
|
+
out.report_fixes.push({ page_id: row.page_id, report: note.id, text });
|
|
343
|
+
}
|
|
328
344
|
if (property(row.properties, "Full text") !== true)
|
|
329
345
|
return;
|
|
330
|
-
const brainId = property(row.properties, "Brain ID");
|
|
331
346
|
const ignore = (id) => out.ignored.push({ database: row.database, page_id: row.page_id, brain_id: id, property: "Full text", reason: "NO_FULL_TEXT" });
|
|
332
347
|
if (typeof brainId !== "string" || brainId === "") {
|
|
333
348
|
ignore(null);
|
|
334
349
|
return;
|
|
335
350
|
}
|
|
336
|
-
const note = workspace.get(brainId);
|
|
337
351
|
if (note === undefined) {
|
|
338
352
|
out.conflicts.push(conflict(row, "UNKNOWN_BRAIN_ID", `No note has Brain ID ${brainId}`));
|
|
339
353
|
return;
|
|
@@ -369,7 +383,7 @@ function planKeyResult(workspace, asOf, row, out) {
|
|
|
369
383
|
}
|
|
370
384
|
function sortPlan(plan) {
|
|
371
385
|
const order = (item) => [DATABASES.indexOf(String(item.database)), String(item.page_id ?? item.id), String(item.field ?? item.property ?? "")].join("\u0000");
|
|
372
|
-
for (const values of [plan.create, plan.updates, plan.corrections, plan.decisions, plan.full_text_requests, plan.conflicts, plan.ignored])
|
|
386
|
+
for (const values of [plan.create, plan.updates, plan.corrections, plan.decisions, plan.full_text_requests, plan.refresh_requests, plan.report_fixes, plan.conflicts, plan.ignored])
|
|
373
387
|
values.sort((a, b) => order(a).localeCompare(order(b)));
|
|
374
388
|
}
|
|
375
389
|
function notionFields(database, p, asOf) {
|
|
@@ -382,7 +396,7 @@ function notionFields(database, p, asOf) {
|
|
|
382
396
|
}
|
|
383
397
|
function brainProperties(database) {
|
|
384
398
|
const common = [["Name", "title"], ["Summary", "summary"], ["Lifecycle", "lifecycle"]];
|
|
385
|
-
const extras = { goals: [["Why", "why"], ["Deadline", "facts.deadline"], ["Area", "area"]], projects: [["Key Facts", "facts"], ["Area", "area"], ["Goal", "goal"], ["Client", "client"], ["People", "people"]], people: [["Role", "facts.role"], ["Org", "org"]], decisions: [["Assessment", "assessment"], ["Project", "projects"], ["Area", "derived_area"]], questions: [["Kind", "kind"], ["Project", "projects"], ["Area", "derived_area"]], concepts: [["Project", "concept_projects"], ["Area", "concept_area"]] };
|
|
399
|
+
const extras = { goals: [["Why", "why"], ["Deadline", "facts.deadline"], ["Area", "area"]], projects: [["Key Facts", "facts"], ["Area", "area"], ["Goal", "goal"], ["Client", "client"], ["People", "people"]], people: [["Role", "facts.role"], ["Org", "org"]], decisions: [["Assessment", "assessment"], ["Project", "projects"], ["Area", "derived_area"]], questions: [["Kind", "kind"], ["Project", "projects"], ["Area", "derived_area"]], concepts: [["Project", "concept_projects"], ["Area", "concept_area"]], references: [["Project", "reference_projects"], ["Area", "reference_area"]] };
|
|
386
400
|
return [...common, ...(extras[database] ?? [])];
|
|
387
401
|
}
|
|
388
402
|
function convertedValue(workspace, row, name, value, conflicts) {
|
|
@@ -446,9 +460,10 @@ export function parsePlan(input) {
|
|
|
446
460
|
if (!isRecord(input))
|
|
447
461
|
assertNoProblems([{ pointer: "", message: "must be an object" }]);
|
|
448
462
|
const p = input;
|
|
449
|
-
const problems = unknownKeys(p, ["as_of", "create", "updates", "corrections", "decisions", "full_text_requests", "conflicts", "ignored"], "");
|
|
450
|
-
|
|
451
|
-
|
|
463
|
+
const problems = unknownKeys(p, ["as_of", "create", "updates", "corrections", "decisions", "full_text_requests", "refresh_requests", "report_fixes", "conflicts", "ignored"], "");
|
|
464
|
+
for (const key of ["full_text_requests", "refresh_requests", "report_fixes"])
|
|
465
|
+
if (p[key] !== undefined && !Array.isArray(p[key]))
|
|
466
|
+
problems.push({ pointer: `/${key}`, message: "must be a list" });
|
|
452
467
|
if (!isTimestamp(p.as_of))
|
|
453
468
|
problems.push({ pointer: "/as_of", message: "must be an ISO 8601 UTC timestamp" });
|
|
454
469
|
for (const key of ["create", "updates", "corrections", "decisions", "conflicts", "ignored"])
|
|
@@ -466,7 +481,7 @@ export function parsePlan(input) {
|
|
|
466
481
|
problems.push({ pointer: `/create/${index}/lifecycle`, message: "must be a lifecycle string" });
|
|
467
482
|
});
|
|
468
483
|
assertNoProblems(problems);
|
|
469
|
-
return { ...p, full_text_requests: p.full_text_requests ?? [], as_of: normalizeTimestamp(p.as_of) };
|
|
484
|
+
return { ...p, full_text_requests: p.full_text_requests ?? [], refresh_requests: p.refresh_requests ?? [], report_fixes: p.report_fixes ?? [], as_of: normalizeTimestamp(p.as_of) };
|
|
470
485
|
}
|
|
471
486
|
export function applyPlan(workspace, plan, now, recordOnly) {
|
|
472
487
|
const create = [];
|
|
@@ -538,17 +553,17 @@ export function applyPlan(workspace, plan, now, recordOnly) {
|
|
|
538
553
|
...updates.filter((item) => item.result === "applied").map((item) => ({ op: "sync-update", id: String(item.id), title: String(workspace.get(String(item.id))?.attributes.title ?? item.id) })),
|
|
539
554
|
...(correctionApply?.log ?? []), ...(decisionApply.log ?? []),
|
|
540
555
|
];
|
|
541
|
-
return { create, updates, corrections, decisions: decisionResult, full_text_requests: plan.full_text_requests, conflicts: plan.conflicts, ignored: plan.ignored, summary: summarize([...create, ...updates, ...corrections, ...decisionResult].map((item) => ({ ...item }))), log };
|
|
556
|
+
return { create, updates, corrections, decisions: decisionResult, full_text_requests: plan.full_text_requests, refresh_requests: plan.refresh_requests, report_fixes: plan.report_fixes, conflicts: plan.conflicts, ignored: plan.ignored, summary: summarize([...create, ...updates, ...corrections, ...decisionResult].map((item) => ({ ...item }))), log };
|
|
542
557
|
}
|
|
543
558
|
function references(value) { if (Array.isArray(value))
|
|
544
559
|
return value.flatMap(references); if (isRecord(value))
|
|
545
|
-
return Object.values(value).flatMap(references); return typeof value === "string" && /^(area|gol|prj|per|org|mtg|dec|que|con|src|mem)-/.test(value) ? [value] : []; }
|
|
560
|
+
return Object.values(value).flatMap(references); return typeof value === "string" && /^(area|gol|prj|per|org|mtg|dec|que|con|ref|src|mem)-/.test(value) ? [value] : []; }
|
|
546
561
|
function summarize(items) { const result = { applied: 0, closed: 0, recorded: 0, corrected: 0, skipped: 0, failed: 0 }; for (const item of items)
|
|
547
562
|
if (typeof item.result === "string" && Object.hasOwn(result, item.result))
|
|
548
563
|
result[item.result] += 1; return result; }
|
|
549
564
|
export function render(workspace, now, ids, held) {
|
|
550
565
|
const selected = ids === undefined
|
|
551
|
-
? workspace.notes.filter((note) => (PUBLISHED_TYPES.has(note.type) && !(note.type === "source" && note.attributes.kind === undefined)) ||
|
|
566
|
+
? workspace.notes.filter((note) => (PUBLISHED_TYPES.has(note.type) && !(note.type === "source" && note.attributes.kind === undefined)) || isReport(note))
|
|
552
567
|
: ids.map((id) => workspace.get(id)).filter(Boolean);
|
|
553
568
|
const items = [];
|
|
554
569
|
const errors = [];
|
|
@@ -557,9 +572,6 @@ export function render(workspace, now, ids, held) {
|
|
|
557
572
|
const database = isReport(note) ? "inbox" : DATABASE_FOR_TYPE[note.type];
|
|
558
573
|
if (database === undefined || (note.type === "source" && note.attributes.kind === undefined))
|
|
559
574
|
continue;
|
|
560
|
-
// A published report is never rendered again (0043).
|
|
561
|
-
if (isReport(note) && note.attributes.notion !== undefined)
|
|
562
|
-
continue;
|
|
563
575
|
// An attached source has no Inbox row: its notes go on the parent's (0042).
|
|
564
576
|
if (note.type === "source" && note.attributes.attached_to !== undefined)
|
|
565
577
|
continue;
|
|
@@ -593,7 +605,7 @@ export function render(workspace, now, ids, held) {
|
|
|
593
605
|
const properties = {};
|
|
594
606
|
const pending = [];
|
|
595
607
|
const heldProperties = held?.get(note.id) ?? new Set();
|
|
596
|
-
const isCreate =
|
|
608
|
+
const isCreate = note.attributes.notion === undefined && capturePage === undefined;
|
|
597
609
|
for (const [name, value] of Object.entries(rendered)) {
|
|
598
610
|
if (name === "@content" || heldProperties.has(name) || (capturePage !== undefined && name === "Name"))
|
|
599
611
|
continue;
|
|
@@ -605,6 +617,21 @@ export function render(workspace, now, ids, held) {
|
|
|
605
617
|
if (content.hash !== undefined)
|
|
606
618
|
rendered["@content"] = content.hash;
|
|
607
619
|
const contentChanged = content.hash !== undefined && !isDeepStrictEqual(content.hash, published["@content"]);
|
|
620
|
+
// A report (0045): a refresh sends the body once and clears Refresh; a new fix clears What to fix.
|
|
621
|
+
let reportDue = false;
|
|
622
|
+
if (isReport(note)) {
|
|
623
|
+
const fixes = Array.isArray(note.attributes.fixes) ? note.attributes.fixes.length : 0;
|
|
624
|
+
const stored = typeof published["@fixes"] === "number" ? published["@fixes"] : 0;
|
|
625
|
+
if (!isCreate && contentChanged)
|
|
626
|
+
properties.Refresh = { type: "checkbox", value: false };
|
|
627
|
+
if (!isCreate && !contentChanged)
|
|
628
|
+
content.body = null;
|
|
629
|
+
if (fixes > stored) {
|
|
630
|
+
properties["What to fix"] = { type: "rich_text", value: null };
|
|
631
|
+
reportDue = true;
|
|
632
|
+
}
|
|
633
|
+
rendered["@fixes"] = Math.max(fixes, stored);
|
|
634
|
+
}
|
|
608
635
|
// Full text (0043): the text goes out once per choice, and a ticked box is cleared once per request.
|
|
609
636
|
const full = note.type === "source" && hasInboxBody(note) ? fullTextDue(note, published["@full_text"]) : undefined;
|
|
610
637
|
if (full?.sections !== undefined)
|
|
@@ -614,7 +641,7 @@ export function render(workspace, now, ids, held) {
|
|
|
614
641
|
if (full !== undefined && (full.sections !== undefined || full.clear))
|
|
615
642
|
rendered["@full_text"] = full.record;
|
|
616
643
|
const fullTextDueNow = full !== undefined && (full.sections !== undefined || full.clear);
|
|
617
|
-
if (!isCreate && Object.keys(properties).length === 0 && !contentChanged && !fullTextDueNow)
|
|
644
|
+
if (!isCreate && Object.keys(properties).length === 0 && !contentChanged && !fullTextDueNow && !reportDue)
|
|
618
645
|
continue;
|
|
619
646
|
if (!isCreate && note.type !== "meeting" && note.type !== "proposal")
|
|
620
647
|
properties["Last Synced"] = { type: "date", value: { start: formatTimestamp(now), end: null } };
|
|
@@ -644,7 +671,9 @@ function renderedBrainValues(note, workspace) {
|
|
|
644
671
|
const addLink = a.kind === "add_link";
|
|
645
672
|
const after = addLink
|
|
646
673
|
? proposalLinks(a).map((link) => `${fieldLabel(String(link.field))}: ${noteTitle(link.value, workspace)}`).join("\n")
|
|
647
|
-
: a.kind === "review_notes" ? reviewNotesAfter(a, workspace) : undefined;
|
|
674
|
+
: a.kind === "review_notes" ? reviewNotesAfter(a, workspace) : a.kind === "update_body" ? bodyChange(a, workspace)?.join("\n") ?? null : undefined;
|
|
675
|
+
// A body diff can only be shown while the target's body is the one proposed against; then S keeps it (0045).
|
|
676
|
+
const proposed = staleBody(a, workspace) && isRecord(a.notion_published) && Object.hasOwn(a.notion_published, "Proposed") ? a.notion_published.Proposed : undefined;
|
|
648
677
|
const reviewText = (value) => truncate(typeof value === "string" ? reviewMarkdown(value, target) : value);
|
|
649
678
|
// A Review notes row names its source: the meeting for minutes, else the source (0041).
|
|
650
679
|
const source = a.kind === "review_notes" && typeof a.source === "string" ? workspace.get(a.source) : undefined;
|
|
@@ -652,16 +681,16 @@ function renderedBrainValues(note, workspace) {
|
|
|
652
681
|
const page = (source ?? target)?.attributes.notion;
|
|
653
682
|
const meeting = typeof a.source === "string" ? workspace.get(a.source) : undefined;
|
|
654
683
|
Object.assign(values, {
|
|
655
|
-
Name: a.title, Kind: select(a.kind),
|
|
656
|
-
"
|
|
684
|
+
Name: a.title, Kind: select(a.kind), About: targetText,
|
|
685
|
+
"About Page": typeof page === "string" ? `https://www.notion.so/${page.replaceAll("-", "")}` : null,
|
|
657
686
|
"Source Meeting": meeting?.type === "meeting" && typeof meeting.attributes.notion === "string" ? [meeting.attributes.notion] : [],
|
|
658
|
-
|
|
687
|
+
Now: reviewText(proposalBefore(a, workspace)), Proposed: proposed !== undefined ? proposed : reviewText(after ?? proposalAfter(a, workspace)),
|
|
659
688
|
Why: reviewText(a.why), State: select(proposalState(a.status)), "Brain ID": note.id,
|
|
660
689
|
});
|
|
661
690
|
return values;
|
|
662
691
|
}
|
|
663
692
|
if (isReport(note)) {
|
|
664
|
-
Object.assign(values, { Name: a.title, Kind: "Report", Project: relationPages(a.projects, workspace), Area: relationPages(a.area === undefined ? [] : [a.area], workspace), Ask: typeof a.asked_in === "string" ? [a.asked_in] : [], "Brain Status": "Processed", "Brain ID": note.id });
|
|
693
|
+
Object.assign(values, { Name: a.title, Kind: "Report", Project: relationPages(a.projects, workspace), Area: relationPages(a.area === undefined ? [] : [a.area], workspace), Ask: typeof a.asked_in === "string" ? [a.asked_in] : Array.isArray(a.asked_in) ? a.asked_in : [], "Brain Status": "Processed", "Brain ID": note.id });
|
|
665
694
|
return values;
|
|
666
695
|
}
|
|
667
696
|
if (note.type === "meeting") {
|
|
@@ -692,7 +721,7 @@ function renderedBrainValues(note, workspace) {
|
|
|
692
721
|
else if (published.Status === undefined && !yours)
|
|
693
722
|
values.Status = "Open";
|
|
694
723
|
}
|
|
695
|
-
if (note.type === "concept")
|
|
724
|
+
if (note.type === "concept" || note.type === "reference")
|
|
696
725
|
Object.assign(values, { Project: relationPages(a.projects, workspace), Area: relationPages(a.area === undefined ? [] : [a.area], workspace) });
|
|
697
726
|
return values;
|
|
698
727
|
}
|
|
@@ -707,17 +736,17 @@ function projectAreas(projects, workspace) {
|
|
|
707
736
|
/** The notes a source produced, with those of the sources attached to it (cli §5.14.4, 0042). */
|
|
708
737
|
function producedBy(note, workspace) {
|
|
709
738
|
const sources = new Set([note.id, ...workspace.notes.filter((candidate) => candidate.type === "source" && candidate.attributes.attached_to === note.id).map((candidate) => candidate.id)]);
|
|
710
|
-
return workspace.notes.filter((candidate) => ["decision", "question", "concept", "person"].includes(candidate.type) && Array.isArray(candidate.attributes.sources) && candidate.attributes.sources.some((id) => sources.has(String(id))));
|
|
739
|
+
return workspace.notes.filter((candidate) => ["decision", "question", "concept", "reference", "person"].includes(candidate.type) && Array.isArray(candidate.attributes.sources) && candidate.attributes.sources.some((id) => sources.has(String(id))));
|
|
711
740
|
}
|
|
712
741
|
function sourceValues(note, workspace) {
|
|
713
742
|
const produced = producedBy(note, workspace);
|
|
714
743
|
const ids = (type) => produced.filter((candidate) => candidate.type === type).map((candidate) => candidate.id).sort(compareCodePoints);
|
|
715
744
|
const projects = [...new Set(produced.flatMap((candidate) => Array.isArray(candidate.attributes.projects) ? candidate.attributes.projects.filter((id) => typeof id === "string") : []))].sort(compareCodePoints);
|
|
716
745
|
const areas = [...new Set([...produced.map((candidate) => candidate.attributes.area).filter((id) => typeof id === "string"), ...projects.flatMap((id) => { const area = workspace.get(id)?.attributes.area; return typeof area === "string" ? [area] : []; })])].sort(compareCodePoints);
|
|
717
|
-
return { Name: note.attributes.title, Kind: select(note.attributes.kind), Summary: note.attributes.summary ?? null, Decisions: relationPages(ids("decision"), workspace), Questions: relationPages(ids("question"), workspace), Concepts: relationPages(ids("concept"), workspace), People: relationPages(ids("person"), workspace), Project: relationPages(projects, workspace), Area: relationPages(areas, workspace), "Brain Status": "Processed", "Brain ID": note.id };
|
|
746
|
+
return { Name: note.attributes.title, Kind: select(note.attributes.kind), Summary: note.attributes.summary ?? null, Decisions: relationPages(ids("decision"), workspace), Questions: relationPages(ids("question"), workspace), Concepts: relationPages(ids("concept"), workspace), ...(databaseIdFor(workspace, "references") === "" ? {} : { References: relationPages(ids("reference"), workspace) }), People: relationPages(ids("person"), workspace), Project: relationPages(projects, workspace), Area: relationPages(areas, workspace), "Brain Status": "Processed", "Brain ID": note.id };
|
|
718
747
|
}
|
|
719
748
|
function notionProperty(name, value, note, workspace, pending) {
|
|
720
|
-
const relation = ["Area", "Goal", "Client", "People", "Org", "Project", "Decided In", "Raised In", "Supersedes", "Decisions", "Questions", "Concepts", "Attendees"].includes(name);
|
|
749
|
+
const relation = ["Area", "Goal", "Client", "People", "Org", "Project", "Decided In", "Raised In", "Supersedes", "Decisions", "Questions", "Concepts", "References", "Attendees"].includes(name);
|
|
721
750
|
if (relation) {
|
|
722
751
|
const sourceField = { Area: "area", Goal: "goal", Client: "client", People: "people", Org: "org", Project: "projects", "Decided In": "decided_in", "Raised In": "raised_in", Decisions: "decisions", Questions: "questions", Attendees: "attendees" };
|
|
723
752
|
const raw = name === "Area" && (note.type === "decision" || note.type === "question") ? projectAreas(note.attributes.projects, workspace) : name === "Supersedes" ? (Array.isArray(note.attributes.relations) ? note.attributes.relations.filter(isRecord).filter((item) => item.type === "supersedes").map((item) => item.target) : []) : note.type === "source" ? sourceRelationIds(note, name, workspace) : note.attributes[sourceField[name] ?? ""];
|
|
@@ -735,7 +764,7 @@ function notionProperty(name, value, note, workspace, pending) {
|
|
|
735
764
|
}
|
|
736
765
|
if (name === "Ask")
|
|
737
766
|
return { type: "relation", value: Array.isArray(value) ? value : [] };
|
|
738
|
-
if (name === "
|
|
767
|
+
if (name === "About Page")
|
|
739
768
|
return { type: "url", value };
|
|
740
769
|
if (["Lifecycle", "Assessment", "Kind", "State", "Brain Status", "Status"].includes(name))
|
|
741
770
|
return { type: "select", value };
|
|
@@ -751,6 +780,8 @@ function sourceRelationIds(note, name, workspace) {
|
|
|
751
780
|
return produced.filter((candidate) => candidate.type === "question").map((candidate) => candidate.id);
|
|
752
781
|
if (name === "Concepts")
|
|
753
782
|
return produced.filter((candidate) => candidate.type === "concept").map((candidate) => candidate.id);
|
|
783
|
+
if (name === "References")
|
|
784
|
+
return produced.filter((candidate) => candidate.type === "reference").map((candidate) => candidate.id);
|
|
754
785
|
if (name === "People")
|
|
755
786
|
return produced.filter((candidate) => candidate.type === "person").map((candidate) => candidate.id);
|
|
756
787
|
const projects = [...new Set(produced.flatMap((candidate) => Array.isArray(candidate.attributes.projects) ? candidate.attributes.projects.filter((id) => typeof id === "string") : []))];
|
|
@@ -784,7 +815,7 @@ function renderedContent(note, workspace) {
|
|
|
784
815
|
const body = notionMarkdown(note.body.replace(/^\n+/, ""), note, workspace);
|
|
785
816
|
return { body, sections: null, hash: hash(body) };
|
|
786
817
|
}
|
|
787
|
-
if (note.type === "decision" || note.type === "question" || note.type === "concept") {
|
|
818
|
+
if (note.type === "decision" || note.type === "question" || note.type === "concept" || note.type === "reference") {
|
|
788
819
|
const body = notionMarkdown(note.body, note, workspace);
|
|
789
820
|
return { body, sections: null, hash: hash(body) };
|
|
790
821
|
}
|
|
@@ -824,14 +855,34 @@ function personTitle(value, workspace) { return typeof value === "string" && wor
|
|
|
824
855
|
function noteTitle(value, workspace) { return typeof value === "string" ? workspace.get(value)?.attributes.title ?? value : value ?? null; }
|
|
825
856
|
function fieldLabel(field) { return field[0]?.toUpperCase() + field.slice(1).replaceAll("_", " "); }
|
|
826
857
|
function truncate(value) { return typeof value === "string" ? value.slice(0, 2000) : value; }
|
|
827
|
-
/** The lettered lines of a Review notes row: its notes, then its links (cli §5.14.4). */
|
|
858
|
+
/** The lettered lines of a Review notes row: its changes, then its notes, then its links (cli §5.14.4, 0045). */
|
|
828
859
|
function reviewNotesAfter(attributes, workspace) {
|
|
860
|
+
const changes = (Array.isArray(attributes.changes) ? attributes.changes.filter(isRecord) : []).map((change) => {
|
|
861
|
+
const target = String(noteTitle(change.target, workspace));
|
|
862
|
+
if (change.kind === "update_fact")
|
|
863
|
+
return `⚠ ${fieldLabel(String(change.field))} on ${target}: ${change.before === undefined ? "(new)" : String(noteTitle(change.before, workspace))} → ${String(noteTitle(change.after, workspace))}`;
|
|
864
|
+
if (change.kind === "update_body")
|
|
865
|
+
return `⚠ ${target}: ${bodyChange(change, workspace)?.length ?? 0} lines changed`;
|
|
866
|
+
return `Add to ${target}: ${String(change.content ?? "").split("\n")[0].slice(0, 80)}`;
|
|
867
|
+
});
|
|
829
868
|
const notes = (Array.isArray(attributes.notes) ? attributes.notes : []).map((id) => {
|
|
830
869
|
const note = workspace.get(String(id));
|
|
831
870
|
return `${note === undefined ? "Note" : fieldLabel(note.type)}: ${String(noteTitle(id, workspace))}`;
|
|
832
871
|
});
|
|
833
872
|
const links = (Array.isArray(attributes.links) ? attributes.links.filter(isRecord) : []).map((link) => `${fieldLabel(String(link.field))} on ${String(noteTitle(link.target, workspace))}: ${String(noteTitle(link.value, workspace))}`);
|
|
834
|
-
return [...notes, ...links].map((line, index) => `${letters(index)}) ${line}`).join("\n");
|
|
873
|
+
return [...changes, ...notes, ...links].map((line, index) => `${letters(index)}) ${line}`).join("\n");
|
|
874
|
+
}
|
|
875
|
+
/** The changed lines an update_body proposes against its target's body (cli §5.19), or undefined once that body changed. */
|
|
876
|
+
function bodyChange(change, workspace) {
|
|
877
|
+
const target = typeof change.target === "string" ? workspace.get(change.target) : undefined;
|
|
878
|
+
if (target === undefined || bodyHash(target.body) !== change.before_hash)
|
|
879
|
+
return undefined;
|
|
880
|
+
return lineDiff(target.body, bodyOf(String(change.content ?? "")));
|
|
881
|
+
}
|
|
882
|
+
/** Whether a proposal holds an update_body whose target's body is no longer the one it was proposed against. */
|
|
883
|
+
function staleBody(attributes, workspace) {
|
|
884
|
+
const bodies = attributes.kind === "update_body" ? [attributes] : (Array.isArray(attributes.changes) ? attributes.changes.filter(isRecord) : []).filter((change) => change.kind === "update_body");
|
|
885
|
+
return bodies.some((change) => bodyChange(change, workspace) === undefined);
|
|
835
886
|
}
|
|
836
887
|
/** a, b, … z, then aa, ab, … */
|
|
837
888
|
export function letters(index) {
|
|
@@ -862,7 +913,7 @@ function digestBody(note) {
|
|
|
862
913
|
return [line, typeof a.summary === "string" ? a.summary : undefined, typeof a.digest === "string" ? a.digest.replace(/\n+$/, "") : undefined].filter((part) => part !== undefined).join("\n\n");
|
|
863
914
|
}
|
|
864
915
|
function proposalBefore(attributes, workspace) {
|
|
865
|
-
if (attributes.kind === "add_link" || attributes.kind === "review_notes" || attributes.kind === "answer")
|
|
916
|
+
if (attributes.kind === "add_link" || attributes.kind === "review_notes" || attributes.kind === "answer" || attributes.kind === "update_body")
|
|
866
917
|
return null;
|
|
867
918
|
if (attributes.kind === "supersede")
|
|
868
919
|
return truncate(noteTitle(attributes.supersedes, workspace));
|
package/package.json
CHANGED
|
File without changes
|