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.
@@ -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":"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"}
@@ -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":"AAaA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAE/D,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,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;CAClB;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC,GAAG;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtD;AAiBD,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CA2BtC;AAsDD,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,KAAK,CAAC,CAAC,CAoBxC"}
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"}