@opum-ai/lore 0.1.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/LICENSE +21 -0
- package/README.md +306 -0
- package/bin/lore.cjs +109 -0
- package/package.json +67 -0
- package/src/adapters/backlog.ts +1084 -0
- package/src/adapters/git.ts +221 -0
- package/src/cli.ts +667 -0
- package/src/commands/agent.ts +301 -0
- package/src/commands/agents.ts +302 -0
- package/src/commands/args.ts +209 -0
- package/src/commands/changed.ts +70 -0
- package/src/commands/check.ts +1031 -0
- package/src/commands/codex-bridge.ts +49 -0
- package/src/commands/concurrency.ts +48 -0
- package/src/commands/context.ts +292 -0
- package/src/commands/discover.ts +89 -0
- package/src/commands/explorer.ts +253 -0
- package/src/commands/export.ts +93 -0
- package/src/commands/fswrite.ts +928 -0
- package/src/commands/graph.ts +291 -0
- package/src/commands/help.ts +151 -0
- package/src/commands/impact.ts +59 -0
- package/src/commands/init.ts +583 -0
- package/src/commands/instructions.ts +91 -0
- package/src/commands/link.ts +929 -0
- package/src/commands/new.ts +476 -0
- package/src/commands/orphans.ts +457 -0
- package/src/commands/path.ts +67 -0
- package/src/commands/provenance.ts +68 -0
- package/src/commands/query.ts +312 -0
- package/src/commands/reconcile-shared.ts +280 -0
- package/src/commands/rename.ts +585 -0
- package/src/commands/replace.ts +320 -0
- package/src/commands/scaffold.ts +346 -0
- package/src/commands/schema.ts +293 -0
- package/src/commands/snapshot.ts +130 -0
- package/src/commands/supersede.ts +400 -0
- package/src/commands/sync.ts +371 -0
- package/src/commands/tasks.ts +271 -0
- package/src/commands/traversal.ts +151 -0
- package/src/commands/validate.ts +226 -0
- package/src/config.ts +598 -0
- package/src/core/agent-bridge.ts +287 -0
- package/src/core/agent-context.ts +498 -0
- package/src/core/agent-profile.ts +447 -0
- package/src/core/bundle.ts +893 -0
- package/src/core/check.ts +853 -0
- package/src/core/codex-bridge.ts +100 -0
- package/src/core/concept.ts +597 -0
- package/src/core/consumer-scaffold.ts +433 -0
- package/src/core/context.ts +271 -0
- package/src/core/explorer-contract.ts +441 -0
- package/src/core/explorer-qualification.ts +58 -0
- package/src/core/explorer.ts +518 -0
- package/src/core/finding.ts +31 -0
- package/src/core/graph.ts +201 -0
- package/src/core/indexes.ts +436 -0
- package/src/core/instructions.ts +209 -0
- package/src/core/ladybug-driver.ts +1795 -0
- package/src/core/ladybug-lifecycle.ts +1178 -0
- package/src/core/ladybug-native.ts +95 -0
- package/src/core/ladybug-source.ts +667 -0
- package/src/core/links.ts +681 -0
- package/src/core/log.ts +253 -0
- package/src/core/managed-block.ts +540 -0
- package/src/core/manifest.ts +718 -0
- package/src/core/order.ts +13 -0
- package/src/core/profile.ts +1007 -0
- package/src/core/projection.ts +195 -0
- package/src/core/query.ts +542 -0
- package/src/core/reconcile.ts +236 -0
- package/src/core/replace.ts +419 -0
- package/src/core/retrieval.ts +213 -0
- package/src/core/rewrite.ts +940 -0
- package/src/core/scaffold.ts +255 -0
- package/src/core/schema.ts +366 -0
- package/src/core/snapshot-runtime.ts +52 -0
- package/src/core/snapshot-store.ts +287 -0
- package/src/core/snapshot.ts +711 -0
- package/src/core/template.ts +429 -0
- package/src/core/traversal.ts +487 -0
- package/src/core/validate.ts +517 -0
- package/src/core/workspace-contract.ts +473 -0
- package/src/core/workspace-projection.ts +365 -0
- package/src/core/workspace-retrieval.ts +196 -0
- package/src/core/workspace-source.ts +174 -0
- package/src/errors.ts +697 -0
- package/src/meta.ts +7 -0
- package/src/output.ts +589 -0
- package/src/scripts/upstream-backlog-watch.ts +288 -0
- package/src/state.ts +390 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/** Pure snapshot, view-model, and self-contained artifact logic for the local graph explorer. */
|
|
2
|
+
|
|
3
|
+
import { Buffer } from "node:buffer";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
EXPLORER_CHANGE_SNAPSHOT_SCHEMA_VERSION,
|
|
7
|
+
EXPLORER_RENDER_LIMITS,
|
|
8
|
+
EXPLORER_SNAPSHOT_SCHEMA_VERSION,
|
|
9
|
+
type ExplorerChangeSnapshot,
|
|
10
|
+
type ExplorerSnapshot,
|
|
11
|
+
parseExplorerChangeSnapshot,
|
|
12
|
+
parseExplorerSnapshot,
|
|
13
|
+
serializeExplorerChangeSnapshot,
|
|
14
|
+
serializeExplorerSnapshot,
|
|
15
|
+
} from "./explorer-contract";
|
|
16
|
+
import type { LadybugProjectionSource, ProjectionConceptRecord, ProjectionEdgeRecord } from "./ladybug-source";
|
|
17
|
+
import { compareCodeUnits } from "./order";
|
|
18
|
+
import { CHANGED_MAX_LIMIT, compareRetainedSnapshots, type RetainedSnapshot } from "./snapshot";
|
|
19
|
+
|
|
20
|
+
export const EXPLORER_ARTIFACT_VERSION = "lore-explorer-artifact/1" as const;
|
|
21
|
+
|
|
22
|
+
export type ExplorerNodeKind = "repository" | "concept" | "task";
|
|
23
|
+
|
|
24
|
+
export interface ExplorerViewState {
|
|
25
|
+
readonly search?: string;
|
|
26
|
+
readonly kinds?: readonly ExplorerNodeKind[];
|
|
27
|
+
readonly types?: readonly string[];
|
|
28
|
+
readonly statuses?: readonly string[];
|
|
29
|
+
readonly focusRecordKey?: string | null;
|
|
30
|
+
readonly selectedRecordKey?: string | null;
|
|
31
|
+
readonly depth?: number;
|
|
32
|
+
readonly limit?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ExplorerViewNode {
|
|
36
|
+
readonly recordKey: string;
|
|
37
|
+
readonly kind: ExplorerNodeKind;
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly type: string;
|
|
40
|
+
readonly title: string;
|
|
41
|
+
readonly summary: string | null;
|
|
42
|
+
readonly status: string | null;
|
|
43
|
+
readonly tags: readonly string[];
|
|
44
|
+
readonly sourcePath: string | null;
|
|
45
|
+
readonly repositoryScopeKey: string;
|
|
46
|
+
readonly snapshotKey: string;
|
|
47
|
+
readonly bundleId: string;
|
|
48
|
+
readonly gitCommit: string | null;
|
|
49
|
+
readonly exportDigest: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ExplorerViewEdge {
|
|
53
|
+
readonly recordKey: string;
|
|
54
|
+
readonly edgeKind: string;
|
|
55
|
+
readonly fromRecordKey: string;
|
|
56
|
+
readonly toRecordKey: string | null;
|
|
57
|
+
readonly target: string;
|
|
58
|
+
readonly dangling: boolean;
|
|
59
|
+
readonly relation: "inbound" | "outbound" | "both" | "none";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ExplorerView {
|
|
63
|
+
readonly nodes: readonly ExplorerViewNode[];
|
|
64
|
+
readonly edges: readonly ExplorerViewEdge[];
|
|
65
|
+
readonly selected: ExplorerViewNode | null;
|
|
66
|
+
readonly supersessionChain: readonly string[];
|
|
67
|
+
readonly totalMatchingNodes: number;
|
|
68
|
+
readonly truncated: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Build the separate historical explorer contract without changing ordinary explorer bytes. */
|
|
72
|
+
export function buildExplorerChangeSnapshot(
|
|
73
|
+
from: RetainedSnapshot,
|
|
74
|
+
to: RetainedSnapshot,
|
|
75
|
+
options: {
|
|
76
|
+
readonly mode: "snapshot" | "comparison";
|
|
77
|
+
readonly repositories?: readonly string[];
|
|
78
|
+
},
|
|
79
|
+
): ExplorerChangeSnapshot {
|
|
80
|
+
return parseExplorerChangeSnapshot({
|
|
81
|
+
schemaVersion: EXPLORER_CHANGE_SNAPSHOT_SCHEMA_VERSION,
|
|
82
|
+
mode: options.mode,
|
|
83
|
+
from,
|
|
84
|
+
to,
|
|
85
|
+
comparison: compareRetainedSnapshots(from, to, {
|
|
86
|
+
limit: CHANGED_MAX_LIMIT,
|
|
87
|
+
repositories: options.repositories ?? [],
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Map the validated projection source used by the persistent index into the frozen browser contract. */
|
|
93
|
+
export function buildExplorerSnapshot(source: LadybugProjectionSource): ExplorerSnapshot {
|
|
94
|
+
const common = {
|
|
95
|
+
repositoryScopeKey: source.repositoryScopeKey,
|
|
96
|
+
snapshotKey: source.snapshotKey,
|
|
97
|
+
bundleId: source.manifest.bundle.id,
|
|
98
|
+
gitCommit: source.manifest.bundle.gitCommit,
|
|
99
|
+
exportDigest: source.exportDigest,
|
|
100
|
+
};
|
|
101
|
+
const concepts = source.concepts
|
|
102
|
+
.map((record) => conceptFact(record, common))
|
|
103
|
+
.sort((a, b) => compareCodeUnits(a.recordKey, b.recordKey));
|
|
104
|
+
const tasks = source.tasks
|
|
105
|
+
.map((record) => ({
|
|
106
|
+
...common,
|
|
107
|
+
recordKey: record.key,
|
|
108
|
+
sourcePath: null,
|
|
109
|
+
kind: "task" as const,
|
|
110
|
+
taskId: record.id,
|
|
111
|
+
title: bounded(record.title, 1_024) ?? record.id,
|
|
112
|
+
summary: null,
|
|
113
|
+
status: bounded(record.status, 256) ?? "Unknown",
|
|
114
|
+
labels: boundedList(record.labels, 256, 256),
|
|
115
|
+
priority: bounded(record.priority, 256),
|
|
116
|
+
assignees: boundedList(record.assignees, 256, 256),
|
|
117
|
+
milestone: bounded(record.milestone, 256),
|
|
118
|
+
parentTaskId: bounded(record.parentTaskId, 256),
|
|
119
|
+
}))
|
|
120
|
+
.sort((a, b) => compareCodeUnits(a.recordKey, b.recordKey));
|
|
121
|
+
const conceptPaths = new Map(concepts.map((concept) => [concept.recordKey, concept.sourcePath]));
|
|
122
|
+
const authoredEdges = source.authoredEdges
|
|
123
|
+
.map((record) => edgeFact(record, common, conceptPaths.get(record.from) ?? null))
|
|
124
|
+
.sort(compareExplorerEdges);
|
|
125
|
+
const hasFacts = concepts.length + tasks.length + authoredEdges.length > 0;
|
|
126
|
+
const repositories = hasFacts
|
|
127
|
+
? [
|
|
128
|
+
{
|
|
129
|
+
...common,
|
|
130
|
+
kind: "repository" as const,
|
|
131
|
+
docsRoot: source.manifest.bundle.docsRoot,
|
|
132
|
+
displayName: repositoryDisplayName(source),
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
: [];
|
|
136
|
+
const edgeFingerprints = new Set<string>();
|
|
137
|
+
let duplicateEdges = 0;
|
|
138
|
+
let danglingEdges = 0;
|
|
139
|
+
for (const edge of authoredEdges) {
|
|
140
|
+
if (edge.dangling) danglingEdges += 1;
|
|
141
|
+
const fingerprint = [edge.fromRecordKey, edge.edgeKind, edge.target].join("\0");
|
|
142
|
+
if (edgeFingerprints.has(fingerprint)) duplicateEdges += 1;
|
|
143
|
+
edgeFingerprints.add(fingerprint);
|
|
144
|
+
}
|
|
145
|
+
const warnings = [...new Set(source.warnings.map((warning) => bounded(warning, 1_024)).filter(isString))]
|
|
146
|
+
.sort(compareCodeUnits)
|
|
147
|
+
.slice(0, 64);
|
|
148
|
+
return parseExplorerSnapshot({
|
|
149
|
+
schemaVersion: EXPLORER_SNAPSHOT_SCHEMA_VERSION,
|
|
150
|
+
source: {
|
|
151
|
+
...common,
|
|
152
|
+
docsRoot: source.manifest.bundle.docsRoot,
|
|
153
|
+
sourceFingerprint: source.sourceFingerprint,
|
|
154
|
+
generatedAt: null,
|
|
155
|
+
},
|
|
156
|
+
facts: { repositories, concepts, tasks, authoredEdges },
|
|
157
|
+
health: {
|
|
158
|
+
state: hasFacts ? "ready" : "empty",
|
|
159
|
+
messageCode: null,
|
|
160
|
+
counts: {
|
|
161
|
+
repositories: repositories.length,
|
|
162
|
+
concepts: concepts.length,
|
|
163
|
+
tasks: tasks.length,
|
|
164
|
+
authoredEdges: authoredEdges.length,
|
|
165
|
+
danglingEdges,
|
|
166
|
+
duplicateEdges,
|
|
167
|
+
},
|
|
168
|
+
warnings,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Derive the bounded list/relationship model shared by tests and the browser runtime contract. */
|
|
174
|
+
export function deriveExplorerView(snapshotValue: unknown, state: ExplorerViewState = {}): ExplorerView {
|
|
175
|
+
const snapshot = parseExplorerSnapshot(snapshotValue);
|
|
176
|
+
const allNodes = explorerNodes(snapshot);
|
|
177
|
+
const byKey = new Map(allNodes.map((node) => [node.recordKey, node]));
|
|
178
|
+
const search = (state.search ?? "").trim().toLowerCase();
|
|
179
|
+
const kinds = new Set(state.kinds ?? []);
|
|
180
|
+
const types = new Set((state.types ?? []).map((type) => type.toLowerCase()));
|
|
181
|
+
const statuses = new Set((state.statuses ?? []).map((status) => status.toLowerCase()));
|
|
182
|
+
const focus = state.focusRecordKey ?? null;
|
|
183
|
+
const depth = Math.max(0, Math.min(state.depth ?? 1, EXPLORER_RENDER_LIMITS.maximumFocusDepth));
|
|
184
|
+
const neighborhood = focus === null ? null : focusNeighborhood(snapshot, focus, depth);
|
|
185
|
+
const matching = allNodes.filter((node) => {
|
|
186
|
+
if (kinds.size > 0 && !kinds.has(node.kind)) return false;
|
|
187
|
+
if (types.size > 0 && !types.has(node.type.toLowerCase())) return false;
|
|
188
|
+
if (statuses.size > 0 && !statuses.has((node.status ?? "").toLowerCase())) return false;
|
|
189
|
+
if (neighborhood !== null && !neighborhood.has(node.recordKey)) return false;
|
|
190
|
+
if (search === "") return true;
|
|
191
|
+
return [node.id, node.type, node.title, node.summary ?? "", node.status ?? "", node.sourcePath ?? "", ...node.tags]
|
|
192
|
+
.join("\n")
|
|
193
|
+
.toLowerCase()
|
|
194
|
+
.includes(search);
|
|
195
|
+
});
|
|
196
|
+
const requestedLimit = state.limit ?? EXPLORER_RENDER_LIMITS.initialNodeLimit;
|
|
197
|
+
const limit = Math.max(0, Math.min(requestedLimit, EXPLORER_RENDER_LIMITS.maximumVisibleNodes));
|
|
198
|
+
const nodes = matching.slice(0, limit);
|
|
199
|
+
const visible = new Set(nodes.map((node) => node.recordKey));
|
|
200
|
+
const selectedKey = state.selectedRecordKey ?? focus;
|
|
201
|
+
const edgeLimit =
|
|
202
|
+
focus === null ? EXPLORER_RENDER_LIMITS.initialEdgeLimit : EXPLORER_RENDER_LIMITS.maximumVisibleEdges;
|
|
203
|
+
const edges = snapshot.facts.authoredEdges
|
|
204
|
+
.filter((edge) => visible.has(edge.fromRecordKey) && (edge.toRecordKey === null || visible.has(edge.toRecordKey)))
|
|
205
|
+
.slice(0, edgeLimit)
|
|
206
|
+
.map((edge) => ({
|
|
207
|
+
recordKey: edge.recordKey,
|
|
208
|
+
edgeKind: edge.edgeKind,
|
|
209
|
+
fromRecordKey: edge.fromRecordKey,
|
|
210
|
+
toRecordKey: edge.toRecordKey,
|
|
211
|
+
target: edge.target,
|
|
212
|
+
dangling: edge.dangling,
|
|
213
|
+
relation: edgeRelation(edge, selectedKey),
|
|
214
|
+
}));
|
|
215
|
+
const selected = selectedKey === null || selectedKey === undefined ? null : (byKey.get(selectedKey) ?? null);
|
|
216
|
+
return {
|
|
217
|
+
nodes,
|
|
218
|
+
edges,
|
|
219
|
+
selected,
|
|
220
|
+
supersessionChain: selected === null ? [] : supersessionChain(snapshot, selected.recordKey),
|
|
221
|
+
totalMatchingNodes: matching.length,
|
|
222
|
+
truncated: nodes.length < matching.length,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Render one deterministic, self-contained HTML file. Snapshot bytes are base64-embedded and never fetched. */
|
|
227
|
+
export function renderExplorerArtifact(snapshotValue: unknown): string {
|
|
228
|
+
const snapshot = parseExplorerSnapshot(snapshotValue);
|
|
229
|
+
const snapshotBytes = serializeExplorerSnapshot(snapshot);
|
|
230
|
+
const encoded = Buffer.from(snapshotBytes, "utf8").toString("base64");
|
|
231
|
+
return `<!doctype html>
|
|
232
|
+
<html lang="en">
|
|
233
|
+
<head>
|
|
234
|
+
<meta charset="utf-8">
|
|
235
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
236
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; connect-src 'none'; font-src 'none'; base-uri 'none'; form-action 'none'">
|
|
237
|
+
<meta name="generator" content="${EXPLORER_ARTIFACT_VERSION}">
|
|
238
|
+
<title>Lore graph explorer</title>
|
|
239
|
+
<style>
|
|
240
|
+
:root{color-scheme:light dark;font:16px/1.45 ui-sans-serif,system-ui,sans-serif;--bg:#f6f3ec;--panel:#fffdf7;--ink:#20231f;--muted:#555b53;--line:#888b83;--accent:#075e55;--in:#674092;--out:#964300;--warn:#8f2815}*{box-sizing:border-box}html,body{max-width:100%;overflow-x:hidden}body{margin:0;background:var(--bg);color:var(--ink)}header{padding:1rem 1.25rem;border-bottom:1px solid var(--line);background:var(--panel)}h1,h2,h3,p{margin:.25rem 0 .75rem}.skip-link{position:absolute;left:.5rem;top:-5rem;padding:.7rem;background:var(--panel);color:var(--ink);z-index:10}.skip-link:focus{top:.5rem}.layout{display:grid;grid-template-columns:minmax(14rem,25rem) minmax(18rem,1fr) minmax(16rem,28rem);gap:1rem;padding:1rem}.panel{background:var(--panel);border:1px solid var(--line);border-radius:.65rem;padding:1rem;min-width:0}.controls{display:grid;gap:.7rem}.controls label{display:grid;gap:.2rem;font-weight:600}.checks{display:flex;flex-wrap:wrap;gap:.5rem}.checks label{display:flex;align-items:center;gap:.25rem;font-weight:400}.checks input[type=checkbox]{width:auto;min-width:1rem;height:1rem}input,select,button{font:inherit;min-width:0}input,select{width:100%;padding:.55rem;border:1px solid var(--line);border-radius:.35rem;background:var(--panel);color:var(--ink)}button{width:100%;text-align:left;padding:.65rem;border:1px solid var(--line);border-radius:.4rem;background:transparent;color:inherit;cursor:pointer}button:disabled,input:disabled,select:disabled{cursor:not-allowed;opacity:.65}:focus-visible{outline:3px solid var(--accent);outline-offset:2px}.node-list,.relation-list{list-style:none;margin:0;padding:0;display:grid;gap:.45rem}.node button[aria-selected=true]{outline:3px solid var(--accent)}.node.inbound button{border-left:6px double var(--in)}.node.outbound button{border-left:6px dashed var(--out)}.node.both button{border-left:6px solid var(--accent)}.badge,.relation-cue,.flag{display:inline-block;margin:.1rem .35rem .1rem 0;padding:.05rem .4rem;border:1px solid currentColor;border-radius:999px;font-size:.78rem}.relation-cue{font-weight:700}.muted{color:var(--muted)}.warning{color:var(--warn);font-weight:600}dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:.35rem .75rem}dt{font-weight:700}dd{margin:0;overflow-wrap:anywhere}.relation-list button{padding:.35rem}.sr-status{min-height:1.5em}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.legend{display:flex;gap:.75rem;flex-wrap:wrap}.legend span{border:1px solid currentColor;padding:.15rem .35rem}.legend .in{border-style:double}.legend .out{border-style:dashed}@media(max-width:950px){.layout{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.details{grid-column:1/-1}}@media(max-width:640px){.layout{grid-template-columns:minmax(0,1fr);padding:.5rem}.details{grid-column:auto}dl{grid-template-columns:minmax(0,1fr)}dt{margin-top:.4rem}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media(forced-colors:active){:root{--bg:Canvas;--panel:Canvas;--ink:CanvasText;--muted:CanvasText;--line:CanvasText;--accent:Highlight;--in:LinkText;--out:CanvasText;--warn:MarkText}.node button[aria-selected=true]{outline-color:Highlight}.badge,.relation-cue,.flag,.legend span{forced-color-adjust:auto}}@media(prefers-color-scheme:dark){:root{--bg:#171a18;--panel:#202521;--ink:#f2f0e8;--muted:#c7ccc3;--line:#8b918a;--accent:#70d9cc;--in:#cbb0ee;--out:#ffad69;--warn:#ffad9a}}
|
|
241
|
+
</style>
|
|
242
|
+
</head>
|
|
243
|
+
<body>
|
|
244
|
+
<a class="skip-link" href="#records">Skip to records</a>
|
|
245
|
+
<header><h1>Lore graph explorer</h1><h2 id="status-heading" tabindex="-1">Snapshot status</h2><p id="provenance" class="muted"></p><p id="health" class="sr-status" role="status" aria-live="polite"></p><p id="announcement" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></p></header>
|
|
246
|
+
<main class="layout">
|
|
247
|
+
<section class="panel controls" aria-labelledby="controls-title"><h2 id="controls-title">Find evidence</h2><label for="search">Search</label><input id="search" type="search" autocomplete="off" aria-controls="nodes"><fieldset><legend>Record kind</legend><div class="checks" id="kind-filters"></div></fieldset><label for="type-filter">Type</label><select id="type-filter" aria-controls="nodes"><option value="">All types</option></select><label for="status">Status</label><select id="status" aria-controls="nodes"><option value="">All statuses</option></select><label for="depth">Focus depth</label><select id="depth" aria-controls="nodes"><option>0</option><option selected>1</option><option>2</option><option>3</option><option>4</option></select><button id="clear-focus" type="button">Clear focus</button><div class="legend" aria-label="Relationship legend"><span class="in">Inbound: double line</span><span class="out">Outbound: dashed line</span></div></section>
|
|
248
|
+
<section id="records" class="panel" tabindex="-1" aria-labelledby="records-title"><h2 id="records-title">Records</h2><p id="counts" class="muted"></p><ul id="nodes" class="node-list" role="listbox" aria-label="Graph records" aria-describedby="counts"></ul></section>
|
|
249
|
+
<aside class="panel details" aria-labelledby="details-title"><h2 id="details-title">Details</h2><div id="details"><p class="muted">Select a record to inspect provenance and relationships.</p></div></aside>
|
|
250
|
+
</main>
|
|
251
|
+
<script>
|
|
252
|
+
"use strict";
|
|
253
|
+
const SNAPSHOT=JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("${encoded}"),c=>c.charCodeAt(0))));
|
|
254
|
+
const LIMITS=${JSON.stringify(EXPLORER_RENDER_LIMITS)};
|
|
255
|
+
const byKey=new Map(),nodes=[],recordButtons=new Map(),kindInputs=[];
|
|
256
|
+
for(const r of SNAPSHOT.facts.repositories){const n={...r,recordKey:r.repositoryScopeKey,id:r.displayName,type:"Repository",title:r.displayName,summary:r.docsRoot,status:null,tags:[]};nodes.push(n);byKey.set(n.recordKey,n)}
|
|
257
|
+
for(const r of SNAPSHOT.facts.concepts){const n={...r,id:r.conceptId,type:r.conceptType};nodes.push(n);byKey.set(n.recordKey,n)}
|
|
258
|
+
for(const r of SNAPSHOT.facts.tasks){const n={...r,id:r.taskId,type:"Task",tags:r.labels};nodes.push(n);byKey.set(n.recordKey,n)}
|
|
259
|
+
nodes.sort((a,b)=>a.recordKey<b.recordKey?-1:a.recordKey>b.recordKey?1:0);
|
|
260
|
+
const navigationEnabled=SNAPSHOT.health.state==="ready"||SNAPSHOT.health.state==="stale";
|
|
261
|
+
const state={search:"",kinds:new Set(),type:"",status:"",selected:null,focus:null,depth:1};
|
|
262
|
+
const $=id=>document.getElementById(id);const make=(tag,text,cls)=>{const el=document.createElement(tag);if(text!==undefined)el.textContent=text;if(cls)el.className=cls;return el};
|
|
263
|
+
for(const kind of ["repository","concept","task"]){const label=make("label");const input=document.createElement("input");input.type="checkbox";input.value=kind;kindInputs.push(input);input.addEventListener("change",()=>{input.checked?state.kinds.add(kind):state.kinds.delete(kind);render()});label.append(input,document.createTextNode(kind));$("kind-filters").append(label)}
|
|
264
|
+
for(const type of [...new Set(nodes.map(n=>n.type))].sort()){const option=make("option",type);option.value=type;$("type-filter").append(option)}for(const status of [...new Set(nodes.map(n=>n.status).filter(Boolean))].sort()){const option=make("option",status);option.value=status;$("status").append(option)}
|
|
265
|
+
$("search").addEventListener("input",e=>{state.search=e.target.value.toLowerCase();render()});$("type-filter").addEventListener("change",e=>{state.type=e.target.value;render()});$("status").addEventListener("change",e=>{state.status=e.target.value;render()});$("depth").addEventListener("change",e=>{state.depth=Number(e.target.value);render()});$("clear-focus").addEventListener("click",()=>{state.focus=null;render();focusRecord(state.selected)});
|
|
266
|
+
function neighbors(root,depth){const seen=new Set([root]),queue=[[root,0]];while(queue.length){const [key,d]=queue.shift();if(d>=depth)continue;for(const edge of SNAPSHOT.facts.authoredEdges){let next=null;if(edge.fromRecordKey===key)next=edge.toRecordKey;else if(edge.toRecordKey===key)next=edge.fromRecordKey;if(next&&!seen.has(next)){seen.add(next);queue.push([next,d+1])}}}return seen}
|
|
267
|
+
function matches(n,scope){if(state.kinds.size&&!state.kinds.has(n.kind))return false;if(state.type&&n.type!==state.type)return false;if(state.status&&n.status!==state.status)return false;if(scope&&!scope.has(n.recordKey))return false;if(!state.search)return true;return [n.id,n.type,n.title,n.summary||"",n.status||"",n.sourcePath||"",...(n.tags||[])].join("\\n").toLowerCase().includes(state.search)}
|
|
268
|
+
function flagsFor(key){const flags=[];const seen=new Set();for(const edge of SNAPSHOT.facts.authoredEdges){if(edge.fromRecordKey!==key&&edge.toRecordKey!==key)continue;if(edge.dangling)flags.push("dangling");if(edge.edgeKind==="supersedes"||edge.edgeKind==="superseded_by")flags.push("supersession");const fingerprint=[edge.fromRecordKey,edge.edgeKind,edge.target].join("|");if(seen.has(fingerprint))flags.push("duplicate edge");seen.add(fingerprint)}return [...new Set(flags)]}
|
|
269
|
+
function relationFor(key){if(!state.selected||key===state.selected)return"none";let inbound=false,outbound=false;for(const edge of SNAPSHOT.facts.authoredEdges){if(edge.fromRecordKey===key&&edge.toRecordKey===state.selected)inbound=true;if(edge.fromRecordKey===state.selected&&edge.toRecordKey===key)outbound=true}return inbound&&outbound?"both":inbound?"inbound":outbound?"outbound":"none"}
|
|
270
|
+
function selectRecord(key,returnFocus){state.selected=key;render();if(returnFocus)focusRecord(key)}
|
|
271
|
+
function focusRecord(key){const button=key?recordButtons.get(key):null;if(button&&button.focus)button.focus()}
|
|
272
|
+
function closeDetails(key){state.selected=null;render();focusRecord(key)}
|
|
273
|
+
function handleRecordKey(event,index,key,visible){if(["ArrowDown","ArrowRight","ArrowUp","ArrowLeft","Home","End","Enter","Escape"].includes(event.key)&&event.preventDefault)event.preventDefault();let next=index;if(event.key==="ArrowDown"||event.key==="ArrowRight")next=Math.min(visible.length-1,index+1);else if(event.key==="ArrowUp"||event.key==="ArrowLeft")next=Math.max(0,index-1);else if(event.key==="Home")next=0;else if(event.key==="End")next=visible.length-1;else if(event.key==="Enter"){selectRecord(key,true);return}else if(event.key==="Escape"){closeDetails(key);return}else return;focusRecord(visible[next].recordKey)}
|
|
274
|
+
function render(){const list=$("nodes");list.replaceChildren();recordButtons.clear();if(!navigationEnabled){list.hidden=true;for(const id of ["search","type-filter","status","depth","clear-focus"])$(id).disabled=true;for(const input of kindInputs)input.disabled=true;$("counts").textContent=SNAPSHOT.health.state==="empty"?"No records. Run lore sync, then rebuild the explorer from source.":"Navigation disabled. Rebuild this artifact from validated source. Code: "+SNAPSHOT.health.messageCode;$("details").replaceChildren(make("p","No trusted graph navigation is available.","warning"));$("status-heading").textContent=SNAPSHOT.health.state==="empty"?"Empty snapshot":"Corrupt snapshot";if($("status-heading").focus)$("status-heading").focus();return}
|
|
275
|
+
list.hidden=false;const scope=state.focus?neighbors(state.focus,state.depth):null;const matched=nodes.filter(n=>matches(n,scope));const nodeLimit=state.focus?LIMITS.maximumVisibleNodes:LIMITS.initialNodeLimit;const visible=matched.slice(0,nodeLimit);if(state.selected&&!visible.some(node=>node.recordKey===state.selected))state.selected=null;for(const [index,node] of visible.entries()){const relation=relationFor(node.recordKey);const li=make("li",undefined,"node "+relation);const button=make("button");button.type="button";button.setAttribute("role","option");button.setAttribute("aria-selected",String(node.recordKey===state.selected));button.setAttribute("data-record-key",node.recordKey);button.tabIndex=node.recordKey===state.selected||(!state.selected&&index===0)?0:-1;button.append(make("span",node.kind,"badge"));if(node.status)button.append(make("span","status: "+node.status,"badge"));if(relation!=="none")button.append(make("span",relation+" neighbor","relation-cue"));for(const flag of flagsFor(node.recordKey))button.append(make("span",flag,"flag"));button.append(document.createTextNode(node.title||node.id));button.addEventListener("click",()=>selectRecord(node.recordKey,false));button.addEventListener("dblclick",()=>{state.focus=node.recordKey;selectRecord(node.recordKey,true)});button.addEventListener("keydown",event=>handleRecordKey(event,index,node.recordKey,visible));recordButtons.set(node.recordKey,button);li.append(button);list.append(li)}
|
|
276
|
+
$("counts").textContent=visible.length+" of "+matched.length+" matching records"+(visible.length<matched.length?"; bounded initial view, filter or focus to expand":"");renderDetails(visible,matched.length)}
|
|
277
|
+
function row(dl,key,value){const dt=make("dt",key),dd=make("dd",value??"—");dl.append(dt,dd)}
|
|
278
|
+
function renderDetails(visible,total){const root=$("details");root.replaceChildren();const node=byKey.get(state.selected);if(!node){root.append(make("p","Select a record to inspect provenance and relationships.","muted"));return}root.append(make("h3",node.title||node.id));const close=make("button","Close details and return to record");close.type="button";close.addEventListener("click",()=>closeDetails(node.recordKey));close.addEventListener("keydown",event=>{if(event.key==="Escape"){if(event.preventDefault)event.preventDefault();closeDetails(node.recordKey)}});root.append(close);const dl=make("dl");row(dl,"ID",node.id);row(dl,"Kind",node.kind);row(dl,"Type",node.type);row(dl,"Status",node.status);row(dl,"Source",node.sourcePath);row(dl,"Commit",node.gitCommit);row(dl,"Export",node.exportDigest);root.append(dl);const focus=make("button","Focus this record to depth "+state.depth);focus.type="button";focus.addEventListener("click",()=>{state.focus=node.recordKey;render();focusRecord(node.recordKey)});root.append(focus);
|
|
279
|
+
const relationshipLimit=state.focus?LIMITS.maximumVisibleEdges:LIMITS.initialEdgeLimit;const allRelated=SNAPSHOT.facts.authoredEdges.filter(e=>e.fromRecordKey===node.recordKey||e.toRecordKey===node.recordKey);const related=allRelated.slice(0,relationshipLimit);const inbound=allRelated.filter(e=>e.toRecordKey===node.recordKey).length,outbound=allRelated.filter(e=>e.fromRecordKey===node.recordKey).length;root.append(make("h3","Relationships"));const ul=make("ul",undefined,"relation-list");for(const edge of related){const isOutbound=edge.fromRecordKey===node.recordKey;const other=isOutbound?edge.toRecordKey:edge.fromRecordKey;const target=other?byKey.get(other):null;const li=make("li");const label=(isOutbound?"outbound ":"inbound ")+edge.edgeKind+" → "+(target?.title||edge.target)+(edge.dangling?" (dangling)":"");if(target){const button=make("button",label);button.type="button";button.addEventListener("click",()=>selectRecord(target.recordKey,false));button.addEventListener("keydown",event=>{if(event.key==="Escape"){if(event.preventDefault)event.preventDefault();focusRecord(node.recordKey)}});li.append(button)}else li.append(make("span",label,"warning"));ul.append(li)}if(!related.length)ul.append(make("li","No authored relationships.","muted"));if(related.length<allRelated.length)ul.append(make("li",related.length+" of "+allRelated.length+" relationships shown","muted"));root.append(ul);
|
|
280
|
+
const supersession=new Set([node.recordKey]),queue=[node.recordKey];while(queue.length){const key=queue.shift();for(const edge of SNAPSHOT.facts.authoredEdges){if(!["supersedes","superseded_by"].includes(edge.edgeKind)||!edge.toRecordKey)continue;let next=null;if(edge.fromRecordKey===key)next=edge.toRecordKey;else if(edge.toRecordKey===key)next=edge.fromRecordKey;if(next&&!supersession.has(next)){supersession.add(next);queue.push(next)}}}if(supersession.size>1){root.append(make("h3","Supersession chain"));const chain=make("ul");for(const key of [...supersession].sort()){const item=byKey.get(key);chain.append(make("li",item?.title||key))}root.append(chain)}const position=visible.findIndex(item=>item.recordKey===node.recordKey)+1;const flags=flagsFor(node.recordKey);$("announcement").textContent=node.kind+" "+(node.title||node.id)+"; "+inbound+" inbound, "+outbound+" outbound; "+(flags.length?flags.join(", "):"no graph-health flags")+"; position "+position+" of "+total}
|
|
281
|
+
$("status-heading").textContent=SNAPSHOT.health.state==="stale"?"Stale snapshot":"Ready snapshot";$("health").textContent=SNAPSHOT.health.state+": "+SNAPSHOT.health.counts.concepts+" concepts, "+SNAPSHOT.health.counts.tasks+" tasks, "+SNAPSHOT.health.counts.authoredEdges+" edges, "+SNAPSHOT.health.counts.danglingEdges+" dangling edges"+(SNAPSHOT.health.messageCode?" · code "+SNAPSHOT.health.messageCode:"");$("provenance").textContent="Snapshot "+SNAPSHOT.source.snapshotKey+" · commit "+(SNAPSHOT.source.gitCommit||"uncommitted")+" · export "+SNAPSHOT.source.exportDigest+" · schema "+SNAPSHOT.schemaVersion;window.__LORE_EXPLORER__={snapshot:SNAPSHOT,state,render};render();
|
|
282
|
+
</script>
|
|
283
|
+
</body>
|
|
284
|
+
</html>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Render a deterministic offline retained-snapshot/change explorer with paired provenance. */
|
|
289
|
+
export function renderExplorerChangeArtifact(snapshotValue: unknown): string {
|
|
290
|
+
const snapshot = parseExplorerChangeSnapshot(snapshotValue);
|
|
291
|
+
const encoded = Buffer.from(serializeExplorerChangeSnapshot(snapshot), "utf8").toString("base64");
|
|
292
|
+
return `<!doctype html>
|
|
293
|
+
<html lang="en">
|
|
294
|
+
<head>
|
|
295
|
+
<meta charset="utf-8">
|
|
296
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
297
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; connect-src 'none'; font-src 'none'; base-uri 'none'; form-action 'none'">
|
|
298
|
+
<meta name="generator" content="${EXPLORER_ARTIFACT_VERSION}">
|
|
299
|
+
<title>Lore retained snapshot explorer</title>
|
|
300
|
+
<style>
|
|
301
|
+
:root{color-scheme:light dark;font:16px/1.45 ui-sans-serif,system-ui,sans-serif;--bg:#f5f3ed;--panel:#fffdf8;--ink:#20231f;--muted:#565b54;--line:#858a82;--accent:#075e55;--add:#176b36;--remove:#9a2d22;--change:#7c4d00}*{box-sizing:border-box}html,body{max-width:100%;overflow-x:hidden}body{margin:0;background:var(--bg);color:var(--ink)}header{padding:1rem 1.25rem;border-bottom:1px solid var(--line);background:var(--panel)}h1,h2,h3,p{margin:.25rem 0 .75rem}.skip{position:absolute;top:-5rem;left:.5rem;background:var(--panel);padding:.7rem}.skip:focus{top:.5rem}.layout{display:grid;grid-template-columns:minmax(14rem,22rem) minmax(18rem,1fr) minmax(18rem,30rem);gap:1rem;padding:1rem}.panel{min-width:0;padding:1rem;border:1px solid var(--line);border-radius:.6rem;background:var(--panel)}.controls{display:grid;gap:.65rem}.controls label{font-weight:650}.checks{display:flex;flex-wrap:wrap;gap:.55rem}.checks label{font-weight:400}input,select,button{font:inherit;color:inherit}input,select{width:100%;padding:.5rem;background:var(--panel);border:1px solid var(--line);border-radius:.35rem}button{width:100%;padding:.6rem;text-align:left;background:transparent;border:1px solid var(--line);border-radius:.35rem;cursor:pointer}:focus-visible{outline:3px solid var(--accent);outline-offset:2px}ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem}.badge{display:inline-block;margin-right:.4rem;padding:.05rem .4rem;border:1px solid currentColor;border-radius:999px;font-size:.8rem}.added{border-left:6px solid var(--add)}.removed{border-left:6px double var(--remove)}.changed{border-left:6px dashed var(--change)}.snapshot{border-left:6px solid var(--accent)}.muted{color:var(--muted)}dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:.35rem .75rem}dt{font-weight:700}dd{margin:0;overflow-wrap:anywhere}pre{overflow:auto;max-height:18rem;padding:.65rem;border:1px solid var(--line);white-space:pre-wrap;overflow-wrap:anywhere}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:950px){.layout{grid-template-columns:1fr 1fr}.details{grid-column:1/-1}}@media(max-width:640px){.layout{grid-template-columns:1fr;padding:.5rem}.details{grid-column:auto}dl{grid-template-columns:1fr}}@media(forced-colors:active){:root{--bg:Canvas;--panel:Canvas;--ink:CanvasText;--muted:CanvasText;--line:CanvasText;--accent:Highlight;--add:LinkText;--remove:MarkText;--change:CanvasText}}@media(prefers-color-scheme:dark){:root{--bg:#171a18;--panel:#202521;--ink:#f2f0e8;--muted:#c7ccc3;--line:#8b918a;--accent:#70d9cc;--add:#86d69c;--remove:#ffaaa0;--change:#f0c277}}
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<a class="skip" href="#records">Skip to retained records</a>
|
|
306
|
+
<header><h1>Lore retained snapshot explorer</h1><p id="scope" class="muted"></p><p id="status" role="status" aria-live="polite"></p><p id="announcement" class="sr-only" role="status" aria-live="polite"></p></header>
|
|
307
|
+
<main class="layout">
|
|
308
|
+
<section class="panel controls" aria-labelledby="filters-title"><h2 id="filters-title">Filter evidence</h2><label for="search">Search</label><input id="search" type="search" autocomplete="off" aria-controls="items"><label for="kind">Fact kind</label><select id="kind"><option value="">All kinds</option><option>concept</option><option>task</option><option>edge</option></select><fieldset><legend>Change classification</legend><div id="change-filters" class="checks"></div></fieldset></section>
|
|
309
|
+
<section id="records" class="panel" tabindex="-1" aria-labelledby="records-title"><h2 id="records-title">Retained records</h2><p id="counts" class="muted"></p><ul id="items" role="listbox" aria-label="Retained snapshot records" aria-describedby="counts"></ul></section>
|
|
310
|
+
<aside class="panel details" aria-labelledby="details-title"><h2 id="details-title">Paired evidence</h2><div id="details"><p class="muted">Select a record to inspect exact source provenance.</p></div></aside>
|
|
311
|
+
</main>
|
|
312
|
+
<script>
|
|
313
|
+
"use strict";
|
|
314
|
+
const SNAPSHOT=JSON.parse(new TextDecoder().decode(Uint8Array.from(atob("${encoded}"),c=>c.charCodeAt(0))));
|
|
315
|
+
const $=id=>document.getElementById(id),make=(tag,text,cls)=>{const el=document.createElement(tag);if(text!==undefined)el.textContent=text;if(cls)el.className=cls;return el};
|
|
316
|
+
const selectedRepositories=new Set(SNAPSHOT.comparison.filters.repositories);const rows=SNAPSHOT.mode==="snapshot"?SNAPSHOT.to.facts.filter(f=>!selectedRepositories.size||(f.provenance.memberId!==null&&selectedRepositories.has(f.provenance.memberId))).map(f=>({change:"snapshot",recordKind:f.kind,id:f.id,recordKey:f.recordKey,fieldsChanged:[],from:f,to:f})):SNAPSHOT.comparison.changes;
|
|
317
|
+
const state={search:"",kind:"",changes:new Set(),selected:null},buttons=new Map();
|
|
318
|
+
for(const change of ["added","removed","changed",...(SNAPSHOT.mode==="snapshot"?["snapshot"]:[])]){const label=make("label"),input=document.createElement("input");input.type="checkbox";input.value=change;input.addEventListener("change",()=>{input.checked?state.changes.add(change):state.changes.delete(change);render()});label.append(input,document.createTextNode(change));$("change-filters").append(label)}
|
|
319
|
+
$("search").addEventListener("input",e=>{state.search=e.target.value.toLowerCase();render()});$("kind").addEventListener("change",e=>{state.kind=e.target.value;render()});
|
|
320
|
+
function fact(row){return row.to||row.from}function matches(row){const f=fact(row);if(state.kind&&row.recordKind!==state.kind)return false;if(state.changes.size&&!state.changes.has(row.change))return false;if(!state.search)return true;return [row.id,row.recordKey,row.recordKind,row.change,f.provenance.sourcePath||"",f.provenance.memberId||"",JSON.stringify(f.value)].join("\\n").toLowerCase().includes(state.search)}
|
|
321
|
+
function select(key){state.selected=key;render();const button=buttons.get(key);if(button)button.focus()}
|
|
322
|
+
function row(dl,key,value){dl.append(make("dt",key),make("dd",value??"—"))}
|
|
323
|
+
function renderDetails(selected){const root=$("details");root.replaceChildren();if(!selected){root.append(make("p","Select a record to inspect exact source provenance.","muted"));return}const f=fact(selected),p=f.provenance;root.append(make("h3",selected.change+" "+selected.recordKind+" "+selected.id));const dl=make("dl");row(dl,"Record key",selected.recordKey);row(dl,"Member",p.memberId);row(dl,"Source path",p.sourcePath);row(dl,"Source key",p.sourceKey);row(dl,"Source record",p.sourceRecordKey);row(dl,"Repository",p.repositoryScopeKey);row(dl,"Commit",p.gitCommit);row(dl,"Export",p.exportDigest);root.append(dl);if(selected.fieldsChanged.length)root.append(make("p","Fields changed: "+selected.fieldsChanged.join(", ")));for(const side of ["from","to"]){if(!selected[side])continue;root.append(make("h3",side+" authored value"));root.append(make("pre",JSON.stringify(selected[side].value,null,2)))}$("announcement").textContent=selected.change+" "+selected.recordKind+" "+selected.id+", source "+(p.sourcePath||"none")}
|
|
324
|
+
function render(){const visible=rows.filter(matches),list=$("items");list.replaceChildren();buttons.clear();visible.forEach((entry,index)=>{const li=make("li",undefined,entry.change),button=make("button");button.type="button";button.setAttribute("role","option");button.setAttribute("aria-selected",String(state.selected===entry.recordKey));button.tabIndex=state.selected===entry.recordKey||(!state.selected&&index===0)?0:-1;button.append(make("span",entry.change,"badge"),make("span",entry.recordKind,"badge"),document.createTextNode(entry.id));button.addEventListener("click",()=>select(entry.recordKey));button.addEventListener("keydown",event=>{let next=index;if(event.key==="ArrowDown"||event.key==="ArrowRight")next=Math.min(visible.length-1,index+1);else if(event.key==="ArrowUp"||event.key==="ArrowLeft")next=Math.max(0,index-1);else if(event.key==="Home")next=0;else if(event.key==="End")next=visible.length-1;else if(event.key==="Enter"){select(entry.recordKey);return}else return;event.preventDefault();const target=buttons.get(visible[next].recordKey);if(target)target.focus()});buttons.set(entry.recordKey,button);li.append(button);list.append(li)});$("counts").textContent=visible.length+" of "+rows.length+" retained records"+(SNAPSHOT.comparison.truncated?"; comparison truncated at its explicit bound":"");renderDetails(rows.find(entry=>entry.recordKey===state.selected)||null)}
|
|
325
|
+
$("scope").textContent=SNAPSHOT.mode+" · "+SNAPSHOT.from.snapshotKey+(SNAPSHOT.mode==="comparison"?" → "+SNAPSHOT.to.snapshotKey:"")+" · schema "+SNAPSHOT.schemaVersion;$("status").textContent=SNAPSHOT.mode==="snapshot"?rows.length+" retained facts":SNAPSHOT.comparison.totalChanges+" changes, "+SNAPSHOT.comparison.shown+" available offline";window.__LORE_EXPLORER_CHANGE__={snapshot:SNAPSHOT,state,render};render();
|
|
326
|
+
</script>
|
|
327
|
+
</body>
|
|
328
|
+
</html>
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function explorerArtifactDigest(html: string): string {
|
|
333
|
+
return `sha256:${createHash("sha256").update(html).digest("hex")}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function conceptFact(
|
|
337
|
+
record: ProjectionConceptRecord,
|
|
338
|
+
common: {
|
|
339
|
+
readonly repositoryScopeKey: string;
|
|
340
|
+
readonly snapshotKey: string;
|
|
341
|
+
readonly bundleId: string;
|
|
342
|
+
readonly gitCommit: string | null;
|
|
343
|
+
readonly exportDigest: string;
|
|
344
|
+
},
|
|
345
|
+
) {
|
|
346
|
+
return {
|
|
347
|
+
...common,
|
|
348
|
+
recordKey: record.key,
|
|
349
|
+
sourcePath: record.path,
|
|
350
|
+
kind: "concept" as const,
|
|
351
|
+
conceptId: record.id,
|
|
352
|
+
conceptType: bounded(record.type, 256) ?? "Concept",
|
|
353
|
+
title: bounded(scalar(record.frontmatter.title), 1_024),
|
|
354
|
+
summary: bounded(scalar(record.frontmatter.summary) ?? scalar(record.frontmatter.description), 4_096),
|
|
355
|
+
status: bounded(scalar(record.frontmatter.status), 256),
|
|
356
|
+
tags: boundedList(record.frontmatter.tags, 256, 256),
|
|
357
|
+
contentHash: record.contentHash,
|
|
358
|
+
tokenEstimate: record.tokenEstimate,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function edgeFact(
|
|
363
|
+
record: ProjectionEdgeRecord,
|
|
364
|
+
common: {
|
|
365
|
+
readonly repositoryScopeKey: string;
|
|
366
|
+
readonly snapshotKey: string;
|
|
367
|
+
readonly bundleId: string;
|
|
368
|
+
readonly gitCommit: string | null;
|
|
369
|
+
readonly exportDigest: string;
|
|
370
|
+
},
|
|
371
|
+
sourcePath: string | null,
|
|
372
|
+
) {
|
|
373
|
+
return {
|
|
374
|
+
...common,
|
|
375
|
+
recordKey: record.key,
|
|
376
|
+
sourcePath,
|
|
377
|
+
kind: "authored-edge" as const,
|
|
378
|
+
edgeKind: bounded(record.kind, 256) ?? "link",
|
|
379
|
+
fromRecordKey: record.from,
|
|
380
|
+
toRecordKey: record.to,
|
|
381
|
+
target: record.target,
|
|
382
|
+
ordinal: record.ordinal,
|
|
383
|
+
dangling: record.dangling,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function repositoryDisplayName(source: LadybugProjectionSource): string {
|
|
388
|
+
const root = source.concepts.find((concept) => concept.id === "index");
|
|
389
|
+
return bounded(scalar(root?.frontmatter.title), 256) ?? source.manifest.bundle.docsRoot;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function explorerNodes(snapshot: ExplorerSnapshot): ExplorerViewNode[] {
|
|
393
|
+
const repositories = snapshot.facts.repositories.map((record) => ({
|
|
394
|
+
recordKey: record.repositoryScopeKey,
|
|
395
|
+
kind: "repository" as const,
|
|
396
|
+
id: record.displayName,
|
|
397
|
+
type: "Repository",
|
|
398
|
+
title: record.displayName,
|
|
399
|
+
summary: record.docsRoot,
|
|
400
|
+
status: null,
|
|
401
|
+
tags: [] as readonly string[],
|
|
402
|
+
sourcePath: record.docsRoot,
|
|
403
|
+
repositoryScopeKey: record.repositoryScopeKey,
|
|
404
|
+
snapshotKey: record.snapshotKey,
|
|
405
|
+
bundleId: record.bundleId,
|
|
406
|
+
gitCommit: record.gitCommit,
|
|
407
|
+
exportDigest: record.exportDigest,
|
|
408
|
+
}));
|
|
409
|
+
const concepts = snapshot.facts.concepts.map((record) => ({
|
|
410
|
+
recordKey: record.recordKey,
|
|
411
|
+
kind: "concept" as const,
|
|
412
|
+
id: record.conceptId,
|
|
413
|
+
type: record.conceptType,
|
|
414
|
+
title: record.title ?? record.conceptId,
|
|
415
|
+
summary: record.summary,
|
|
416
|
+
status: record.status,
|
|
417
|
+
tags: record.tags,
|
|
418
|
+
sourcePath: record.sourcePath,
|
|
419
|
+
repositoryScopeKey: record.repositoryScopeKey,
|
|
420
|
+
snapshotKey: record.snapshotKey,
|
|
421
|
+
bundleId: record.bundleId,
|
|
422
|
+
gitCommit: record.gitCommit,
|
|
423
|
+
exportDigest: record.exportDigest,
|
|
424
|
+
}));
|
|
425
|
+
const tasks = snapshot.facts.tasks.map((record) => ({
|
|
426
|
+
recordKey: record.recordKey,
|
|
427
|
+
kind: "task" as const,
|
|
428
|
+
id: record.taskId,
|
|
429
|
+
type: "Task",
|
|
430
|
+
title: record.title,
|
|
431
|
+
summary: record.summary,
|
|
432
|
+
status: record.status,
|
|
433
|
+
tags: record.labels,
|
|
434
|
+
sourcePath: record.sourcePath,
|
|
435
|
+
repositoryScopeKey: record.repositoryScopeKey,
|
|
436
|
+
snapshotKey: record.snapshotKey,
|
|
437
|
+
bundleId: record.bundleId,
|
|
438
|
+
gitCommit: record.gitCommit,
|
|
439
|
+
exportDigest: record.exportDigest,
|
|
440
|
+
}));
|
|
441
|
+
return [...repositories, ...concepts, ...tasks].sort((a, b) => compareCodeUnits(a.recordKey, b.recordKey));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function focusNeighborhood(snapshot: ExplorerSnapshot, root: string, depth: number): Set<string> {
|
|
445
|
+
const seen = new Set([root]);
|
|
446
|
+
const queue: Array<readonly [string, number]> = [[root, 0]];
|
|
447
|
+
for (let index = 0; index < queue.length; index++) {
|
|
448
|
+
const [key, distance] = queue[index] as readonly [string, number];
|
|
449
|
+
if (distance >= depth) continue;
|
|
450
|
+
for (const edge of snapshot.facts.authoredEdges) {
|
|
451
|
+
const next = edge.fromRecordKey === key ? edge.toRecordKey : edge.toRecordKey === key ? edge.fromRecordKey : null;
|
|
452
|
+
if (next !== null && !seen.has(next)) {
|
|
453
|
+
seen.add(next);
|
|
454
|
+
queue.push([next, distance + 1]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return seen;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function supersessionChain(snapshot: ExplorerSnapshot, root: string): string[] {
|
|
462
|
+
const seen = new Set([root]);
|
|
463
|
+
const queue = [root];
|
|
464
|
+
for (let index = 0; index < queue.length; index++) {
|
|
465
|
+
const key = queue[index] as string;
|
|
466
|
+
for (const edge of snapshot.facts.authoredEdges) {
|
|
467
|
+
if (edge.edgeKind !== "supersedes" && edge.edgeKind !== "superseded_by") continue;
|
|
468
|
+
const next = edge.fromRecordKey === key ? edge.toRecordKey : edge.toRecordKey === key ? edge.fromRecordKey : null;
|
|
469
|
+
if (next !== null && !seen.has(next)) {
|
|
470
|
+
seen.add(next);
|
|
471
|
+
queue.push(next);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return [...seen].sort(compareCodeUnits);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function edgeRelation(
|
|
479
|
+
edge: ExplorerSnapshot["facts"]["authoredEdges"][number],
|
|
480
|
+
selected: string | null | undefined,
|
|
481
|
+
): ExplorerViewEdge["relation"] {
|
|
482
|
+
if (selected === null || selected === undefined) return "none";
|
|
483
|
+
const inbound = edge.toRecordKey === selected;
|
|
484
|
+
const outbound = edge.fromRecordKey === selected;
|
|
485
|
+
return inbound && outbound ? "both" : inbound ? "inbound" : outbound ? "outbound" : "none";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function compareExplorerEdges(
|
|
489
|
+
a: ExplorerSnapshot["facts"]["authoredEdges"][number],
|
|
490
|
+
b: ExplorerSnapshot["facts"]["authoredEdges"][number],
|
|
491
|
+
): number {
|
|
492
|
+
const key = (edge: typeof a) =>
|
|
493
|
+
[edge.fromRecordKey, edge.edgeKind, edge.target, String(edge.ordinal).padStart(16, "0"), edge.recordKey].join("\0");
|
|
494
|
+
return compareCodeUnits(key(a), key(b));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function scalar(value: unknown): string | null {
|
|
498
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function bounded(value: string | null | undefined, maximum: number): string | null {
|
|
502
|
+
if (value === null || value === undefined) return null;
|
|
503
|
+
const clean = value.trim();
|
|
504
|
+
if (clean === "") return null;
|
|
505
|
+
return clean.slice(0, maximum);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function boundedList(value: unknown, maximumItems: number, maximumLength: number): string[] {
|
|
509
|
+
if (!Array.isArray(value)) return [];
|
|
510
|
+
return value
|
|
511
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
512
|
+
.map((entry) => entry.slice(0, maximumLength))
|
|
513
|
+
.slice(0, maximumItems);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function isString(value: string | null): value is string {
|
|
517
|
+
return value !== null;
|
|
518
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* finding.ts — the shared shape of a tiered lint/validation **finding**.
|
|
3
|
+
*
|
|
4
|
+
* `lore validate` (concept frontmatter/section/quote-safety checks) and `lore check`
|
|
5
|
+
* (link/anchor + portability + external-liveness passes) each emit a list of typed problems.
|
|
6
|
+
* Those two finding types had drifted into near-identical copies — the same `severity` tiering
|
|
7
|
+
* and `{ severity, rule, message }` core, spelled twice (a `/code-review max` finding). This
|
|
8
|
+
* module is the single home for that core so the two passes can never disagree on what a
|
|
9
|
+
* finding *is*; each keeps only its own domain `rule` union (and `check` its per-file `file`
|
|
10
|
+
* field) by parameterizing the shared shape.
|
|
11
|
+
*
|
|
12
|
+
* Pure types only — no runtime, no IO.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The two finding tiers: an `error` fails the gate (exit 6); a `warning` is advisory. */
|
|
16
|
+
export type Severity = "error" | "warning";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One tiered problem, parameterized by the domain `rule` union of the pass that raised it
|
|
20
|
+
* ({@link import('./validate').FindingRule} for `validate`, {@link import('./check').CheckRule}
|
|
21
|
+
* for `check`). The `severity`/`rule`/`message` core is shared; a pass that needs more (e.g.
|
|
22
|
+
* `check`'s per-file attribution) intersects this with its own fields.
|
|
23
|
+
*/
|
|
24
|
+
export interface Finding<Rule extends string = string> {
|
|
25
|
+
/** `error` (fails the gate / exit 6) or `warning` (advisory; fails only under `--strict`). */
|
|
26
|
+
readonly severity: Severity;
|
|
27
|
+
/** The pass-specific check that raised it. */
|
|
28
|
+
readonly rule: Rule;
|
|
29
|
+
/** A single-line, actionable description. */
|
|
30
|
+
readonly message: string;
|
|
31
|
+
}
|