daftari 1.13.1 → 1.15.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/CHANGELOG.md +92 -0
- package/README.md +1 -0
- package/dist/curation/lint.d.ts +26 -1
- package/dist/curation/lint.d.ts.map +1 -1
- package/dist/curation/lint.js +129 -74
- package/dist/curation/lint.js.map +1 -1
- package/dist/curation/tension-blast.d.ts +37 -0
- package/dist/curation/tension-blast.d.ts.map +1 -0
- package/dist/curation/tension-blast.js +223 -0
- package/dist/curation/tension-blast.js.map +1 -0
- package/dist/curation/tension-clusters.d.ts +18 -0
- package/dist/curation/tension-clusters.d.ts.map +1 -0
- package/dist/curation/tension-clusters.js +157 -0
- package/dist/curation/tension-clusters.js.map +1 -0
- package/dist/curation/tension.d.ts +25 -1
- package/dist/curation/tension.d.ts.map +1 -1
- package/dist/curation/tension.js +258 -15
- package/dist/curation/tension.js.map +1 -1
- package/dist/curation/vault-docs.d.ts +14 -0
- package/dist/curation/vault-docs.d.ts.map +1 -0
- package/dist/curation/vault-docs.js +90 -0
- package/dist/curation/vault-docs.js.map +1 -0
- package/dist/tools/curation.d.ts +7 -1
- package/dist/tools/curation.d.ts.map +1 -1
- package/dist/tools/curation.js +238 -10
- package/dist/tools/curation.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +8 -4
- package/dist/tools/search.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Tension blast radius — Phase 3 of the tension graph plan (2026-05-31).
|
|
2
|
+
//
|
|
3
|
+
// "Blast" is the transitive closure of downstream documents that cite or
|
|
4
|
+
// depend on a contested node. Given either a single document or a tension
|
|
5
|
+
// cluster, this surface walks the dependency graph and reports the docs
|
|
6
|
+
// exposed to that contested state.
|
|
7
|
+
//
|
|
8
|
+
// Two edge types (spec, Gap 5):
|
|
9
|
+
// - frontmatter `sources` array → "source" edges (primary, high-confidence).
|
|
10
|
+
// - in-vault markdown links → "link" edges (advisory).
|
|
11
|
+
//
|
|
12
|
+
// `superseded_by` is NOT a blast edge. A doc that supersedes a contested doc
|
|
13
|
+
// is the *replacement*, not an inheritor — walking the blast through that
|
|
14
|
+
// relationship would falsely contaminate the resolution path. The existing
|
|
15
|
+
// `deprecated-still-linked` lint check covers the related-but-different
|
|
16
|
+
// question of deprecated docs still being cited.
|
|
17
|
+
//
|
|
18
|
+
// Two confidence channels (spec, Gap 6):
|
|
19
|
+
// - `primary_blast`: count of downstream docs that have at least one
|
|
20
|
+
// incoming source edge from a visited node (a contested member or a doc
|
|
21
|
+
// already in the downstream set).
|
|
22
|
+
// - `advisory_blast`: count of downstream docs reached only via link edges.
|
|
23
|
+
//
|
|
24
|
+
// A doc reachable via both edge types counts as primary — higher-confidence
|
|
25
|
+
// wins. The per-immediate-edge rule matches the spec's "reached via both edge
|
|
26
|
+
// types" wording: from the downstream doc's perspective, does any visited
|
|
27
|
+
// predecessor cite it via the `sources` frontmatter array?
|
|
28
|
+
//
|
|
29
|
+
// Two input modes (spec, Gap 7):
|
|
30
|
+
// - document mode: blast for a single doc. Response also identifies the
|
|
31
|
+
// containing cluster (if any) so the agent sees the broader region.
|
|
32
|
+
// - cluster mode: blast for the union of all cluster members. Seeds are
|
|
33
|
+
// never reported as downstream of themselves.
|
|
34
|
+
//
|
|
35
|
+
// Computation is on-demand — we build the reverse-source and reverse-link
|
|
36
|
+
// maps at tool-call time from the loaded docs. There is no maintained graph.
|
|
37
|
+
// Cycle protection is implicit: BFS marks nodes visited; a node is never
|
|
38
|
+
// re-queued, so a sources b sources a terminates after one round trip.
|
|
39
|
+
import { err, ok } from "../frontmatter/types.js";
|
|
40
|
+
import { loadTensionClusters } from "./tension-clusters.js";
|
|
41
|
+
import { buildPathIndexes, extractLinks, loadDocuments, resolveLink, } from "./vault-docs.js";
|
|
42
|
+
// Reverse-source map: target doc → docs whose frontmatter `sources` cites
|
|
43
|
+
// the target. Source paths are resolved through the same path-resolution
|
|
44
|
+
// rules as in-vault links (exact match, .md suffix, relative-to-from,
|
|
45
|
+
// basename), so author-written shorthand paths like `pricing/foo` line up
|
|
46
|
+
// with the canonical `pricing/foo.md`. Unresolved sources (typos, links to
|
|
47
|
+
// non-vault material) and self-citations are dropped.
|
|
48
|
+
export function buildReverseSourceMap(docs) {
|
|
49
|
+
const reverse = new Map();
|
|
50
|
+
const { byPath, byBasename } = buildPathIndexes(docs);
|
|
51
|
+
for (const d of docs) {
|
|
52
|
+
for (const raw of d.frontmatter.sources ?? []) {
|
|
53
|
+
const target = resolveLink(raw, d.path, byPath, byBasename);
|
|
54
|
+
if (!target || target === d.path)
|
|
55
|
+
continue;
|
|
56
|
+
if (!reverse.has(target))
|
|
57
|
+
reverse.set(target, new Set());
|
|
58
|
+
reverse.get(target).add(d.path);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return reverse;
|
|
62
|
+
}
|
|
63
|
+
// Reverse-link map: target doc → docs whose body contains an in-vault
|
|
64
|
+
// markdown or wikilink to the target. Mirrors lint's inbound-link map but
|
|
65
|
+
// exposed as its own helper so blast and lint can't drift.
|
|
66
|
+
export function buildReverseLinkMap(docs) {
|
|
67
|
+
const reverse = new Map();
|
|
68
|
+
const { byPath, byBasename } = buildPathIndexes(docs);
|
|
69
|
+
for (const d of docs) {
|
|
70
|
+
for (const raw of extractLinks(d.content)) {
|
|
71
|
+
const target = resolveLink(raw, d.path, byPath, byBasename);
|
|
72
|
+
if (!target || target === d.path)
|
|
73
|
+
continue;
|
|
74
|
+
if (!reverse.has(target))
|
|
75
|
+
reverse.set(target, new Set());
|
|
76
|
+
reverse.get(target).add(d.path);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return reverse;
|
|
80
|
+
}
|
|
81
|
+
// Pure BFS over the combined edge set. Returns the sorted downstream list,
|
|
82
|
+
// the two channel counts, and the max depth observed.
|
|
83
|
+
//
|
|
84
|
+
// Layered BFS guarantees `distance` is the minimum hop count from any seed;
|
|
85
|
+
// the `visited` set is the cycle guard, so a doc is never re-queued.
|
|
86
|
+
//
|
|
87
|
+
// Channel assignment is per-immediate-edge: a downstream doc D is "source"
|
|
88
|
+
// iff some visited node V (a seed or another downstream doc) has D in its
|
|
89
|
+
// reverse-source successor list, i.e. D's frontmatter cites V. Otherwise D
|
|
90
|
+
// is "link". This matches the spec's "reached via both edge types counts as
|
|
91
|
+
// primary" wording while staying decidable from the dependency graph alone.
|
|
92
|
+
export function computeBlast(args) {
|
|
93
|
+
const { seeds, reverseSource, reverseLink } = args;
|
|
94
|
+
const seedSet = new Set(seeds);
|
|
95
|
+
const visited = new Set(seeds);
|
|
96
|
+
const distance = new Map();
|
|
97
|
+
let frontier = new Set(seeds);
|
|
98
|
+
let depth = 0;
|
|
99
|
+
while (frontier.size > 0) {
|
|
100
|
+
const next = new Set();
|
|
101
|
+
for (const node of frontier) {
|
|
102
|
+
const successors = new Set();
|
|
103
|
+
for (const s of reverseSource.get(node) ?? [])
|
|
104
|
+
successors.add(s);
|
|
105
|
+
for (const s of reverseLink.get(node) ?? [])
|
|
106
|
+
successors.add(s);
|
|
107
|
+
for (const succ of successors) {
|
|
108
|
+
if (visited.has(succ))
|
|
109
|
+
continue;
|
|
110
|
+
visited.add(succ);
|
|
111
|
+
distance.set(succ, depth + 1);
|
|
112
|
+
next.add(succ);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
frontier = next;
|
|
116
|
+
depth += 1;
|
|
117
|
+
}
|
|
118
|
+
// primaryDownstream: every doc that has at least one incoming source edge
|
|
119
|
+
// from a visited node. Built by iterating each visited node's
|
|
120
|
+
// reverse-source successors; we exclude seeds because the response only
|
|
121
|
+
// surfaces downstream-of-the-contested-set, never the contested set
|
|
122
|
+
// itself.
|
|
123
|
+
const primaryDownstream = new Set();
|
|
124
|
+
for (const v of visited) {
|
|
125
|
+
for (const child of reverseSource.get(v) ?? []) {
|
|
126
|
+
if (seedSet.has(child))
|
|
127
|
+
continue;
|
|
128
|
+
primaryDownstream.add(child);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const entries = [];
|
|
132
|
+
for (const [path, dist] of distance) {
|
|
133
|
+
const dependency_type = primaryDownstream.has(path) ? "source" : "link";
|
|
134
|
+
entries.push({ path, dependency_type, distance: dist });
|
|
135
|
+
}
|
|
136
|
+
entries.sort((a, b) => {
|
|
137
|
+
if (a.distance !== b.distance)
|
|
138
|
+
return a.distance - b.distance;
|
|
139
|
+
if (a.dependency_type !== b.dependency_type) {
|
|
140
|
+
return a.dependency_type === "source" ? -1 : 1;
|
|
141
|
+
}
|
|
142
|
+
return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
143
|
+
});
|
|
144
|
+
let primary_blast = 0;
|
|
145
|
+
let advisory_blast = 0;
|
|
146
|
+
let max_depth = 0;
|
|
147
|
+
for (const e of entries) {
|
|
148
|
+
if (e.dependency_type === "source")
|
|
149
|
+
primary_blast += 1;
|
|
150
|
+
else
|
|
151
|
+
advisory_blast += 1;
|
|
152
|
+
if (e.distance > max_depth)
|
|
153
|
+
max_depth = e.distance;
|
|
154
|
+
}
|
|
155
|
+
return { downstream: entries, primary_blast, advisory_blast, max_depth };
|
|
156
|
+
}
|
|
157
|
+
// Orchestration: validate the exactly-one-of input, load the docs and
|
|
158
|
+
// clusters, resolve seeds, build the reverse maps, run the BFS, and assemble
|
|
159
|
+
// the response. Returns a Result — tool handlers never throw.
|
|
160
|
+
export async function computeTensionBlast(vaultRoot, input) {
|
|
161
|
+
const hasDoc = typeof input.document === "string" && input.document.length > 0;
|
|
162
|
+
const hasCluster = typeof input.cluster_id === "string" && input.cluster_id.length > 0;
|
|
163
|
+
if (hasDoc && hasCluster) {
|
|
164
|
+
return err(new Error("vault_tension_blast accepts exactly one of 'document' or 'cluster_id', not both"));
|
|
165
|
+
}
|
|
166
|
+
if (!hasDoc && !hasCluster) {
|
|
167
|
+
return err(new Error("vault_tension_blast requires exactly one of 'document' or 'cluster_id'"));
|
|
168
|
+
}
|
|
169
|
+
const docsResult = await loadDocuments(vaultRoot);
|
|
170
|
+
if (!docsResult.ok)
|
|
171
|
+
return docsResult;
|
|
172
|
+
const docs = docsResult.value;
|
|
173
|
+
const knownPaths = new Set(docs.map((d) => d.path));
|
|
174
|
+
const clustersResult = await loadTensionClusters(vaultRoot);
|
|
175
|
+
if (!clustersResult.ok)
|
|
176
|
+
return clustersResult;
|
|
177
|
+
const clusters = clustersResult.value.clusters;
|
|
178
|
+
let seeds;
|
|
179
|
+
let contested_document;
|
|
180
|
+
let cluster_id = null;
|
|
181
|
+
let cluster_documents = [];
|
|
182
|
+
if (hasDoc) {
|
|
183
|
+
const doc = input.document;
|
|
184
|
+
if (!knownPaths.has(doc)) {
|
|
185
|
+
return err(new Error(`vault_tension_blast: document not found in vault: ${doc}`));
|
|
186
|
+
}
|
|
187
|
+
contested_document = doc;
|
|
188
|
+
const containing = clusters.find((c) => c.documents.includes(doc));
|
|
189
|
+
if (containing) {
|
|
190
|
+
cluster_id = containing.id;
|
|
191
|
+
cluster_documents = [...containing.documents];
|
|
192
|
+
}
|
|
193
|
+
seeds = [doc];
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const id = input.cluster_id;
|
|
197
|
+
const found = clusters.find((c) => c.id === id);
|
|
198
|
+
if (!found) {
|
|
199
|
+
return err(new Error(`vault_tension_blast: cluster_id not found: ${id}`));
|
|
200
|
+
}
|
|
201
|
+
contested_document = null;
|
|
202
|
+
cluster_id = found.id;
|
|
203
|
+
cluster_documents = [...found.documents];
|
|
204
|
+
seeds = [...found.documents];
|
|
205
|
+
}
|
|
206
|
+
const reverseSource = buildReverseSourceMap(docs);
|
|
207
|
+
const reverseLink = buildReverseLinkMap(docs);
|
|
208
|
+
const { downstream, primary_blast, advisory_blast, max_depth } = computeBlast({
|
|
209
|
+
seeds,
|
|
210
|
+
reverseSource,
|
|
211
|
+
reverseLink,
|
|
212
|
+
});
|
|
213
|
+
return ok({
|
|
214
|
+
contested_document,
|
|
215
|
+
cluster_id,
|
|
216
|
+
cluster_documents,
|
|
217
|
+
downstream,
|
|
218
|
+
primary_blast,
|
|
219
|
+
advisory_blast,
|
|
220
|
+
max_depth,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=tension-blast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tension-blast.js","sourceRoot":"","sources":["../../src/curation/tension-blast.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Result } from "../frontmatter/types.js";
|
|
2
|
+
import { type TensionEntry, type TensionKind } from "./tension.js";
|
|
3
|
+
export interface TensionCluster {
|
|
4
|
+
id: string;
|
|
5
|
+
size: number;
|
|
6
|
+
documents: string[];
|
|
7
|
+
tension_count: number;
|
|
8
|
+
kinds: Record<TensionKind, number>;
|
|
9
|
+
oldest_tension_age_days: number;
|
|
10
|
+
newest_tension_age_days: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TensionClustersResult {
|
|
13
|
+
cluster_count: number;
|
|
14
|
+
clusters: TensionCluster[];
|
|
15
|
+
}
|
|
16
|
+
export declare function computeTensionClusters(tensions: TensionEntry[], now?: Date): TensionClustersResult;
|
|
17
|
+
export declare function loadTensionClusters(vaultRoot: string, now?: Date): Promise<Result<TensionClustersResult, Error>>;
|
|
18
|
+
//# sourceMappingURL=tension-clusters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tension-clusters.d.ts","sourceRoot":"","sources":["../../src/curation/tension-clusters.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EAA+B,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhG,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACnC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAuDD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,YAAY,EAAE,EACxB,GAAG,GAAE,IAAiB,GACrB,qBAAqB,CAuEvB;AAID,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC,CAI/C"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Tension clusters — Phase 2 of the tension graph plan (2026-05-31).
|
|
2
|
+
//
|
|
3
|
+
// A "cluster" is a connected component of the tension graph: a maximal set of
|
|
4
|
+
// documents joined transitively by in-scope tensions. Clusters surface the
|
|
5
|
+
// regions of the vault where contradiction is composing rather than just
|
|
6
|
+
// pairwise.
|
|
7
|
+
//
|
|
8
|
+
// In-scope = "live contested." A tension contributes an edge iff:
|
|
9
|
+
// - `resolved: false`, AND
|
|
10
|
+
// - `resolution.kind !== "accepted"`
|
|
11
|
+
//
|
|
12
|
+
// The two conditions reduce to `!resolved` in the current data model
|
|
13
|
+
// (resolveTension always sets resolved: true), but the spec phrases them
|
|
14
|
+
// separately for forward-compatibility. We keep both for the same reason.
|
|
15
|
+
//
|
|
16
|
+
// Cluster IDs are content-addressed (spec, Gap 3):
|
|
17
|
+
// "cluster:" + first 8 hex chars of sha256(canonical-sorted member paths
|
|
18
|
+
// joined by "\n").
|
|
19
|
+
// Same membership → identical ID across runs. Membership change → different
|
|
20
|
+
// ID. A merged cluster's ID matches neither predecessor's, which is the
|
|
21
|
+
// correct semantic: a different membership is genuinely a different cluster.
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { err, ok } from "../frontmatter/types.js";
|
|
24
|
+
import { ageInDays } from "./staleness.js";
|
|
25
|
+
import { listTensions, TENSION_KINDS } from "./tension.js";
|
|
26
|
+
// Returns true iff the tension participates in cluster formation. Spec:
|
|
27
|
+
// "Cluster scope" — only unresolved, non-accepted tensions form edges.
|
|
28
|
+
function inScope(entry) {
|
|
29
|
+
if (entry.resolved)
|
|
30
|
+
return false;
|
|
31
|
+
if (entry.resolution?.kind === "accepted")
|
|
32
|
+
return false;
|
|
33
|
+
// Self-loops contribute nothing to clustering and would inflate
|
|
34
|
+
// tension_count for no reason; defensive filter.
|
|
35
|
+
if (entry.sourceA === entry.sourceB)
|
|
36
|
+
return false;
|
|
37
|
+
// Either endpoint missing means a malformed entry; skip rather than crash.
|
|
38
|
+
if (!entry.sourceA || !entry.sourceB)
|
|
39
|
+
return false;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// Union-find with path compression. No rank tracking — at the tension-log
|
|
43
|
+
// scale this is unnecessary and a class would violate the project style rule.
|
|
44
|
+
function ufFind(parent, x) {
|
|
45
|
+
if (!parent.has(x)) {
|
|
46
|
+
parent.set(x, x);
|
|
47
|
+
return x;
|
|
48
|
+
}
|
|
49
|
+
let root = x;
|
|
50
|
+
while (parent.get(root) !== root) {
|
|
51
|
+
root = parent.get(root);
|
|
52
|
+
}
|
|
53
|
+
// Path compression on the second pass.
|
|
54
|
+
let cur = x;
|
|
55
|
+
while (parent.get(cur) !== root) {
|
|
56
|
+
const next = parent.get(cur);
|
|
57
|
+
parent.set(cur, root);
|
|
58
|
+
cur = next;
|
|
59
|
+
}
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
function ufUnion(parent, a, b) {
|
|
63
|
+
const ra = ufFind(parent, a);
|
|
64
|
+
const rb = ufFind(parent, b);
|
|
65
|
+
if (ra !== rb)
|
|
66
|
+
parent.set(ra, rb);
|
|
67
|
+
}
|
|
68
|
+
// Content-addressed cluster id. The hash input is the canonical-sorted
|
|
69
|
+
// member list joined by newlines — the same canonicalization used to render
|
|
70
|
+
// the `documents` field, so the ID and the visible member list cannot drift
|
|
71
|
+
// out of sync.
|
|
72
|
+
function clusterIdFor(sortedMembers) {
|
|
73
|
+
const canonical = sortedMembers.join("\n");
|
|
74
|
+
const digest = createHash("sha256").update(canonical).digest("hex");
|
|
75
|
+
return `cluster:${digest.slice(0, 8)}`;
|
|
76
|
+
}
|
|
77
|
+
// Pure computation over a tension list. Synthetic-entry tests and the vault
|
|
78
|
+
// loader both go through this function so the algorithm is exercised
|
|
79
|
+
// identically in either path.
|
|
80
|
+
export function computeTensionClusters(tensions, now = new Date()) {
|
|
81
|
+
const scoped = tensions.filter(inScope);
|
|
82
|
+
if (scoped.length === 0)
|
|
83
|
+
return { cluster_count: 0, clusters: [] };
|
|
84
|
+
const parent = new Map();
|
|
85
|
+
for (const t of scoped) {
|
|
86
|
+
ufUnion(parent, t.sourceA, t.sourceB);
|
|
87
|
+
}
|
|
88
|
+
// Group every document by its component root. The root key itself is not
|
|
89
|
+
// user-visible — it's a UF artifact — so we re-key by the content-addressed
|
|
90
|
+
// cluster id once members are known.
|
|
91
|
+
const docsByRoot = new Map();
|
|
92
|
+
for (const doc of parent.keys()) {
|
|
93
|
+
const root = ufFind(parent, doc);
|
|
94
|
+
if (!docsByRoot.has(root))
|
|
95
|
+
docsByRoot.set(root, new Set());
|
|
96
|
+
docsByRoot.get(root).add(doc);
|
|
97
|
+
}
|
|
98
|
+
// For each component, collect every tension whose endpoints both fall in
|
|
99
|
+
// the component. (By construction every in-scope tension's endpoints share
|
|
100
|
+
// a root — but we re-check rather than assume, so the count is provably
|
|
101
|
+
// correct.)
|
|
102
|
+
const tensionsByRoot = new Map();
|
|
103
|
+
for (const t of scoped) {
|
|
104
|
+
const root = ufFind(parent, t.sourceA);
|
|
105
|
+
const otherRoot = ufFind(parent, t.sourceB);
|
|
106
|
+
if (root !== otherRoot)
|
|
107
|
+
continue;
|
|
108
|
+
if (!tensionsByRoot.has(root))
|
|
109
|
+
tensionsByRoot.set(root, []);
|
|
110
|
+
tensionsByRoot.get(root).push(t);
|
|
111
|
+
}
|
|
112
|
+
const clusters = [];
|
|
113
|
+
for (const [root, docSet] of docsByRoot) {
|
|
114
|
+
const members = [...docSet].sort();
|
|
115
|
+
const id = clusterIdFor(members);
|
|
116
|
+
const clusterTensions = tensionsByRoot.get(root) ?? [];
|
|
117
|
+
const kinds = Object.fromEntries(TENSION_KINDS.map((k) => [k, 0]));
|
|
118
|
+
let oldest = 0;
|
|
119
|
+
let newest = Number.POSITIVE_INFINITY;
|
|
120
|
+
for (const t of clusterTensions) {
|
|
121
|
+
kinds[t.kind] += 1;
|
|
122
|
+
const age = ageInDays(t.date, now);
|
|
123
|
+
if (age > oldest)
|
|
124
|
+
oldest = age;
|
|
125
|
+
if (age < newest)
|
|
126
|
+
newest = age;
|
|
127
|
+
}
|
|
128
|
+
if (!Number.isFinite(newest))
|
|
129
|
+
newest = 0;
|
|
130
|
+
clusters.push({
|
|
131
|
+
id,
|
|
132
|
+
size: members.length,
|
|
133
|
+
documents: members,
|
|
134
|
+
tension_count: clusterTensions.length,
|
|
135
|
+
kinds,
|
|
136
|
+
oldest_tension_age_days: oldest,
|
|
137
|
+
newest_tension_age_days: newest,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Largest cluster first; ties broken by id ASCII ascending so the order is
|
|
141
|
+
// deterministic across runs even when sizes collide.
|
|
142
|
+
clusters.sort((a, b) => {
|
|
143
|
+
if (b.size !== a.size)
|
|
144
|
+
return b.size - a.size;
|
|
145
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
146
|
+
});
|
|
147
|
+
return { cluster_count: clusters.length, clusters };
|
|
148
|
+
}
|
|
149
|
+
// Async wrapper that reads the vault's tension log and runs the computation.
|
|
150
|
+
// Mirrors the listTensions / addTension / resolveTension Result contract.
|
|
151
|
+
export async function loadTensionClusters(vaultRoot, now = new Date()) {
|
|
152
|
+
const tensions = await listTensions(vaultRoot);
|
|
153
|
+
if (!tensions.ok)
|
|
154
|
+
return err(tensions.error);
|
|
155
|
+
return ok(computeTensionClusters(tensions.value, now));
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=tension-clusters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tension-clusters.js","sourceRoot":"","sources":["../../src/curation/tension-clusters.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,yEAAyE;AACzE,YAAY;AACZ,EAAE;AACF,kEAAkE;AAClE,6BAA6B;AAC7B,uCAAuC;AACvC,EAAE;AACF,qEAAqE;AACrE,yEAAyE;AACzE,0EAA0E;AAC1E,EAAE;AACF,mDAAmD;AACnD,2EAA2E;AAC3E,qBAAqB;AACrB,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAuC,MAAM,cAAc,CAAC;AAiBhG,wEAAwE;AACxE,uEAAuE;AACvE,SAAS,OAAO,CAAC,KAAmB;IAClC,IAAI,KAAK,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACjC,IAAI,KAAK,CAAC,UAAU,EAAE,IAAI,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACxD,gEAAgE;IAChE,iDAAiD;IACjD,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAClD,2EAA2E;IAC3E,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACnD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0EAA0E;AAC1E,8EAA8E;AAC9E,SAAS,MAAM,CAAC,MAA2B,EAAE,CAAS;IACpD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACjB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QACjC,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAW,CAAC;IACpC,CAAC;IACD,uCAAuC;IACvC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAW,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACtB,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,MAA2B,EAAE,CAAS,EAAE,CAAS;IAChE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC7B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC7B,IAAI,EAAE,KAAK,EAAE;QAAE,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,uEAAuE;AACvE,4EAA4E;AAC5E,4EAA4E;AAC5E,eAAe;AACf,SAAS,YAAY,CAAC,aAAuB;IAC3C,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpE,OAAO,WAAW,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AACzC,CAAC;AAED,4EAA4E;AAC5E,qEAAqE;AACrE,8BAA8B;AAC9B,MAAM,UAAU,sBAAsB,CACpC,QAAwB,EACxB,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAEnE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,yEAAyE;IACzE,4EAA4E;IAC5E,qCAAqC;IACrC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;IAClD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QAC1D,UAAU,CAAC,GAAG,CAAC,IAAI,CAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjD,CAAC;IAED,yEAAyE;IACzE,2EAA2E;IAC3E,wEAAwE;IACxE,YAAY;IACZ,MAAM,cAAc,GAAG,IAAI,GAAG,EAA0B,CAAC;IACzD,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QACjC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3D,cAAc,CAAC,GAAG,CAAC,IAAI,CAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,QAAQ,GAAqB,EAAE,CAAC;IACtC,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QACxC,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,eAAe,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAEvD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAGhE,CAAC;QACF,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,MAAM,GAAG,MAAM,CAAC,iBAAiB,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,eAAe,EAAE,CAAC;YAChC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,MAAM,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACnC,IAAI,GAAG,GAAG,MAAM;gBAAE,MAAM,GAAG,GAAG,CAAC;YAC/B,IAAI,GAAG,GAAG,MAAM;gBAAE,MAAM,GAAG,GAAG,CAAC;QACjC,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,MAAM,GAAG,CAAC,CAAC;QAEzC,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE;YACF,IAAI,EAAE,OAAO,CAAC,MAAM;YACpB,SAAS,EAAE,OAAO;YAClB,aAAa,EAAE,eAAe,CAAC,MAAM;YACrC,KAAK;YACL,uBAAuB,EAAE,MAAM;YAC/B,uBAAuB,EAAE,MAAM;SAChC,CAAC,CAAC;IACL,CAAC;IAED,2EAA2E;IAC3E,qDAAqD;IACrD,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACrB,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;YAAE,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;QAC9C,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;AACtD,CAAC;AAED,6EAA6E;AAC7E,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,SAAiB,EACjB,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,EAAE,CAAC,sBAAsB,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { type Result } from "../frontmatter/types.js";
|
|
2
2
|
export declare const DEFAULT_TENSION_STATUS = "unresolved";
|
|
3
|
+
export declare const RESOLVED_TENSION_STATUS = "resolved";
|
|
4
|
+
export declare const TENSION_KINDS: readonly ["temporal", "factual", "interpretive", "unspecified"];
|
|
5
|
+
export type TensionKind = (typeof TENSION_KINDS)[number];
|
|
6
|
+
export declare const LOGGABLE_TENSION_KINDS: readonly ["temporal", "factual", "interpretive"];
|
|
7
|
+
export type LoggableTensionKind = (typeof LOGGABLE_TENSION_KINDS)[number];
|
|
8
|
+
export declare const RESOLUTION_KINDS: readonly ["superseded", "corrected", "accepted", "invalid"];
|
|
9
|
+
export type ResolutionKind = (typeof RESOLUTION_KINDS)[number];
|
|
10
|
+
export interface TensionResolution {
|
|
11
|
+
resolved_at: string;
|
|
12
|
+
resolved_by: string;
|
|
13
|
+
kind: ResolutionKind;
|
|
14
|
+
rationale?: string;
|
|
15
|
+
references?: string[];
|
|
16
|
+
}
|
|
3
17
|
export interface TensionEntry {
|
|
18
|
+
id?: string;
|
|
4
19
|
date: string;
|
|
5
20
|
title: string;
|
|
21
|
+
kind: TensionKind;
|
|
6
22
|
sourceA: string;
|
|
7
23
|
claimA: string;
|
|
8
24
|
sourceB: string;
|
|
9
25
|
claimB: string;
|
|
10
26
|
status: string;
|
|
11
27
|
loggedBy: string;
|
|
28
|
+
resolved: boolean;
|
|
29
|
+
resolution?: TensionResolution;
|
|
12
30
|
}
|
|
13
|
-
export type TensionInput = Omit<TensionEntry, "date" | "status"> & {
|
|
31
|
+
export type TensionInput = Omit<TensionEntry, "date" | "status" | "kind" | "id" | "resolved" | "resolution"> & {
|
|
14
32
|
date?: string;
|
|
15
33
|
status?: string;
|
|
34
|
+
kind: LoggableTensionKind;
|
|
16
35
|
};
|
|
17
36
|
export declare function tensionsPath(vaultRoot: string): string;
|
|
18
37
|
export declare function addTension(vaultRoot: string, input: TensionInput): Promise<Result<TensionEntry, Error>>;
|
|
19
38
|
export declare function listTensions(vaultRoot: string, status?: string): Promise<Result<TensionEntry[], Error>>;
|
|
39
|
+
export declare function resolveTension(vaultRoot: string, id: string, resolution: TensionResolution): Promise<Result<TensionEntry, Error>>;
|
|
40
|
+
export declare const AGING_TIERS: readonly ["fresh", "aging", "stale"];
|
|
41
|
+
export type AgingTier = (typeof AGING_TIERS)[number];
|
|
42
|
+
export declare const STALE_TIER_LINT_COPY: Record<Exclude<TensionKind, "unspecified">, string>;
|
|
43
|
+
export declare function agingTier(entry: TensionEntry, now?: Date): AgingTier | null;
|
|
20
44
|
//# sourceMappingURL=tension.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tension.d.ts","sourceRoot":"","sources":["../../src/curation/tension.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tension.d.ts","sourceRoot":"","sources":["../../src/curation/tension.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAG/D,eAAO,MAAM,sBAAsB,eAAe,CAAC;AACnD,eAAO,MAAM,uBAAuB,aAAa,CAAC;AAElD,eAAO,MAAM,aAAa,iEAAkE,CAAC;AAC7F,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAGzD,eAAO,MAAM,sBAAsB,kDAAmD,CAAC;AACvF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,eAAO,MAAM,gBAAgB,6DAA8D,CAAC;AAC5F,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAC7B,YAAY,EACZ,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,CAC9D,GAAG;IACF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,mBAAmB,CAAC;CAC3B,CAAC;AAEF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtD;AAsDD,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAyCtC;AAwGD,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,KAAK,CAAC,CAAC,CAoBxC;AAMD,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,iBAAiB,GAC5B,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAiEtC;AA8BD,eAAO,MAAM,WAAW,sCAAuC,CAAC;AAChE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAKrD,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC,EAAE,MAAM,CAOpF,CAAC;AAKF,wBAAgB,SAAS,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,GAAE,IAAiB,GAAG,SAAS,GAAG,IAAI,CAQvF"}
|