project-librarian 0.2.1 → 0.4.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/README.ko.md +261 -93
- package/README.md +190 -76
- package/SKILL.md +16 -10
- package/dist/args.js +25 -2
- package/dist/code-index-file-policy.js +105 -2
- package/dist/code-index.js +338 -70
- package/dist/hooks.js +78 -0
- package/dist/init-project-wiki.js +265 -167
- package/dist/install-skill.js +2 -9
- package/dist/mcp-server.js +709 -0
- package/dist/migration.js +875 -60
- package/dist/modes.js +262 -62
- package/dist/retrieval-eval.js +68 -0
- package/dist/taxonomy.js +193 -0
- package/dist/templates.js +214 -36
- package/dist/wiki-concepts.js +49 -0
- package/dist/wiki-files.js +120 -27
- package/dist/wiki-graph.js +166 -0
- package/dist/wiki-visualizer.js +558 -0
- package/package.json +5 -4
- package/README.ja.md +0 -215
- package/README.zh.md +0 -215
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.defaultWikiVisualizerOutput = void 0;
|
|
37
|
+
exports.buildWikiVisualizerPayload = buildWikiVisualizerPayload;
|
|
38
|
+
exports.writeWikiVisualizer = writeWikiVisualizer;
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const wiki_concepts_1 = require("./wiki-concepts");
|
|
41
|
+
const wiki_graph_1 = require("./wiki-graph");
|
|
42
|
+
const wiki_files_1 = require("./wiki-files");
|
|
43
|
+
const workspace_1 = require("./workspace");
|
|
44
|
+
exports.defaultWikiVisualizerOutput = ".project-wiki/wiki-graph.html";
|
|
45
|
+
function uniqueSorted(values) {
|
|
46
|
+
return Array.from(new Set(values)).sort();
|
|
47
|
+
}
|
|
48
|
+
function visualizerEdges(graph, pages) {
|
|
49
|
+
const linkEdges = graph.links
|
|
50
|
+
.filter((link) => graph.files.has(link.normalizedTarget))
|
|
51
|
+
.map((link) => ({ kind: "link", source: link.file, target: link.normalizedTarget }));
|
|
52
|
+
const decisionRefEdges = [];
|
|
53
|
+
for (const page of pages) {
|
|
54
|
+
const ref = graph.outgoingDecisionRef.get(page.file);
|
|
55
|
+
if (ref && graph.files.has(ref))
|
|
56
|
+
decisionRefEdges.push({ kind: "decision_ref", source: page.file, target: ref });
|
|
57
|
+
}
|
|
58
|
+
return [...linkEdges, ...decisionRefEdges].sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.kind.localeCompare(b.kind));
|
|
59
|
+
}
|
|
60
|
+
function brokenLinksFor(graph, file) {
|
|
61
|
+
return uniqueSorted((graph.outgoingLinks.get(file) ?? []).map((link) => link.normalizedTarget).filter((target) => !graph.files.has(target)));
|
|
62
|
+
}
|
|
63
|
+
function buildWikiVisualizerPayload(pages, generatedAt = new Date().toISOString()) {
|
|
64
|
+
const graph = (0, wiki_graph_1.buildWikiGraph)(pages);
|
|
65
|
+
const depths = (0, wiki_graph_1.wikiRouterDepths)(graph);
|
|
66
|
+
const conceptByFile = new Map(pages.map((page) => [page.file, (0, wiki_concepts_1.conceptFromPage)(page.file, page.text)]));
|
|
67
|
+
const edges = visualizerEdges(graph, pages);
|
|
68
|
+
const nodes = pages.map((page) => {
|
|
69
|
+
const concept = conceptByFile.get(page.file);
|
|
70
|
+
const incomingCount = uniqueSorted((graph.incomingLinks.get(page.file) ?? []).map((link) => link.file)).length;
|
|
71
|
+
const outgoingCount = uniqueSorted((graph.outgoingLinks.get(page.file) ?? []).map((link) => link.normalizedTarget).filter((target) => graph.files.has(target))).length;
|
|
72
|
+
return {
|
|
73
|
+
brokenLinks: brokenLinksFor(graph, page.file),
|
|
74
|
+
budget: concept.budget,
|
|
75
|
+
decisionRefCount: graph.incomingDecisionRefs.get(page.file)?.length ?? 0,
|
|
76
|
+
description: concept.description,
|
|
77
|
+
file: page.file,
|
|
78
|
+
incomingCount,
|
|
79
|
+
outgoingCount,
|
|
80
|
+
reviewTrigger: concept.reviewTrigger,
|
|
81
|
+
routerDepth: depths.get(page.file) ?? null,
|
|
82
|
+
scope: concept.scope,
|
|
83
|
+
status: concept.status,
|
|
84
|
+
timestamp: concept.timestamp,
|
|
85
|
+
title: concept.title,
|
|
86
|
+
type: concept.type,
|
|
87
|
+
};
|
|
88
|
+
}).sort((a, b) => a.file.localeCompare(b.file));
|
|
89
|
+
return {
|
|
90
|
+
generatedAt,
|
|
91
|
+
nodes,
|
|
92
|
+
edges,
|
|
93
|
+
summary: {
|
|
94
|
+
brokenCount: nodes.filter((node) => node.brokenLinks.length > 0).length,
|
|
95
|
+
edgeCount: edges.length,
|
|
96
|
+
nodeCount: nodes.length,
|
|
97
|
+
orphanCount: nodes.filter((node) => node.incomingCount === 0 && node.outgoingCount === 0).length,
|
|
98
|
+
typeCount: new Set(nodes.map((node) => node.type)).size,
|
|
99
|
+
unreachableCount: nodes.filter((node) => node.routerDepth === null).length,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function jsonForHtml(value) {
|
|
104
|
+
return JSON.stringify(value).replace(/[<\u2028\u2029]/g, (ch) => "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
105
|
+
}
|
|
106
|
+
function renderWikiVisualizerHtml(payload) {
|
|
107
|
+
const payloadJson = jsonForHtml(payload);
|
|
108
|
+
return `<!doctype html>
|
|
109
|
+
<html lang="en">
|
|
110
|
+
<head>
|
|
111
|
+
<meta charset="utf-8">
|
|
112
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
113
|
+
<title>Project Librarian Wiki Graph</title>
|
|
114
|
+
<style>
|
|
115
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f8fa; color: #16202a; }
|
|
116
|
+
* { box-sizing: border-box; }
|
|
117
|
+
body { margin: 0; min-height: 100vh; }
|
|
118
|
+
header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 20px; border-bottom: 1px solid #d9dee7; background: #ffffff; }
|
|
119
|
+
h1 { margin: 0; font-size: 18px; font-weight: 700; }
|
|
120
|
+
main { display: grid; grid-template-columns: minmax(280px, 360px) 1fr minmax(280px, 380px); height: calc(100vh - 65px); }
|
|
121
|
+
aside, section { min-width: 0; }
|
|
122
|
+
.panel { border-right: 1px solid #d9dee7; background: #ffffff; padding: 16px; overflow: auto; }
|
|
123
|
+
.detail { border-left: 1px solid #d9dee7; border-right: 0; }
|
|
124
|
+
.summary { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: #516070; }
|
|
125
|
+
.summary span { padding: 4px 7px; border: 1px solid #cfd6e1; border-radius: 6px; background: #f4f6f9; }
|
|
126
|
+
.summary span.alert { border-color: #e0a3a3; background: #fbecec; color: #9a3b3b; }
|
|
127
|
+
label { display: block; margin: 14px 0 6px; font-size: 12px; font-weight: 700; color: #394756; }
|
|
128
|
+
input, select { width: 100%; padding: 8px 10px; border: 1px solid #bdc7d4; border-radius: 6px; background: #ffffff; font: inherit; font-size: 14px; }
|
|
129
|
+
.list-meta { margin-top: 16px; font-size: 12px; color: #667484; }
|
|
130
|
+
.panel.nav { overflow: hidden; display: flex; flex-direction: column; }
|
|
131
|
+
.list { margin-top: 10px; display: grid; gap: 8px; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
132
|
+
button.node { width: 100%; text-align: left; border: 1px solid #d8dee8; border-radius: 8px; background: #ffffff; padding: 10px; cursor: pointer; }
|
|
133
|
+
button.node[aria-current="true"] { border-color: #2474b7; box-shadow: 0 0 0 2px rgba(36, 116, 183, 0.14); }
|
|
134
|
+
.node-title { display: block; font-size: 14px; font-weight: 700; color: #152436; overflow-wrap: anywhere; }
|
|
135
|
+
.node-meta { display: block; margin-top: 4px; font-size: 12px; color: #667484; overflow-wrap: anywhere; }
|
|
136
|
+
.tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
137
|
+
.tag { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 999px; border: 1px solid; }
|
|
138
|
+
.tag.unreachable { color: #9a3b3b; border-color: #e0a3a3; background: #fbecec; }
|
|
139
|
+
.tag.orphan { color: #8a6d1b; border-color: #e3cd8f; background: #faf3df; }
|
|
140
|
+
.tag.broken { color: #9a3b3b; border-color: #e0a3a3; background: #fbecec; }
|
|
141
|
+
.graph-wrap { position: relative; overflow: hidden; background: #f7f8fa; }
|
|
142
|
+
svg { width: 100%; height: calc(100vh - 65px); display: block; touch-action: none; cursor: grab; }
|
|
143
|
+
svg.panning { cursor: grabbing; }
|
|
144
|
+
.node { cursor: pointer; }
|
|
145
|
+
.edge { stroke: #aeb8c5; stroke-width: 1.2; marker-end: url(#arrow); vector-effect: non-scaling-stroke; }
|
|
146
|
+
.edge.decision_ref { stroke: #9b5c8f; stroke-dasharray: 5 4; }
|
|
147
|
+
.edge.dim { opacity: 0.1; }
|
|
148
|
+
.edge.hi { stroke: #17212b; stroke-width: 2; opacity: 1; }
|
|
149
|
+
.edge.path { stroke: #d08700; stroke-width: 2.6; opacity: 1; }
|
|
150
|
+
.point { stroke: #ffffff; stroke-width: 2; cursor: pointer; }
|
|
151
|
+
.point.selected { stroke: #17212b; stroke-width: 3; }
|
|
152
|
+
.point.unreachable { stroke: #cc4b4b; stroke-dasharray: 3 2; }
|
|
153
|
+
.point.dim { opacity: 0.18; }
|
|
154
|
+
.badge { fill: #cc4b4b; pointer-events: none; }
|
|
155
|
+
.label { font-size: 11px; fill: #2b3848; paint-order: stroke; stroke: #f7f8fa; stroke-width: 4; stroke-linejoin: round; pointer-events: none; }
|
|
156
|
+
.label.hidden { display: none; }
|
|
157
|
+
.controls { position: absolute; right: 12px; top: 12px; display: flex; gap: 6px; }
|
|
158
|
+
.controls button { width: 30px; height: 30px; border: 1px solid #cfd6e1; border-radius: 6px; background: #ffffff; cursor: pointer; font-size: 16px; line-height: 1; color: #2b3848; }
|
|
159
|
+
.controls button:hover { border-color: #2474b7; }
|
|
160
|
+
.topbar { position: absolute; top: 12px; left: 12px; z-index: 2; display: flex; align-items: center; gap: 8px; background: rgba(255, 255, 255, 0.93); border: 1px solid #d9dee7; border-radius: 8px; padding: 6px 10px; }
|
|
161
|
+
.topbar label { display: inline; margin: 0; font-size: 12px; color: #394756; }
|
|
162
|
+
.topbar select { width: auto; }
|
|
163
|
+
.legend { position: absolute; left: 12px; bottom: 12px; max-width: calc(100% - 24px); background: rgba(255, 255, 255, 0.93); border: 1px solid #d9dee7; border-radius: 8px; padding: 8px 10px; display: flex; flex-wrap: wrap; gap: 6px 12px; font-size: 11px; color: #435367; }
|
|
164
|
+
.legend .k { display: flex; align-items: center; gap: 5px; }
|
|
165
|
+
.legend .sw { width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 0 0 1px #cfd6e1; }
|
|
166
|
+
.legend .sw.ring { background: #ffffff; border: 1.5px dashed #cc4b4b; box-shadow: none; }
|
|
167
|
+
.legend .sw.dot { background: #cc4b4b; }
|
|
168
|
+
.empty { position: absolute; inset: 0; display: grid; place-items: center; color: #617083; pointer-events: none; }
|
|
169
|
+
.empty[hidden] { display: none; }
|
|
170
|
+
.detail h2 { margin: 0 0 8px; font-size: 18px; overflow-wrap: anywhere; }
|
|
171
|
+
.detail dl { display: grid; grid-template-columns: 92px 1fr; gap: 8px 10px; margin: 16px 0; font-size: 13px; }
|
|
172
|
+
.detail dt { font-weight: 700; color: #435367; }
|
|
173
|
+
.detail dd { margin: 0; color: #1d2a36; overflow-wrap: anywhere; }
|
|
174
|
+
.links { margin: 8px 0 0; padding: 0; list-style: none; display: grid; gap: 6px; }
|
|
175
|
+
button.link { display: block; width: 100%; text-align: left; padding: 6px 8px; border: 1px solid #d9dee7; border-radius: 6px; background: #f8fafc; cursor: pointer; font: inherit; font-size: 13px; overflow-wrap: anywhere; }
|
|
176
|
+
button.link:hover { border-color: #2474b7; }
|
|
177
|
+
.kind { color: #7a8696; font-size: 11px; }
|
|
178
|
+
.links li { padding: 6px 8px; border: 1px solid #e0a3a3; border-radius: 6px; background: #fbecec; font-size: 13px; overflow-wrap: anywhere; }
|
|
179
|
+
@media (max-width: 960px) { main { grid-template-columns: 1fr; height: auto; } .panel, .detail { border: 0; border-bottom: 1px solid #d9dee7; max-height: 44vh; } .panel.nav { display: block; overflow: auto; } .list { flex: none; overflow: visible; } svg { height: 56vh; } }
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<header>
|
|
184
|
+
<h1>Project Librarian Wiki Graph</h1>
|
|
185
|
+
<div class="summary" id="summary"></div>
|
|
186
|
+
</header>
|
|
187
|
+
<main>
|
|
188
|
+
<aside class="panel nav">
|
|
189
|
+
<label for="search">Search</label>
|
|
190
|
+
<input id="search" autocomplete="off" placeholder="Title, path, type, scope">
|
|
191
|
+
<label for="group">Group by</label>
|
|
192
|
+
<select id="group">
|
|
193
|
+
<option value="type">Concept type</option>
|
|
194
|
+
<option value="section">Section / folder</option>
|
|
195
|
+
<option value="depth">Router depth</option>
|
|
196
|
+
<option value="community">Link community</option>
|
|
197
|
+
</select>
|
|
198
|
+
<label for="type">Type</label>
|
|
199
|
+
<select id="type"></select>
|
|
200
|
+
<label for="hygiene">Hygiene</label>
|
|
201
|
+
<select id="hygiene">
|
|
202
|
+
<option value="all">All pages</option>
|
|
203
|
+
<option value="unreachable">Unreachable</option>
|
|
204
|
+
<option value="orphan">Orphan (no links)</option>
|
|
205
|
+
<option value="broken">Broken links</option>
|
|
206
|
+
</select>
|
|
207
|
+
<div class="list-meta" id="list-meta"></div>
|
|
208
|
+
<div class="list" id="list"></div>
|
|
209
|
+
</aside>
|
|
210
|
+
<section class="graph-wrap">
|
|
211
|
+
<div class="topbar">
|
|
212
|
+
<label for="depth">Router depth</label>
|
|
213
|
+
<select id="depth">
|
|
214
|
+
<option value="all">All depths</option>
|
|
215
|
+
<option value="reachable">Reachable</option>
|
|
216
|
+
<option value="unreachable">Unreachable</option>
|
|
217
|
+
</select>
|
|
218
|
+
</div>
|
|
219
|
+
<svg id="graph" role="img" aria-label="Wiki graph"></svg>
|
|
220
|
+
<div class="controls">
|
|
221
|
+
<button id="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
222
|
+
<button id="zoom-out" title="Zoom out" aria-label="Zoom out">−</button>
|
|
223
|
+
<button id="zoom-reset" title="Reset view" aria-label="Reset view">⊕</button>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="legend" id="legend"></div>
|
|
226
|
+
<div class="empty" id="empty" hidden>No matching nodes</div>
|
|
227
|
+
</section>
|
|
228
|
+
<aside class="panel detail" id="detail"></aside>
|
|
229
|
+
</main>
|
|
230
|
+
<script id="wiki-graph-data" type="application/json">${payloadJson}</script>
|
|
231
|
+
<script>
|
|
232
|
+
var payload = JSON.parse(document.getElementById("wiki-graph-data").textContent);
|
|
233
|
+
var state = { selected: "", query: "", type: "all", depth: "all", group: "type", hygiene: "all" };
|
|
234
|
+
var view = { x: 0, y: 0, k: 1 };
|
|
235
|
+
var neighborSet = new Set();
|
|
236
|
+
var dragging = false, last = null, down = null, moved = false, downOnPoint = false;
|
|
237
|
+
var HUB = 8;
|
|
238
|
+
|
|
239
|
+
function escapeHtml(value) { return String(value == null ? "" : value).replace(/[&<>"']/g, function (char) { return ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]; }); }
|
|
240
|
+
function escapeAttr(value) { return escapeHtml(value).replace(/"/g, """); }
|
|
241
|
+
|
|
242
|
+
var colorByType = new Map();
|
|
243
|
+
var palette = ["#2f6f9f", "#5b7f3a", "#9a5f27", "#7b5aa6", "#bd4b4b", "#327d77", "#5d6f86", "#8b6f2b", "#6d7892"];
|
|
244
|
+
function color(type) { if (!colorByType.has(type)) colorByType.set(type, palette[colorByType.size % palette.length]); return colorByType.get(type); }
|
|
245
|
+
var sortedTypes = Array.from(new Set(payload.nodes.map(function (n) { return n.type; }))).sort();
|
|
246
|
+
sortedTypes.forEach(function (t) { color(t); });
|
|
247
|
+
|
|
248
|
+
var byFile = new Map(payload.nodes.map(function (n) { return [n.file, n]; }));
|
|
249
|
+
|
|
250
|
+
function isUnreachable(n) { return n.routerDepth === null; }
|
|
251
|
+
function isOrphan(n) { return n.incomingCount === 0 && n.outgoingCount === 0; }
|
|
252
|
+
function isBroken(n) { return (n.brokenLinks || []).length > 0; }
|
|
253
|
+
function degreeOf(n) { return n.incomingCount + n.outgoingCount; }
|
|
254
|
+
function sectionOf(file) { var parts = String(file).split("/"); return parts.length > 2 ? parts[1] : "(root)"; }
|
|
255
|
+
|
|
256
|
+
function computeCommunities(nodes, edges) {
|
|
257
|
+
var adj = new Map();
|
|
258
|
+
nodes.forEach(function (n) { adj.set(n.file, []); });
|
|
259
|
+
edges.forEach(function (e) { if (adj.has(e.source) && adj.has(e.target)) { adj.get(e.source).push(e.target); adj.get(e.target).push(e.source); } });
|
|
260
|
+
var label = new Map();
|
|
261
|
+
nodes.forEach(function (n) { label.set(n.file, n.file); });
|
|
262
|
+
var order = nodes.map(function (n) { return n.file; }).sort();
|
|
263
|
+
for (var iter = 0; iter < 12; iter++) {
|
|
264
|
+
var changed = false;
|
|
265
|
+
for (var oi = 0; oi < order.length; oi++) {
|
|
266
|
+
var f = order[oi];
|
|
267
|
+
var nb = adj.get(f);
|
|
268
|
+
if (!nb.length) continue;
|
|
269
|
+
var counts = new Map();
|
|
270
|
+
for (var j = 0; j < nb.length; j++) { var l = label.get(nb[j]); counts.set(l, (counts.get(l) || 0) + 1); }
|
|
271
|
+
var pairs = Array.from(counts.keys()).sort().map(function (k) { return [k, counts.get(k)]; });
|
|
272
|
+
var best = label.get(f), bestC = -1;
|
|
273
|
+
for (var p = 0; p < pairs.length; p++) { if (pairs[p][1] > bestC) { bestC = pairs[p][1]; best = pairs[p][0]; } }
|
|
274
|
+
if (best !== label.get(f)) { label.set(f, best); changed = true; }
|
|
275
|
+
}
|
|
276
|
+
if (!changed) break;
|
|
277
|
+
}
|
|
278
|
+
return label;
|
|
279
|
+
}
|
|
280
|
+
var community = computeCommunities(payload.nodes, payload.edges);
|
|
281
|
+
function groupKey(n) {
|
|
282
|
+
if (state.group === "section") return sectionOf(n.file);
|
|
283
|
+
if (state.group === "depth") return isUnreachable(n) ? "unreachable" : "depth " + n.routerDepth;
|
|
284
|
+
if (state.group === "community") return community.get(n.file) || "-";
|
|
285
|
+
return n.type;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
var summary = document.getElementById("summary");
|
|
289
|
+
function chip(text, alert) { return '<span class="' + (alert ? "alert" : "") + '">' + escapeHtml(text) + "</span>"; }
|
|
290
|
+
summary.innerHTML = [
|
|
291
|
+
chip(payload.summary.nodeCount + " nodes", false),
|
|
292
|
+
chip(payload.summary.edgeCount + " edges", false),
|
|
293
|
+
chip(payload.summary.typeCount + " types", false),
|
|
294
|
+
chip(payload.summary.unreachableCount + " unreachable", payload.summary.unreachableCount > 0),
|
|
295
|
+
chip(payload.summary.orphanCount + " orphan", payload.summary.orphanCount > 0),
|
|
296
|
+
chip(payload.summary.brokenCount + " broken", payload.summary.brokenCount > 0),
|
|
297
|
+
chip("Generated " + payload.generatedAt, false)
|
|
298
|
+
].join("");
|
|
299
|
+
|
|
300
|
+
var typeSelect = document.getElementById("type");
|
|
301
|
+
typeSelect.innerHTML = '<option value="all">All types</option>' + sortedTypes.map(function (type) { return '<option value="' + escapeAttr(type) + '">' + escapeHtml(type) + "</option>"; }).join("");
|
|
302
|
+
|
|
303
|
+
var legend = document.getElementById("legend");
|
|
304
|
+
legend.innerHTML = sortedTypes.map(function (t) { return '<span class="k"><span class="sw" style="background:' + color(t) + '"></span>' + escapeHtml(t) + "</span>"; }).join("") +
|
|
305
|
+
'<span class="k"><span class="sw ring"></span>unreachable</span>' +
|
|
306
|
+
'<span class="k"><span class="sw dot"></span>broken link</span>';
|
|
307
|
+
|
|
308
|
+
document.getElementById("search").addEventListener("input", function (event) { state.query = event.target.value.toLowerCase(); render(); });
|
|
309
|
+
typeSelect.addEventListener("change", function (event) { state.type = event.target.value; render(); });
|
|
310
|
+
document.getElementById("depth").addEventListener("change", function (event) { state.depth = event.target.value; render(); });
|
|
311
|
+
document.getElementById("group").addEventListener("change", function (event) { state.group = event.target.value; view = { x: 0, y: 0, k: 1 }; render(); });
|
|
312
|
+
document.getElementById("hygiene").addEventListener("change", function (event) { state.hygiene = event.target.value; render(); });
|
|
313
|
+
document.getElementById("zoom-in").addEventListener("click", function () { zoomAtCenter(1.25); });
|
|
314
|
+
document.getElementById("zoom-out").addEventListener("click", function () { zoomAtCenter(1 / 1.25); });
|
|
315
|
+
document.getElementById("zoom-reset").addEventListener("click", function () { view = { x: 0, y: 0, k: 1 }; applyView(); });
|
|
316
|
+
|
|
317
|
+
function matchHygiene(n) {
|
|
318
|
+
if (state.hygiene === "unreachable") return isUnreachable(n);
|
|
319
|
+
if (state.hygiene === "orphan") return isOrphan(n);
|
|
320
|
+
if (state.hygiene === "broken") return isBroken(n);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
function filteredNodes() {
|
|
324
|
+
return payload.nodes.filter(function (node) {
|
|
325
|
+
var haystack = [node.file, node.title, node.type, node.scope, node.description].join(" ").toLowerCase();
|
|
326
|
+
var typeOk = state.type === "all" || node.type === state.type;
|
|
327
|
+
var depthOk = state.depth === "all" || (state.depth === "reachable" ? node.routerDepth !== null : node.routerDepth === null);
|
|
328
|
+
return typeOk && depthOk && matchHygiene(node) && (!state.query || haystack.includes(state.query));
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function ringLayout(nodes, width, height) {
|
|
333
|
+
var centerX = width / 2, centerY = height / 2;
|
|
334
|
+
var byDepth = new Map();
|
|
335
|
+
nodes.forEach(function (node) { var key = node.routerDepth === null ? "unreachable" : String(node.routerDepth); byDepth.set(key, (byDepth.get(key) || []).concat([node])); });
|
|
336
|
+
var positions = new Map();
|
|
337
|
+
var rings = Array.from(byDepth.keys()).sort(function (a, b) { return (a === "unreachable" ? 999 : Number(a)) - (b === "unreachable" ? 999 : Number(b)); });
|
|
338
|
+
rings.forEach(function (ring, ringIndex) {
|
|
339
|
+
var items = byDepth.get(ring);
|
|
340
|
+
var radius = ring === "0" ? 0 : Math.min(width, height) * (0.13 + ringIndex * 0.09);
|
|
341
|
+
items.forEach(function (node, index) {
|
|
342
|
+
var angle = items.length === 1 ? -Math.PI / 2 : (Math.PI * 2 * index) / items.length - Math.PI / 2;
|
|
343
|
+
positions.set(node.file, { x: centerX + Math.cos(angle) * radius, y: centerY + Math.sin(angle) * radius });
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
return positions;
|
|
347
|
+
}
|
|
348
|
+
function clusterLayout(nodes, width, height) {
|
|
349
|
+
var groups = new Map();
|
|
350
|
+
nodes.forEach(function (n) { var k = groupKey(n); if (!groups.has(k)) groups.set(k, []); groups.get(k).push(n); });
|
|
351
|
+
var keys = Array.from(groups.keys()).sort();
|
|
352
|
+
var cx = width / 2, cy = height / 2;
|
|
353
|
+
var R = Math.min(width, height) * 0.38;
|
|
354
|
+
var positions = new Map();
|
|
355
|
+
keys.forEach(function (k, gi) {
|
|
356
|
+
var items = groups.get(k).slice().sort(function (a, b) { return a.file < b.file ? -1 : 1; });
|
|
357
|
+
var ga = keys.length === 1 ? 0 : (Math.PI * 2 * gi) / keys.length - Math.PI / 2;
|
|
358
|
+
var gx = keys.length === 1 ? cx : cx + Math.cos(ga) * R;
|
|
359
|
+
var gy = keys.length === 1 ? cy : cy + Math.sin(ga) * R;
|
|
360
|
+
items.forEach(function (n, i) { var a = i * 2.399963229; var rr = 15 * Math.sqrt(i); positions.set(n.file, { x: gx + Math.cos(a) * rr, y: gy + Math.sin(a) * rr }); });
|
|
361
|
+
});
|
|
362
|
+
return positions;
|
|
363
|
+
}
|
|
364
|
+
function layout(nodes, width, height) { return state.group === "depth" ? ringLayout(nodes, width, height) : clusterLayout(nodes, width, height); }
|
|
365
|
+
|
|
366
|
+
function rootFile() { var r = payload.nodes.find(function (n) { return n.routerDepth === 0; }); return r ? r.file : "wiki/startup.md"; }
|
|
367
|
+
function pathTo(target) {
|
|
368
|
+
var root = rootFile();
|
|
369
|
+
if (!target || !byFile.has(target) || target === root) return [];
|
|
370
|
+
var adj = new Map();
|
|
371
|
+
payload.edges.forEach(function (e) { if (!adj.has(e.source)) adj.set(e.source, []); adj.get(e.source).push(e.target); });
|
|
372
|
+
var queue = [root], prev = new Map([[root, null]]);
|
|
373
|
+
while (queue.length) { var c = queue.shift(); if (c === target) break; var nx = adj.get(c) || []; for (var i = 0; i < nx.length; i++) { if (!prev.has(nx[i])) { prev.set(nx[i], c); queue.push(nx[i]); } } }
|
|
374
|
+
if (!prev.has(target)) return [];
|
|
375
|
+
var pathNodes = [], cur = target;
|
|
376
|
+
while (cur != null) { pathNodes.unshift(cur); cur = prev.get(cur); }
|
|
377
|
+
return pathNodes;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function applyView() {
|
|
381
|
+
var vp = document.getElementById("viewport");
|
|
382
|
+
if (!vp) return;
|
|
383
|
+
vp.setAttribute("transform", "translate(" + view.x + "," + view.y + ") scale(" + view.k + ")");
|
|
384
|
+
var inv = 1 / view.k;
|
|
385
|
+
var groups = vp.querySelectorAll("g.node");
|
|
386
|
+
for (var i = 0; i < groups.length; i++) {
|
|
387
|
+
var g = groups[i];
|
|
388
|
+
g.setAttribute("transform", "translate(" + g.getAttribute("data-x") + "," + g.getAttribute("data-y") + ") scale(" + inv + ")");
|
|
389
|
+
}
|
|
390
|
+
updateLOD();
|
|
391
|
+
}
|
|
392
|
+
function updateLOD() {
|
|
393
|
+
var showAll = view.k >= 1.8;
|
|
394
|
+
var labels = document.querySelectorAll("text.label");
|
|
395
|
+
for (var i = 0; i < labels.length; i++) {
|
|
396
|
+
var t = labels[i];
|
|
397
|
+
var deg = Number(t.getAttribute("data-deg")) || 0;
|
|
398
|
+
var f = t.getAttribute("data-file");
|
|
399
|
+
var keep = showAll || deg >= HUB || f === state.selected || neighborSet.has(f);
|
|
400
|
+
t.classList.toggle("hidden", !keep);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function zoomAtCenter(factor) {
|
|
404
|
+
var svg = document.getElementById("graph");
|
|
405
|
+
var rect = svg.getBoundingClientRect();
|
|
406
|
+
zoomAt(rect.width / 2, rect.height / 2, factor);
|
|
407
|
+
}
|
|
408
|
+
function zoomAt(mx, my, factor) {
|
|
409
|
+
var nk = Math.min(6, Math.max(0.3, view.k * factor));
|
|
410
|
+
var r = nk / view.k;
|
|
411
|
+
view.x = mx - r * (mx - view.x);
|
|
412
|
+
view.y = my - r * (my - view.y);
|
|
413
|
+
view.k = nk;
|
|
414
|
+
applyView();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderGraph(nodes) {
|
|
418
|
+
var svg = document.getElementById("graph");
|
|
419
|
+
var empty = document.getElementById("empty");
|
|
420
|
+
var width = svg.clientWidth || 900;
|
|
421
|
+
var height = svg.clientHeight || 640;
|
|
422
|
+
var files = new Set(nodes.map(function (node) { return node.file; }));
|
|
423
|
+
var positions = layout(nodes, width, height);
|
|
424
|
+
var edges = payload.edges.filter(function (edge) { return files.has(edge.source) && files.has(edge.target); });
|
|
425
|
+
empty.hidden = nodes.length > 0;
|
|
426
|
+
|
|
427
|
+
var focusOn = state.selected && byFile.has(state.selected) && files.has(state.selected);
|
|
428
|
+
neighborSet = new Set();
|
|
429
|
+
var pathSet = new Set();
|
|
430
|
+
if (focusOn) {
|
|
431
|
+
edges.forEach(function (e) { if (e.source === state.selected) neighborSet.add(e.target); if (e.target === state.selected) neighborSet.add(e.source); });
|
|
432
|
+
neighborSet.add(state.selected);
|
|
433
|
+
var p = pathTo(state.selected);
|
|
434
|
+
for (var i = 0; i + 1 < p.length; i++) pathSet.add(p[i] + "->" + p[i + 1]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
var defs = '<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#aeb8c5"></path></marker></defs>';
|
|
438
|
+
var edgeMarkup = edges.map(function (edge) {
|
|
439
|
+
var source = positions.get(edge.source), target = positions.get(edge.target);
|
|
440
|
+
if (!source || !target) return "";
|
|
441
|
+
var cls = "edge " + edge.kind;
|
|
442
|
+
if (focusOn) {
|
|
443
|
+
if (pathSet.has(edge.source + "->" + edge.target)) cls += " path";
|
|
444
|
+
else if (edge.source === state.selected || edge.target === state.selected) cls += " hi";
|
|
445
|
+
else cls += " dim";
|
|
446
|
+
}
|
|
447
|
+
return '<line class="' + cls + '" x1="' + source.x + '" y1="' + source.y + '" x2="' + target.x + '" y2="' + target.y + '"></line>';
|
|
448
|
+
}).join("");
|
|
449
|
+
var nodeMarkup = nodes.map(function (node) {
|
|
450
|
+
var point = positions.get(node.file);
|
|
451
|
+
if (!point) return "";
|
|
452
|
+
var cls = "point";
|
|
453
|
+
if (node.file === state.selected) cls += " selected";
|
|
454
|
+
if (isUnreachable(node)) cls += " unreachable";
|
|
455
|
+
if (focusOn && !neighborSet.has(node.file)) cls += " dim";
|
|
456
|
+
var markup = '<g class="node" data-file="' + escapeAttr(node.file) + '" data-x="' + point.x + '" data-y="' + point.y + '">';
|
|
457
|
+
markup += '<circle class="' + cls + '" r="8" fill="' + color(node.type) + '"></circle>';
|
|
458
|
+
if (isBroken(node)) markup += '<circle class="badge" cx="6" cy="-6" r="3"></circle>';
|
|
459
|
+
markup += '<text class="label" data-file="' + escapeAttr(node.file) + '" data-deg="' + degreeOf(node) + '" x="11" y="4">' + escapeHtml(node.title.slice(0, 34)) + "</text></g>";
|
|
460
|
+
return markup;
|
|
461
|
+
}).join("");
|
|
462
|
+
svg.innerHTML = defs + '<g id="viewport">' + edgeMarkup + nodeMarkup + "</g>";
|
|
463
|
+
svg.querySelectorAll("g.node").forEach(function (g) { g.addEventListener("click", function () { state.selected = g.dataset.file; render(); }); });
|
|
464
|
+
applyView();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function tagsFor(node) {
|
|
468
|
+
var tags = "";
|
|
469
|
+
if (isUnreachable(node)) tags += '<span class="tag unreachable">unreachable</span>';
|
|
470
|
+
if (isOrphan(node)) tags += '<span class="tag orphan">orphan</span>';
|
|
471
|
+
if (isBroken(node)) tags += '<span class="tag broken">broken ' + node.brokenLinks.length + "</span>";
|
|
472
|
+
return tags ? '<span class="tags">' + tags + "</span>" : "";
|
|
473
|
+
}
|
|
474
|
+
function renderList(nodes) {
|
|
475
|
+
var list = document.getElementById("list");
|
|
476
|
+
var meta = document.getElementById("list-meta");
|
|
477
|
+
meta.textContent = nodes.length + " / " + payload.nodes.length + " pages" + (nodes.length > 200 ? " (showing first 200)" : "");
|
|
478
|
+
list.innerHTML = nodes.slice(0, 200).map(function (node) {
|
|
479
|
+
return '<button class="node" aria-current="' + (node.file === state.selected ? "true" : "false") + '" data-file="' + escapeAttr(node.file) + '"><span class="node-title">' + escapeHtml(node.title) + '</span><span class="node-meta">' + escapeHtml(node.type + " - " + node.file) + "</span>" + tagsFor(node) + "</button>";
|
|
480
|
+
}).join("");
|
|
481
|
+
list.querySelectorAll("button.node").forEach(function (button) { button.addEventListener("click", function () { state.selected = button.dataset.file; render(); }); });
|
|
482
|
+
}
|
|
483
|
+
function renderDetail() {
|
|
484
|
+
var node = byFile.get(state.selected) || payload.nodes[0];
|
|
485
|
+
var detail = document.getElementById("detail");
|
|
486
|
+
if (!node) { detail.innerHTML = ""; return; }
|
|
487
|
+
function linkRow(file, kind) { var n = byFile.get(file); return '<button class="link" data-file="' + escapeAttr(file) + '">' + escapeHtml(n ? n.title : file) + ' <span class="kind">' + escapeHtml(kind) + "</span></button>"; }
|
|
488
|
+
var incoming = payload.edges.filter(function (edge) { return edge.target === node.file; }).map(function (edge) { return linkRow(edge.source, edge.kind); });
|
|
489
|
+
var outgoing = payload.edges.filter(function (edge) { return edge.source === node.file; }).map(function (edge) { return linkRow(edge.target, edge.kind); });
|
|
490
|
+
var brokenHtml = (node.brokenLinks || []).length ? '<strong>Broken links</strong><ul class="links">' + node.brokenLinks.map(function (b) { return "<li>" + escapeHtml(b) + "</li>"; }).join("") + "</ul>" : "";
|
|
491
|
+
detail.innerHTML = "<h2>" + escapeHtml(node.title) + "</h2>" + tagsFor(node) + "<p>" + escapeHtml(node.description || "No description") + "</p>" +
|
|
492
|
+
"<dl>" +
|
|
493
|
+
"<dt>Path</dt><dd>" + escapeHtml(node.file) + "</dd>" +
|
|
494
|
+
"<dt>Type</dt><dd>" + escapeHtml(node.type) + "</dd>" +
|
|
495
|
+
"<dt>Scope</dt><dd>" + escapeHtml(node.scope) + "</dd>" +
|
|
496
|
+
"<dt>Status</dt><dd>" + escapeHtml(node.status) + "</dd>" +
|
|
497
|
+
"<dt>Budget</dt><dd>" + escapeHtml(node.budget) + "</dd>" +
|
|
498
|
+
"<dt>Router</dt><dd>" + escapeHtml(node.routerDepth === null ? "unreachable" : "depth " + node.routerDepth) + "</dd>" +
|
|
499
|
+
"<dt>Updated</dt><dd>" + escapeHtml(node.timestamp || "-") + "</dd>" +
|
|
500
|
+
"<dt>Trigger</dt><dd>" + escapeHtml(node.reviewTrigger || "-") + "</dd>" +
|
|
501
|
+
"</dl>" +
|
|
502
|
+
"<strong>Incoming</strong><ul class=\\"links\\">" + (incoming.length ? incoming.join("") : "<li>none</li>") + "</ul>" +
|
|
503
|
+
"<strong>Outgoing</strong><ul class=\\"links\\">" + (outgoing.length ? outgoing.join("") : "<li>none</li>") + "</ul>" +
|
|
504
|
+
brokenHtml;
|
|
505
|
+
detail.querySelectorAll("button.link").forEach(function (button) { button.addEventListener("click", function () { state.selected = button.dataset.file; render(); }); });
|
|
506
|
+
}
|
|
507
|
+
function render() {
|
|
508
|
+
var nodes = filteredNodes();
|
|
509
|
+
if (state.selected && !nodes.some(function (node) { return node.file === state.selected; })) state.selected = "";
|
|
510
|
+
renderList(nodes);
|
|
511
|
+
renderGraph(nodes);
|
|
512
|
+
renderDetail();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
var svgEl = document.getElementById("graph");
|
|
516
|
+
svgEl.addEventListener("wheel", function (e) { e.preventDefault(); var rect = svgEl.getBoundingClientRect(); zoomAt(e.clientX - rect.left, e.clientY - rect.top, e.deltaY < 0 ? 1.12 : 1 / 1.12); }, { passive: false });
|
|
517
|
+
svgEl.addEventListener("pointerdown", function (e) {
|
|
518
|
+
down = { x: e.clientX, y: e.clientY }; moved = false;
|
|
519
|
+
downOnPoint = !!(e.target.closest && e.target.closest("g.node"));
|
|
520
|
+
if (!downOnPoint) { dragging = true; last = { x: e.clientX, y: e.clientY }; svgEl.classList.add("panning"); }
|
|
521
|
+
});
|
|
522
|
+
svgEl.addEventListener("pointermove", function (e) {
|
|
523
|
+
if (down && (Math.abs(e.clientX - down.x) + Math.abs(e.clientY - down.y)) > 4) moved = true;
|
|
524
|
+
if (dragging) { view.x += e.clientX - last.x; view.y += e.clientY - last.y; last = { x: e.clientX, y: e.clientY }; applyView(); }
|
|
525
|
+
});
|
|
526
|
+
svgEl.addEventListener("pointerup", function () {
|
|
527
|
+
if (dragging) { dragging = false; svgEl.classList.remove("panning"); }
|
|
528
|
+
if (!moved && !downOnPoint && state.selected) { state.selected = ""; render(); }
|
|
529
|
+
down = null;
|
|
530
|
+
});
|
|
531
|
+
svgEl.addEventListener("pointerleave", function () { if (dragging) { dragging = false; svgEl.classList.remove("panning"); } });
|
|
532
|
+
window.addEventListener("resize", render);
|
|
533
|
+
render();
|
|
534
|
+
</script>
|
|
535
|
+
</body>
|
|
536
|
+
</html>
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
function normalizedVisualizerOutput(outputPath) {
|
|
540
|
+
const normalized = (0, workspace_1.normalizePath)(path.normalize(outputPath || exports.defaultWikiVisualizerOutput));
|
|
541
|
+
if (path.isAbsolute(outputPath) || normalized.startsWith("../") || normalized === "..") {
|
|
542
|
+
throw new Error(`--wiki-visualize-out must be a repository-relative path under .project-wiki/: ${outputPath}`);
|
|
543
|
+
}
|
|
544
|
+
if (!normalized.startsWith(".project-wiki/")) {
|
|
545
|
+
throw new Error(`--wiki-visualize-out must stay under .project-wiki/: ${outputPath}`);
|
|
546
|
+
}
|
|
547
|
+
return normalized;
|
|
548
|
+
}
|
|
549
|
+
function writeWikiVisualizer(outputPath = exports.defaultWikiVisualizerOutput) {
|
|
550
|
+
const files = (0, wiki_files_1.wikiMarkdownFiles)();
|
|
551
|
+
if (files.length === 0)
|
|
552
|
+
throw new Error("wiki directory has no markdown files; run Project Librarian before --wiki-visualize.");
|
|
553
|
+
const pages = files.map((file) => ({ file, text: (0, workspace_1.read)(file) }));
|
|
554
|
+
const payload = buildWikiVisualizerPayload(pages);
|
|
555
|
+
const relativeOutput = normalizedVisualizerOutput(outputPath);
|
|
556
|
+
(0, workspace_1.write)(relativeOutput, renderWikiVisualizerHtml(payload));
|
|
557
|
+
return (0, workspace_1.normalizePath)(path.relative(workspace_1.root, path.join(workspace_1.root, relativeOutput)));
|
|
558
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-librarian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create and maintain compact project context for humans and LLM coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -28,8 +28,6 @@
|
|
|
28
28
|
"LICENSE",
|
|
29
29
|
"README.md",
|
|
30
30
|
"README.ko.md",
|
|
31
|
-
"README.ja.md",
|
|
32
|
-
"README.zh.md",
|
|
33
31
|
"SKILL.md"
|
|
34
32
|
],
|
|
35
33
|
"bin": {
|
|
@@ -45,7 +43,10 @@
|
|
|
45
43
|
"benchmark:llm": "npm run build && node benchmarks/codex-llm-metrics.js",
|
|
46
44
|
"benchmark:llm:dry-run": "npm run build && node benchmarks/codex-llm-metrics.js --dry-run",
|
|
47
45
|
"benchmark:llm:parse-smoke": "node tests/validators/codex-llm-benchmark-smoke.js",
|
|
48
|
-
"benchmark:
|
|
46
|
+
"benchmark:injection-sentinel": "node benchmarks/tools/injection-sentinel.js",
|
|
47
|
+
"benchmark:real-corpus:demo": "npm run build && node benchmarks/tools/real-corpus-offline-demo.js",
|
|
48
|
+
"benchmark:release:preview": "npm run benchmark:llm -- --payload-preview benchmarks/reports/llm/payload-preview.json --sanitized-pack --full-matrix --runs 3 --warmup-runs 1 --min-runs-for-claim 3 --require-clean --require-claimable --model gpt-5.5",
|
|
49
|
+
"benchmark:release": "npm run benchmark:llm -- --sanitized-pack --full-matrix --runs 3 --warmup-runs 1 --min-runs-for-claim 3 --require-clean --require-claimable --model gpt-5.5 --out benchmarks/reports/llm/current.json --markdown benchmarks/reports/llm/current.md",
|
|
49
50
|
"build": "tsc && chmod +x dist/init-project-wiki.js",
|
|
50
51
|
"typecheck": "tsc --noEmit",
|
|
51
52
|
"unit": "node --test tests/unit/*.test.js",
|