ctxr-cli 0.23.0 → 0.24.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.
Files changed (107) hide show
  1. package/README.md +6 -1
  2. package/dist/commands/graph-diff.d.ts +33 -0
  3. package/dist/commands/graph-diff.d.ts.map +1 -0
  4. package/dist/commands/graph-diff.js +52 -0
  5. package/dist/commands/graph-diff.js.map +1 -0
  6. package/dist/commands/publish-check.d.ts +6 -0
  7. package/dist/commands/publish-check.d.ts.map +1 -1
  8. package/dist/commands/publish-check.js +78 -3
  9. package/dist/commands/publish-check.js.map +1 -1
  10. package/dist/commands/publish-gather.d.ts.map +1 -1
  11. package/dist/commands/publish-gather.js +4 -1
  12. package/dist/commands/publish-gather.js.map +1 -1
  13. package/dist/commands/publish-new.d.ts +5 -0
  14. package/dist/commands/publish-new.d.ts.map +1 -1
  15. package/dist/commands/publish-new.js +30 -10
  16. package/dist/commands/publish-new.js.map +1 -1
  17. package/dist/commands/publish-stamp.d.ts +32 -0
  18. package/dist/commands/publish-stamp.d.ts.map +1 -0
  19. package/dist/commands/publish-stamp.js +78 -0
  20. package/dist/commands/publish-stamp.js.map +1 -0
  21. package/dist/commands/publish-urls.d.ts +6 -0
  22. package/dist/commands/publish-urls.d.ts.map +1 -1
  23. package/dist/commands/publish-urls.js +10 -1
  24. package/dist/commands/publish-urls.js.map +1 -1
  25. package/dist/commands/serve.d.ts.map +1 -1
  26. package/dist/commands/serve.js +83 -5
  27. package/dist/commands/serve.js.map +1 -1
  28. package/dist/core/browse/diagram-assets.d.ts +15 -0
  29. package/dist/core/browse/diagram-assets.d.ts.map +1 -0
  30. package/dist/core/browse/diagram-assets.js +35 -0
  31. package/dist/core/browse/diagram-assets.js.map +1 -0
  32. package/dist/core/browse/page-url.d.ts +22 -0
  33. package/dist/core/browse/page-url.d.ts.map +1 -1
  34. package/dist/core/browse/page-url.js +31 -0
  35. package/dist/core/browse/page-url.js.map +1 -1
  36. package/dist/core/browse/render.d.ts +10 -1
  37. package/dist/core/browse/render.d.ts.map +1 -1
  38. package/dist/core/browse/render.js +23 -1
  39. package/dist/core/browse/render.js.map +1 -1
  40. package/dist/core/browse/templates.d.ts +4 -1
  41. package/dist/core/browse/templates.d.ts.map +1 -1
  42. package/dist/core/browse/templates.js +18 -2
  43. package/dist/core/browse/templates.js.map +1 -1
  44. package/dist/core/checks/organize-checks.d.ts +17 -0
  45. package/dist/core/checks/organize-checks.d.ts.map +1 -1
  46. package/dist/core/checks/organize-checks.js +116 -0
  47. package/dist/core/checks/organize-checks.js.map +1 -1
  48. package/dist/core/errors.d.ts +43 -0
  49. package/dist/core/errors.d.ts.map +1 -1
  50. package/dist/core/errors.js +105 -0
  51. package/dist/core/errors.js.map +1 -1
  52. package/dist/core/git/exec.d.ts +10 -0
  53. package/dist/core/git/exec.d.ts.map +1 -1
  54. package/dist/core/git/exec.js +52 -2
  55. package/dist/core/git/exec.js.map +1 -1
  56. package/dist/core/graph/diff-block.d.ts +6 -0
  57. package/dist/core/graph/diff-block.d.ts.map +1 -0
  58. package/dist/core/graph/diff-block.js +54 -0
  59. package/dist/core/graph/diff-block.js.map +1 -0
  60. package/dist/core/graph/diff-document.d.ts +37 -0
  61. package/dist/core/graph/diff-document.d.ts.map +1 -0
  62. package/dist/core/graph/diff-document.js +119 -0
  63. package/dist/core/graph/diff-document.js.map +1 -0
  64. package/dist/core/graph/diff.d.ts +123 -0
  65. package/dist/core/graph/diff.d.ts.map +1 -0
  66. package/dist/core/graph/diff.js +253 -0
  67. package/dist/core/graph/diff.js.map +1 -0
  68. package/dist/core/graph/tree.d.ts +24 -0
  69. package/dist/core/graph/tree.d.ts.map +1 -0
  70. package/dist/core/graph/tree.js +45 -0
  71. package/dist/core/graph/tree.js.map +1 -0
  72. package/dist/core/notes/list.d.ts +10 -0
  73. package/dist/core/notes/list.d.ts.map +1 -1
  74. package/dist/core/notes/list.js +35 -12
  75. package/dist/core/notes/list.js.map +1 -1
  76. package/dist/core/publish/filing.d.ts +12 -12
  77. package/dist/core/publish/filing.d.ts.map +1 -1
  78. package/dist/core/publish/filing.js +20 -18
  79. package/dist/core/publish/filing.js.map +1 -1
  80. package/dist/core/publish/lifespan.d.ts +37 -0
  81. package/dist/core/publish/lifespan.d.ts.map +1 -0
  82. package/dist/core/publish/lifespan.js +59 -0
  83. package/dist/core/publish/lifespan.js.map +1 -0
  84. package/dist/core/publish/represents.d.ts +72 -0
  85. package/dist/core/publish/represents.d.ts.map +1 -0
  86. package/dist/core/publish/represents.js +83 -0
  87. package/dist/core/publish/represents.js.map +1 -0
  88. package/dist/run.d.ts.map +1 -1
  89. package/dist/run.js +34 -4
  90. package/dist/run.js.map +1 -1
  91. package/dist/version.d.ts +1 -1
  92. package/dist/version.js +1 -1
  93. package/package.json +2 -1
  94. package/templates/conventions/baseline-conventions.md +6 -0
  95. package/templates/graph/knowledge-changes/archived.md +1 -0
  96. package/templates/graph/knowledge-changes/bare-route.md +2 -0
  97. package/templates/graph/knowledge-changes/block.md +10 -0
  98. package/templates/graph/knowledge-changes/counts.md +1 -0
  99. package/templates/graph/knowledge-changes/dangling.md +1 -0
  100. package/templates/graph/knowledge-changes/empty.md +1 -0
  101. package/templates/graph/knowledge-changes/review.md +4 -0
  102. package/templates/serve/diagrams.mjs +136 -0
  103. package/templates/serve/shell.html +1 -1
  104. package/templates/serve/style.css +27 -0
  105. package/templates/skills/ctxr-organize-audit.md +19 -0
  106. package/templates/skills/ctxr-publish.md +100 -45
  107. package/templates/skills/ctxr-submit.md +24 -15
@@ -0,0 +1,54 @@
1
+ import { packagedTemplate, substituteBlock } from '../templates.js';
2
+ import { isEmptyDelta } from './diff.js';
3
+ /**
4
+ * review-the-knowledge-diff (D9): the **Knowledge changes** block a pull
5
+ * request body carries verbatim, written by the command so the party under
6
+ * review writes none of it. It names both commits in full and the command
7
+ * that reproduces it, gives every kind as a count, and lists in full the two
8
+ * kinds most likely to be silent losses: introduced dangling links and
9
+ * archived notes. It carries no diagram and no judgment the delta does not
10
+ * make.
11
+ *
12
+ * Every fixed sentence lives in `templates/graph/knowledge-changes/`; this
13
+ * module fills in values only.
14
+ */
15
+ function template(name) {
16
+ return packagedTemplate('graph/knowledge-changes', name);
17
+ }
18
+ const REASON_TEXT = { not_found: 'not found', ambiguous: 'ambiguous' };
19
+ function code(text) {
20
+ const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((match) => match[0].length));
21
+ const fence = '`'.repeat(longest + 1);
22
+ return `${fence}${text.startsWith('`') || text.endsWith('`') ? ` ${text} ` : text}${fence}`;
23
+ }
24
+ function bodyLines(delta) {
25
+ if (isEmptyDelta(delta))
26
+ return template('empty').split('\n');
27
+ const counts = Object.entries(delta.counts).reduce((text, [key, count]) => text.replaceAll(`__${key.toUpperCase()}__`, String(count)), template('counts'));
28
+ const lines = counts.split('\n');
29
+ if (delta.dangling.introduced.length > 0) {
30
+ lines.push('', ...template('dangling').split('\n'), '');
31
+ for (const d of delta.dangling.introduced)
32
+ lines.push(`- ${code(d.from)} → ${code(d.target)} (${REASON_TEXT[d.reason] ?? d.reason})`);
33
+ }
34
+ if (delta.notes.archived.length > 0) {
35
+ lines.push('', ...template('archived').split('\n'), '');
36
+ for (const r of delta.notes.archived)
37
+ lines.push(`- ${code(r.from)} → ${code(r.to)}`);
38
+ }
39
+ return lines;
40
+ }
41
+ /** The review address with its reachability line, and the bare-route line when no base URL is declared; empty without an address. */
42
+ export function reviewLines(review) {
43
+ if (review === null)
44
+ return [];
45
+ const text = template('review').replace('__ADDRESS__', () => review.url ?? review.route);
46
+ return ['', ...substituteBlock(text, '__BARE_ROUTE__', review.url === null ? template('bare-route').split('\n') : []).split('\n')];
47
+ }
48
+ export function renderKnowledgeChangesBlock(delta, review) {
49
+ let text = template('block').replaceAll('__BASE__', delta.base).replaceAll('__HEAD__', delta.head);
50
+ text = substituteBlock(text, '__BODY__', bodyLines(delta));
51
+ text = substituteBlock(text, '__REVIEW__', reviewLines(review));
52
+ return `${text}\n`;
53
+ }
54
+ //# sourceMappingURL=diff-block.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-block.js","sourceRoot":"","sources":["../../../src/core/graph/diff-block.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type GraphEdge } from './model.js';
2
+ import { type GraphDelta } from './diff.js';
3
+ /**
4
+ * review-the-knowledge-diff (D5): one render of a delta, used by the command's
5
+ * human summary and by the review view — one derivation, two renders, the
6
+ * same pattern as `graph.json` / `graph.md`. It carries only what the delta
7
+ * carries: paths, link targets, edge types, counts and source entries, never a
8
+ * note body, and no text an agent wrote about the change.
9
+ *
10
+ * The document is a list of parts rather than one string because the review
11
+ * view renders markdown with `html: false`: a `<details>` group is emitted by
12
+ * the caller around its rendered markdown, not parsed out of it. `toMarkdown`
13
+ * joins the parts with literal `<details>` tags for a markdown reader.
14
+ */
15
+ export type DiffDocumentPart = {
16
+ kind: 'markdown';
17
+ text: string;
18
+ } | {
19
+ kind: 'details';
20
+ summary: string;
21
+ text: string;
22
+ };
23
+ export interface DiffDocumentOptions {
24
+ /** The href for a path the caller can serve, or null to leave it as text. Absent: no path is linked. */
25
+ linkFor?: (notePath: string) => string | null;
26
+ /** Append the link-delta diagram (D8). The human summary omits it. */
27
+ diagram?: boolean;
28
+ }
29
+ /** D8: past this many changed links the diagram is replaced by one line, so it never becomes unreadable. */
30
+ export declare const MAX_DIAGRAM_EDGES = 50;
31
+ /** D8: added links solid, removed links dashed, nodes labelled by stem; edge types label a typed link. */
32
+ export declare function renderDiagram(added: readonly GraphEdge[], removed: readonly GraphEdge[]): string | null;
33
+ export declare function renderDiffParts(delta: GraphDelta, options?: DiffDocumentOptions): DiffDocumentPart[];
34
+ /** The parts as one markdown document, `<details>` groups written as literal tags. */
35
+ export declare function toMarkdown(parts: readonly DiffDocumentPart[]): string;
36
+ export declare function renderDiffDocument(delta: GraphDelta, options?: DiffDocumentOptions): string;
37
+ //# sourceMappingURL=diff-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-document.d.ts","sourceRoot":"","sources":["../../../src/core/graph/diff-document.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAE1D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvH,MAAM,WAAW,mBAAmB;IAClC,wGAAwG;IACxG,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAC9C,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,4GAA4G;AAC5G,eAAO,MAAM,iBAAiB,KAAK,CAAC;AA8BpC,0GAA0G;AAC1G,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,GAAG,IAAI,CAsBvG;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,GAAE,mBAAwB,GAAG,gBAAgB,EAAE,CAuExG;AAED,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAMrE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAE/F"}
@@ -0,0 +1,119 @@
1
+ import path from 'node:path';
2
+ import { LINK_EDGE_TYPE } from './model.js';
3
+ import { isEmptyDelta } from './diff.js';
4
+ /** D8: past this many changed links the diagram is replaced by one line, so it never becomes unreadable. */
5
+ export const MAX_DIAGRAM_EDGES = 50;
6
+ /** A code span that survives any backtick run in the text, with table pipes escaped. */
7
+ function code(text) {
8
+ const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((match) => match[0].length));
9
+ const fence = '`'.repeat(longest + 1);
10
+ const padded = text.startsWith('`') || text.endsWith('`') ? ` ${text} ` : text;
11
+ return `${fence}${padded.replace(/\|/g, '\\|')}${fence}`;
12
+ }
13
+ function pathCell(notePath, options) {
14
+ const href = options.linkFor?.(notePath) ?? null;
15
+ return href === null ? code(notePath) : `[${code(notePath)}](${href})`;
16
+ }
17
+ function table(header, rows) {
18
+ return [`| ${header.join(' | ')} |`, `|${header.map(() => '---').join('|')}|`, ...rows.map((row) => `| ${row.join(' | ')} |`)].join('\n');
19
+ }
20
+ const REASON_TEXT = { not_found: 'not found', ambiguous: 'ambiguous' };
21
+ function stemOf(notePath) {
22
+ return path.posix.basename(notePath, '.md');
23
+ }
24
+ /** Mermaid's entity form, so a label's quotes, brackets and pipes cannot end the label early. */
25
+ function mermaidLabel(text) {
26
+ return text.replace(/[#"<>|[\]{}()]/g, (ch) => `#${ch.charCodeAt(0)};`);
27
+ }
28
+ /** D8: added links solid, removed links dashed, nodes labelled by stem; edge types label a typed link. */
29
+ export function renderDiagram(added, removed) {
30
+ const changed = added.length + removed.length;
31
+ if (changed === 0)
32
+ return null;
33
+ if (changed > MAX_DIAGRAM_EDGES) {
34
+ return `The link delta has ${changed} changed links, more than the diagram draws (${MAX_DIAGRAM_EDGES}); the tables above list every one.`;
35
+ }
36
+ const ids = new Map();
37
+ for (const edge of [...added, ...removed]) {
38
+ for (const notePath of [edge.src, edge.dst])
39
+ if (!ids.has(notePath))
40
+ ids.set(notePath, `n${ids.size}`);
41
+ }
42
+ const arrow = (edge, solid) => {
43
+ const label = edge.type === LINK_EDGE_TYPE ? '' : `|${mermaidLabel(edge.type)}|`;
44
+ return ` ${ids.get(edge.src)} ${solid ? '-->' : '-.->'}${label} ${ids.get(edge.dst)}`;
45
+ };
46
+ return [
47
+ '```mermaid',
48
+ 'flowchart LR',
49
+ ...[...ids].map(([notePath, id]) => ` ${id}["${mermaidLabel(stemOf(notePath))}"]`),
50
+ ...added.map((edge) => arrow(edge, true)),
51
+ ...removed.map((edge) => arrow(edge, false)),
52
+ '```',
53
+ ].join('\n');
54
+ }
55
+ export function renderDiffParts(delta, options = {}) {
56
+ const cell = (notePath) => pathCell(notePath, options);
57
+ const parts = [
58
+ {
59
+ kind: 'markdown',
60
+ text: ['# Knowledge changes', '', `- Base commit: ${code(delta.base)}`, `- Head commit: ${code(delta.head)}`].join('\n'),
61
+ },
62
+ ];
63
+ if (isEmptyDelta(delta)) {
64
+ parts.push({ kind: 'markdown', text: 'No knowledge changes: this range changes no note.' });
65
+ return parts;
66
+ }
67
+ const c = delta.counts;
68
+ parts.push({
69
+ kind: 'markdown',
70
+ text: table(['Kind', 'Count'], [
71
+ ['Notes added', String(c.notes_added)],
72
+ ['Notes removed', String(c.notes_removed)],
73
+ ['Notes modified', String(c.notes_modified)],
74
+ ['Notes moved', String(c.notes_moved)],
75
+ ['Notes archived', String(c.notes_archived)],
76
+ ['Links added', String(c.links_added)],
77
+ ['Links removed', String(c.links_removed)],
78
+ ['Dangling links introduced', String(c.dangling_introduced)],
79
+ ['Dangling links resolved', String(c.dangling_resolved)],
80
+ ['Notes whose inbound links changed', String(c.inbound_changed)],
81
+ ['Rollups touched', String(c.rollups_touched)],
82
+ ['Notes citing new sources', String(c.sources_cited)],
83
+ ]),
84
+ });
85
+ const group = (summary, count, text) => {
86
+ if (count > 0)
87
+ parts.push({ kind: 'details', summary: `${summary} (${count})`, text });
88
+ };
89
+ const edgeRows = (edges) => edges.map((e) => [cell(e.src), cell(e.dst), code(e.type)]);
90
+ const danglingRows = (links) => links.map((d) => [cell(d.from), code(d.target), REASON_TEXT[d.reason] ?? d.reason]);
91
+ group('Notes added', c.notes_added, table(['Note'], delta.notes.added.map((p) => [cell(p)])));
92
+ group('Notes removed', c.notes_removed, table(['Note'], delta.notes.removed.map((p) => [code(p)])));
93
+ group('Notes modified', c.notes_modified, table(['Note'], delta.notes.modified.map((p) => [cell(p)])));
94
+ group('Notes moved', c.notes_moved, table(['From', 'To'], delta.notes.moved.map((r) => [code(r.from), cell(r.to)])));
95
+ group('Notes archived', c.notes_archived, table(['From', 'To'], delta.notes.archived.map((r) => [code(r.from), cell(r.to)])));
96
+ group('Links added', c.links_added, table(['From', 'To', 'Type'], edgeRows(delta.links.added)));
97
+ group('Links removed', c.links_removed, table(['From', 'To', 'Type'], edgeRows(delta.links.removed)));
98
+ group('Dangling links introduced', c.dangling_introduced, table(['From', 'Target', 'Reason'], danglingRows(delta.dangling.introduced)));
99
+ group('Dangling links resolved', c.dangling_resolved, table(['From', 'Target', 'Reason'], danglingRows(delta.dangling.resolved)));
100
+ group('Notes whose inbound links changed', c.inbound_changed, table(['Note', 'Gained from', 'Lost from', 'Has rollup'], delta.inbound.map((i) => [cell(i.path), i.gained.map(code).join(', '), i.lost.map(code).join(', '), i.rollup ? 'yes' : 'no'])));
101
+ group('Rollups touched', c.rollups_touched, table(['Note', 'Region'], delta.rollups.map((r) => [cell(r.path), r.change])));
102
+ group('Sources newly cited', c.sources_cited, table(['Note', 'Sources'], delta.sources.map((s) => [cell(s.path), s.cited.map(code).join(', ')])));
103
+ if (options.diagram) {
104
+ const diagram = renderDiagram(delta.links.added, delta.links.removed);
105
+ if (diagram !== null)
106
+ parts.push({ kind: 'markdown', text: ['## Link delta', '', diagram].join('\n') });
107
+ }
108
+ return parts;
109
+ }
110
+ /** The parts as one markdown document, `<details>` groups written as literal tags. */
111
+ export function toMarkdown(parts) {
112
+ return parts
113
+ .map((part) => part.kind === 'markdown' ? part.text : ['<details>', `<summary>${part.summary}</summary>`, '', part.text, '', '</details>'].join('\n'))
114
+ .join('\n\n');
115
+ }
116
+ export function renderDiffDocument(delta, options = {}) {
117
+ return `${toMarkdown(renderDiffParts(delta, options))}\n`;
118
+ }
119
+ //# sourceMappingURL=diff-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-document.js","sourceRoot":"","sources":["../../../src/core/graph/diff-document.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ import type { StoreConfig } from '../../config/schema.js';
2
+ import type { GitRunner } from '../git/exec.js';
3
+ import { type DanglingLink, type GraphBuildResult, type GraphEdge } from './model.js';
4
+ import { type CommitNote } from './tree.js';
5
+ /**
6
+ * review-the-knowledge-diff: how the store's knowledge changed between two
7
+ * commits — facts only, with no severity, score or ranking (D10). Every list
8
+ * is sorted by path and then by its remaining fields, and nothing here reads
9
+ * the clock, so the same two commits and configuration give the same value.
10
+ */
11
+ export interface NoteRename {
12
+ from: string;
13
+ to: string;
14
+ }
15
+ export interface InboundChange {
16
+ path: string;
17
+ gained: string[];
18
+ lost: string[];
19
+ /** Whether the note carries a rollup region on the head side. */
20
+ rollup: boolean;
21
+ }
22
+ export type RollupChange = 'added' | 'removed' | 'changed';
23
+ export interface RollupTouched {
24
+ path: string;
25
+ change: RollupChange;
26
+ }
27
+ export interface SourcesCited {
28
+ path: string;
29
+ cited: string[];
30
+ }
31
+ export interface GraphDeltaCounts {
32
+ notes_added: number;
33
+ notes_removed: number;
34
+ notes_modified: number;
35
+ notes_moved: number;
36
+ notes_archived: number;
37
+ links_added: number;
38
+ links_removed: number;
39
+ dangling_introduced: number;
40
+ dangling_resolved: number;
41
+ inbound_changed: number;
42
+ rollups_touched: number;
43
+ sources_cited: number;
44
+ }
45
+ export interface GraphDelta {
46
+ /** The full id of the base commit: the merge base of `--since` and the head. */
47
+ base: string;
48
+ /** The full id of the head commit. */
49
+ head: string;
50
+ counts: GraphDeltaCounts;
51
+ notes: {
52
+ added: string[];
53
+ removed: string[];
54
+ modified: string[];
55
+ moved: NoteRename[];
56
+ archived: NoteRename[];
57
+ };
58
+ links: {
59
+ added: GraphEdge[];
60
+ removed: GraphEdge[];
61
+ };
62
+ dangling: {
63
+ introduced: DanglingLink[];
64
+ resolved: DanglingLink[];
65
+ };
66
+ inbound: InboundChange[];
67
+ rollups: RollupTouched[];
68
+ sources: SourcesCited[];
69
+ }
70
+ /** One side of the comparison: a commit, the notes it holds, and the graph built from them. */
71
+ export interface GraphSide {
72
+ commit: string;
73
+ notes: readonly CommitNote[];
74
+ graph: GraphBuildResult;
75
+ }
76
+ /** A rename git's rename detection reported between the two commits, store-relative. */
77
+ export type RenameTable = readonly NoteRename[];
78
+ export declare function isEmptyDelta(delta: GraphDelta): boolean;
79
+ /**
80
+ * review-the-knowledge-diff (D3): the delta between two built sides. Every
81
+ * base-side path is carried through the moves and archives first, so a note
82
+ * that moved with its links still resolving reports no link change, and edges
83
+ * compare as distinct `(src, dst, type)` triples, so a link written twice
84
+ * counts once.
85
+ *
86
+ * A rename into `organize.archive_destination` from outside it is an archive,
87
+ * whether or not the destination is admitted; any other rename between two
88
+ * admitted paths is a move. A rename that leaves the admitted set is a removal
89
+ * and one that enters it is an addition, which is what each side shows.
90
+ */
91
+ export declare function diffGraphs(base: GraphSide, head: GraphSide, renames: RenameTable, config: StoreConfig): GraphDelta;
92
+ /** The full id of the commit `ref` names, or the error naming it. */
93
+ export declare function resolveCommit(git: GitRunner, cwd: string, ref: string, flag: '--since' | '--until'): Promise<string>;
94
+ /**
95
+ * review-the-knowledge-diff (D1): the head is `until` (HEAD when absent) and
96
+ * the base is the merge base of `since` and the head — the `<ref>...HEAD`
97
+ * meaning `publish urls --since` uses, so both pull-request sections describe
98
+ * one change set. A base id and a head id that descends from it resolve back
99
+ * to themselves, which is what makes the reported range re-runnable.
100
+ */
101
+ export declare function resolveRange(git: GitRunner, cwd: string, since: string, until: string | undefined): Promise<{
102
+ base: string;
103
+ head: string;
104
+ }>;
105
+ /**
106
+ * review-the-knowledge-diff (D3): git's renames between the two commits, with
107
+ * every knob that could change them pinned on the command line, so a user's
108
+ * `diff.renames`, `diff.renameLimit`, external diff driver or textconv filter
109
+ * cannot change the report. `--relative` makes the paths store-relative for a
110
+ * store rooted below the repository's top level.
111
+ */
112
+ export declare function readRenames(git: GitRunner, storeRoot: string, base: string, head: string): Promise<NoteRename[]>;
113
+ /**
114
+ * Both sides read from git's object store under the CURRENT configuration
115
+ * (D2), so a path that configuration excludes appears on neither side. No
116
+ * checkout, no stash, and nothing written — the per-checkout graph artifact is
117
+ * neither read nor rebuilt.
118
+ */
119
+ export declare function computeGraphDelta(git: GitRunner, storeRoot: string, range: {
120
+ base: string;
121
+ head: string;
122
+ }, config: StoreConfig): Promise<GraphDelta>;
123
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/core/graph/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,EAA0C,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAC9H,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,UAAU,EAAE,CAAC;QAAC,QAAQ,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IAC/G,KAAK,EAAE;QAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QAAC,OAAO,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;IACpD,QAAQ,EAAE;QAAE,UAAU,EAAE,YAAY,EAAE,CAAC;QAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;IACnE,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,wFAAwF;AACxF,MAAM,MAAM,WAAW,GAAG,SAAS,UAAU,EAAE,CAAC;AAEhD,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAEvD;AAiDD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,UAAU,CAsHlH;AAED,qEAAqE;AACrE,wBAAsB,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1H;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAMzC;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAuBtH;AAOD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACrC,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,UAAU,CAAC,CAOrB"}
@@ -0,0 +1,253 @@
1
+ import { GraphDiffNoMergeBaseError, GraphDiffRefNotFoundError } from '../errors.js';
2
+ import { isStrictlyUnderPrefix, isUnderPrefix } from '../fs/prefix.js';
3
+ import { readFencedRegion } from '../fs/fenced-region.js';
4
+ import { SOURCES_FIELD } from '../ingest/identity.js';
5
+ import { hasRollupSection, ROLLUP_FENCE } from '../rollup.js';
6
+ import { buildGraphFromNotes, graphBuildOptions } from './model.js';
7
+ import { listNotesAtCommit } from './tree.js';
8
+ export function isEmptyDelta(delta) {
9
+ return Object.values(delta.counts).every((count) => count === 0);
10
+ }
11
+ function compareStrings(a, b) {
12
+ return a < b ? -1 : a > b ? 1 : 0;
13
+ }
14
+ function compareTuples(a, b) {
15
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
16
+ const order = compareStrings(a[i], b[i]);
17
+ if (order !== 0)
18
+ return order;
19
+ }
20
+ return a.length - b.length;
21
+ }
22
+ function sortBy(items, key) {
23
+ return items.sort((a, b) => compareTuples(key(a), key(b)));
24
+ }
25
+ const edgeKey = (edge) => [edge.src, edge.dst, edge.type].join('\0');
26
+ const danglingKey = (link) => [link.from, link.target, link.reason].join('\0');
27
+ /** The rollup region's text, or null when the note has none. A malformed fence reads as its whole body, so any edit to it still shows. */
28
+ function rollupRegion(note) {
29
+ if (note === undefined || !hasRollupSection(note))
30
+ return null;
31
+ try {
32
+ return readFencedRegion(note.body, ROLLUP_FENCE).join('\n');
33
+ }
34
+ catch {
35
+ return note.body;
36
+ }
37
+ }
38
+ /** The provenance field's entries; a lone string is one entry, anything else none. */
39
+ function sourcesOf(note) {
40
+ const value = note?.frontmatter?.[SOURCES_FIELD];
41
+ if (Array.isArray(value))
42
+ return value.map((entry) => String(entry));
43
+ if (typeof value === 'string')
44
+ return [value];
45
+ return [];
46
+ }
47
+ function inboundOf(edges) {
48
+ const inbound = new Map();
49
+ for (const edge of edges) {
50
+ const sources = inbound.get(edge.dst) ?? new Set();
51
+ sources.add(edge.src);
52
+ inbound.set(edge.dst, sources);
53
+ }
54
+ return inbound;
55
+ }
56
+ /**
57
+ * review-the-knowledge-diff (D3): the delta between two built sides. Every
58
+ * base-side path is carried through the moves and archives first, so a note
59
+ * that moved with its links still resolving reports no link change, and edges
60
+ * compare as distinct `(src, dst, type)` triples, so a link written twice
61
+ * counts once.
62
+ *
63
+ * A rename into `organize.archive_destination` from outside it is an archive,
64
+ * whether or not the destination is admitted; any other rename between two
65
+ * admitted paths is a move. A rename that leaves the admitted set is a removal
66
+ * and one that enters it is an addition, which is what each side shows.
67
+ */
68
+ export function diffGraphs(base, head, renames, config) {
69
+ const baseNotes = new Map(base.notes.map((note) => [note.path, note]));
70
+ const headNotes = new Map(head.notes.map((note) => [note.path, note]));
71
+ const destination = config.organize.archive_destination;
72
+ const moved = [];
73
+ const archived = [];
74
+ for (const rename of renames) {
75
+ if (!baseNotes.has(rename.from))
76
+ continue;
77
+ if (isStrictlyUnderPrefix(rename.to, destination) && !isUnderPrefix(rename.from, destination))
78
+ archived.push(rename);
79
+ else if (headNotes.has(rename.to))
80
+ moved.push(rename);
81
+ }
82
+ const forward = new Map([...moved, ...archived].map((rename) => [rename.from, rename.to]));
83
+ const backward = new Map([...moved, ...archived].map((rename) => [rename.to, rename.from]));
84
+ const mapPath = (basePath) => forward.get(basePath) ?? basePath;
85
+ const baseOf = (headPath) => baseNotes.get(backward.get(headPath) ?? headPath);
86
+ const mappedBasePaths = new Set(base.notes.map((note) => mapPath(note.path)));
87
+ const added = head.notes.map((note) => note.path).filter((notePath) => !mappedBasePaths.has(notePath));
88
+ const removed = base.notes
89
+ .map((note) => note.path)
90
+ .filter((notePath) => !forward.has(notePath) && !headNotes.has(notePath));
91
+ const modified = head.notes
92
+ .filter((note) => mappedBasePaths.has(note.path) && baseOf(note.path).blob !== note.blob)
93
+ .map((note) => note.path);
94
+ const baseEdges = new Map();
95
+ for (const edge of base.graph.edges) {
96
+ const mapped = { src: mapPath(edge.src), dst: mapPath(edge.dst), type: edge.type };
97
+ baseEdges.set(edgeKey(mapped), mapped);
98
+ }
99
+ const headEdges = new Map(head.graph.edges.map((edge) => [edgeKey(edge), { ...edge }]));
100
+ const linksAdded = [...headEdges].filter(([key]) => !baseEdges.has(key)).map(([, edge]) => edge);
101
+ const linksRemoved = [...baseEdges].filter(([key]) => !headEdges.has(key)).map(([, edge]) => edge);
102
+ const baseDangling = new Map(base.graph.dangling.map((link) => {
103
+ const mapped = { ...link, from: mapPath(link.from) };
104
+ return [danglingKey(mapped), mapped];
105
+ }));
106
+ const headDangling = new Map(head.graph.dangling.map((link) => [danglingKey(link), { ...link }]));
107
+ const introduced = [...headDangling].filter(([key]) => !baseDangling.has(key)).map(([, link]) => link);
108
+ // A dangling link that left with its note was removed, not resolved: only a source still present can resolve one.
109
+ const resolved = [...baseDangling]
110
+ .filter(([key, link]) => !headDangling.has(key) && headNotes.has(link.from))
111
+ .map(([, link]) => link);
112
+ const baseInbound = inboundOf(baseEdges.values());
113
+ const headInbound = inboundOf(headEdges.values());
114
+ const inbound = [];
115
+ for (const target of new Set([...baseInbound.keys(), ...headInbound.keys()])) {
116
+ const before = baseInbound.get(target) ?? new Set();
117
+ const after = headInbound.get(target) ?? new Set();
118
+ const gained = [...after].filter((source) => !before.has(source)).sort(compareStrings);
119
+ const lost = [...before].filter((source) => !after.has(source)).sort(compareStrings);
120
+ if (gained.length === 0 && lost.length === 0)
121
+ continue;
122
+ const headNote = headNotes.get(target);
123
+ inbound.push({ path: target, gained, lost, rollup: headNote !== undefined && hasRollupSection(headNote) });
124
+ }
125
+ const rollups = [];
126
+ for (const note of head.notes) {
127
+ const before = rollupRegion(baseOf(note.path));
128
+ const after = rollupRegion(note);
129
+ if (before === after)
130
+ continue;
131
+ rollups.push({ path: note.path, change: before === null ? 'added' : after === null ? 'removed' : 'changed' });
132
+ }
133
+ for (const note of base.notes) {
134
+ if (headNotes.has(mapPath(note.path)) || rollupRegion(note) === null)
135
+ continue;
136
+ rollups.push({ path: note.path, change: 'removed' });
137
+ }
138
+ const sources = [];
139
+ for (const notePath of [...added, ...modified]) {
140
+ const before = new Set(sourcesOf(baseOf(notePath)));
141
+ const cited = [...new Set(sourcesOf(headNotes.get(notePath)))].filter((entry) => !before.has(entry)).sort(compareStrings);
142
+ if (cited.length > 0)
143
+ sources.push({ path: notePath, cited });
144
+ }
145
+ const delta = {
146
+ base: base.commit,
147
+ head: head.commit,
148
+ counts: {
149
+ notes_added: added.length,
150
+ notes_removed: removed.length,
151
+ notes_modified: modified.length,
152
+ notes_moved: moved.length,
153
+ notes_archived: archived.length,
154
+ links_added: linksAdded.length,
155
+ links_removed: linksRemoved.length,
156
+ dangling_introduced: introduced.length,
157
+ dangling_resolved: resolved.length,
158
+ inbound_changed: inbound.length,
159
+ rollups_touched: rollups.length,
160
+ sources_cited: sources.length,
161
+ },
162
+ notes: {
163
+ added: added.sort(compareStrings),
164
+ removed: removed.sort(compareStrings),
165
+ modified: modified.sort(compareStrings),
166
+ moved: sortBy(moved, (r) => [r.from, r.to]),
167
+ archived: sortBy(archived, (r) => [r.from, r.to]),
168
+ },
169
+ links: {
170
+ added: sortBy(linksAdded, (e) => [e.src, e.dst, e.type]),
171
+ removed: sortBy(linksRemoved, (e) => [e.src, e.dst, e.type]),
172
+ },
173
+ dangling: {
174
+ introduced: sortBy(introduced, (d) => [d.from, d.target, d.reason]),
175
+ resolved: sortBy(resolved, (d) => [d.from, d.target, d.reason]),
176
+ },
177
+ inbound: sortBy(inbound, (i) => [i.path]),
178
+ rollups: sortBy(rollups, (r) => [r.path, r.change]),
179
+ sources: sortBy(sources, (s) => [s.path]),
180
+ };
181
+ return delta;
182
+ }
183
+ /** The full id of the commit `ref` names, or the error naming it. */
184
+ export async function resolveCommit(git, cwd, ref, flag) {
185
+ const result = await git.run(['rev-parse', '--verify', '--quiet', '--end-of-options', `${ref}^{commit}`], { cwd, allowFailure: true });
186
+ if (result.exitCode !== 0)
187
+ throw new GraphDiffRefNotFoundError(flag, ref);
188
+ return result.stdout.trim();
189
+ }
190
+ /**
191
+ * review-the-knowledge-diff (D1): the head is `until` (HEAD when absent) and
192
+ * the base is the merge base of `since` and the head — the `<ref>...HEAD`
193
+ * meaning `publish urls --since` uses, so both pull-request sections describe
194
+ * one change set. A base id and a head id that descends from it resolve back
195
+ * to themselves, which is what makes the reported range re-runnable.
196
+ */
197
+ export async function resolveRange(git, cwd, since, until) {
198
+ const sinceCommit = await resolveCommit(git, cwd, since, '--since');
199
+ const head = await resolveCommit(git, cwd, until ?? 'HEAD', '--until');
200
+ const result = await git.run(['merge-base', sinceCommit, head], { cwd, allowFailure: true });
201
+ if (result.exitCode !== 0)
202
+ throw new GraphDiffNoMergeBaseError(since, until ?? 'HEAD');
203
+ return { base: result.stdout.trim(), head };
204
+ }
205
+ /**
206
+ * review-the-knowledge-diff (D3): git's renames between the two commits, with
207
+ * every knob that could change them pinned on the command line, so a user's
208
+ * `diff.renames`, `diff.renameLimit`, external diff driver or textconv filter
209
+ * cannot change the report. `--relative` makes the paths store-relative for a
210
+ * store rooted below the repository's top level.
211
+ */
212
+ export async function readRenames(git, storeRoot, base, head) {
213
+ const { stdout } = await git.run([
214
+ '-c', 'diff.renameLimit=0',
215
+ '-c', 'diff.renames=true',
216
+ 'diff', '--name-status', '-z', '--relative', '-M50%', '--no-ext-diff', '--no-textconv',
217
+ base, head, '--',
218
+ ], { cwd: storeRoot });
219
+ const fields = stdout.split('\0');
220
+ const renames = [];
221
+ for (let i = 0; i < fields.length; i++) {
222
+ const code = fields[i];
223
+ if (code === '')
224
+ continue;
225
+ if (code.startsWith('R') || code.startsWith('C')) {
226
+ if (code.startsWith('R'))
227
+ renames.push({ from: fields[i + 1], to: fields[i + 2] });
228
+ i += 2;
229
+ continue;
230
+ }
231
+ i += 1;
232
+ }
233
+ return renames;
234
+ }
235
+ async function readSide(git, storeRoot, commit, config) {
236
+ const notes = await listNotesAtCommit(git, storeRoot, commit, config);
237
+ return { commit, notes, graph: buildGraphFromNotes(notes, graphBuildOptions(config)) };
238
+ }
239
+ /**
240
+ * Both sides read from git's object store under the CURRENT configuration
241
+ * (D2), so a path that configuration excludes appears on neither side. No
242
+ * checkout, no stash, and nothing written — the per-checkout graph artifact is
243
+ * neither read nor rebuilt.
244
+ */
245
+ export async function computeGraphDelta(git, storeRoot, range, config) {
246
+ const [base, head, renames] = await Promise.all([
247
+ readSide(git, storeRoot, range.base, config),
248
+ readSide(git, storeRoot, range.head, config),
249
+ readRenames(git, storeRoot, range.base, range.head),
250
+ ]);
251
+ return diffGraphs(base, head, renames, config);
252
+ }
253
+ //# sourceMappingURL=diff.js.map