sdtk-wiki-kit 0.1.3 → 0.1.4
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.md +23 -18
- package/package.json +1 -1
- package/src/commands/enrich.js +51 -0
- package/src/commands/help.js +16 -11
- package/src/commands/lint.js +1 -1
- package/src/commands/operations.js +5 -4
- package/src/commands/search.js +5 -4
- package/src/commands/wiki.js +7 -6
- package/src/index.js +4 -0
- package/src/lib/wiki-compile.js +776 -28
- package/src/lib/wiki-enrich.js +264 -0
- package/src/lib/wiki-extract.js +12 -12
- package/src/lib/wiki-lint.js +90 -29
- package/src/lib/wiki-paths.js +55 -0
- package/src/lib/wiki-search.js +17 -10
package/src/lib/wiki-compile.js
CHANGED
|
@@ -4,7 +4,10 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { CliError, ValidationError } = require("./errors");
|
|
6
6
|
const {
|
|
7
|
+
assertCanonicalWikiWritePath,
|
|
7
8
|
assertWikiWorkspaceWritePath,
|
|
9
|
+
getCanonicalWikiPath,
|
|
10
|
+
getLegacyPersonalBrainPath,
|
|
8
11
|
getWikiReportsPath,
|
|
9
12
|
getWikiWorkspacePath,
|
|
10
13
|
isPathInsideOrEqual,
|
|
@@ -15,7 +18,7 @@ const REPORT_PREFIX = "compile-dry-run-preview";
|
|
|
15
18
|
const APPLY_PLAN_PREFIX = "compile-apply-plan";
|
|
16
19
|
const APPLY_PLAN_RECORD_TYPE = "sdtk_wiki_compile_apply_plan";
|
|
17
20
|
const APPLY_PLAN_SCHEMA_VERSION = 1;
|
|
18
|
-
const
|
|
21
|
+
const CANONICAL_WIKI_RELATIVE = "wiki";
|
|
19
22
|
const APPLY_MODE = "create_only_or_same_content_noop";
|
|
20
23
|
const ALLOWED_OPERATION_TYPES = new Set([
|
|
21
24
|
"append_section",
|
|
@@ -138,6 +141,7 @@ function relatedRepoList(entity) {
|
|
|
138
141
|
|
|
139
142
|
function relativePersonalBrainLink(targetPagePath) {
|
|
140
143
|
const text = toPosix(targetPagePath || "");
|
|
144
|
+
if (text.startsWith("wiki/")) return text.slice("wiki/".length);
|
|
141
145
|
const marker = ".sdtk/wiki/personal-brain/";
|
|
142
146
|
const idx = text.indexOf(marker);
|
|
143
147
|
return idx >= 0 ? text.slice(idx + marker.length) : text;
|
|
@@ -204,6 +208,464 @@ function candidateToolList(records) {
|
|
|
204
208
|
}).join("\n");
|
|
205
209
|
}
|
|
206
210
|
|
|
211
|
+
function localCandidateLabel(record) {
|
|
212
|
+
if (!record) return "local candidate";
|
|
213
|
+
if (record.repo_owner && record.repo_name) return `${record.repo_owner}/${record.repo_name}`;
|
|
214
|
+
if (record.owner && (record.repo_name || record.repo)) return `${record.owner}/${record.repo_name || record.repo}`;
|
|
215
|
+
return record.name || record.entity_id || "local candidate";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function candidateEvidenceCount(record) {
|
|
219
|
+
const explicit = Number(record.source_ref_count || record.local_evidence_count || record.evidence_count || 0);
|
|
220
|
+
if (explicit > 0) return explicit;
|
|
221
|
+
const refs = sourceRefsFor(record);
|
|
222
|
+
return refs.length > 0 ? refs.length : 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function topLocalCandidates(records, limit = 3) {
|
|
226
|
+
return asArray(records)
|
|
227
|
+
.map((record) => ({
|
|
228
|
+
record,
|
|
229
|
+
label: localCandidateLabel(record),
|
|
230
|
+
evidence: candidateEvidenceCount(record),
|
|
231
|
+
category: record.category || record.local_category || "uncategorized",
|
|
232
|
+
confidence: record.source_confidence || record.confidence_tier || record.confidence || "unknown",
|
|
233
|
+
topics: asArray(record.topics || record.shared_topics).filter(Boolean).map(String),
|
|
234
|
+
recommendation: record.local_recommendation || record.recommendation || "review",
|
|
235
|
+
summary: record.summary || record.evidence || "",
|
|
236
|
+
}))
|
|
237
|
+
.sort((a, b) => (b.evidence - a.evidence) || a.label.localeCompare(b.label))
|
|
238
|
+
.slice(0, limit);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function refinedCandidateSentence(candidate) {
|
|
242
|
+
const topicText = candidate.topics.length > 0 ? `; local topics: ${candidate.topics.slice(0, 4).join(", ")}` : "";
|
|
243
|
+
return `${candidate.label} (${candidate.category}) has ${candidate.evidence} local evidence reference(s), confidence ${candidate.confidence}${topicText}.`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function conceptDecisionBrief(concept) {
|
|
247
|
+
const title = recordTitle(concept, "Concept");
|
|
248
|
+
const related = asArray(concept.related_entity_details);
|
|
249
|
+
const candidates = topLocalCandidates(related, 3);
|
|
250
|
+
const sourceCount = sourceRefCount(concept);
|
|
251
|
+
const axisNames = asArray(concept.key_axes).map((axis) => axis.name || axis).filter(Boolean).slice(0, 4);
|
|
252
|
+
const lines = [
|
|
253
|
+
`- Local evidence profile: ${related.length} related tool candidate(s), ${sourceCount} source reference(s), ${axisNames.length} decision axis/axes.`,
|
|
254
|
+
];
|
|
255
|
+
if (candidates.length > 0) {
|
|
256
|
+
lines.push(`- First review target: ${refinedCandidateSentence(candidates[0])}`);
|
|
257
|
+
if (candidates.length > 1) {
|
|
258
|
+
lines.push(`- Compare next: ${candidates.slice(1).map((candidate) => `${candidate.label} (${candidate.evidence} local ref(s))`).join("; ")}.`);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
lines.push("- First review target: no concrete tool candidate was linked to this concept in local evidence.");
|
|
262
|
+
}
|
|
263
|
+
if (axisNames.length > 0) {
|
|
264
|
+
lines.push(`- Decision axes to preserve: ${axisNames.join(", ")}.`);
|
|
265
|
+
}
|
|
266
|
+
lines.push(`- Local-only boundary: treat ${title} as a decision queue from local notes, not as proof of current repository health, license, or adoption readiness.`);
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function comparisonDecisionGuidance(comparison) {
|
|
271
|
+
const rows = asArray(comparison.matrix_rows);
|
|
272
|
+
const candidates = topLocalCandidates(rows, 4);
|
|
273
|
+
const axisNames = asArray(comparison.decision_axes).map((axis) => axis.name || axis).filter(Boolean).slice(0, 5);
|
|
274
|
+
const lines = [
|
|
275
|
+
`- Use this comparison to rank ${rows.length} local candidate(s) across ${axisNames.length || "the recorded"} decision axis/axes.`,
|
|
276
|
+
];
|
|
277
|
+
if (candidates.length > 0) {
|
|
278
|
+
lines.push(`- Local shortlist: ${candidates.map((candidate) => `${candidate.label} (${candidate.evidence} ref(s), ${candidate.recommendation})`).join("; ")}.`);
|
|
279
|
+
}
|
|
280
|
+
if (axisNames.length > 0) {
|
|
281
|
+
lines.push(`- Review order: score candidates first on ${axisNames.join(", ")}, then verify external facts separately.`);
|
|
282
|
+
}
|
|
283
|
+
lines.push("- Do not convert a local shortlist into an adoption recommendation until license, maintenance, security, and current repository identity are verified outside this local evidence set.");
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function synthesisDecisionNarrative(synthesis) {
|
|
288
|
+
const rows = asArray(synthesis.candidate_tools);
|
|
289
|
+
const candidates = topLocalCandidates(rows, 4);
|
|
290
|
+
const axisNames = asArray(synthesis.landscape_axes).map((axis) => axis.name || axis).filter(Boolean).slice(0, 5);
|
|
291
|
+
const lines = [
|
|
292
|
+
`- This synthesis frames ${rows.length} local candidate(s) as a review backlog, not a final recommendation.`,
|
|
293
|
+
];
|
|
294
|
+
if (candidates.length > 0) {
|
|
295
|
+
lines.push(`- Best first inspection path: ${candidates.map((candidate) => `${candidate.label} (${candidate.evidence} ref(s), ${candidate.confidence})`).join("; ")}.`);
|
|
296
|
+
}
|
|
297
|
+
if (axisNames.length > 0) {
|
|
298
|
+
lines.push(`- Decision lens: ${axisNames.join(", ")}.`);
|
|
299
|
+
}
|
|
300
|
+
if (synthesis.related_comparison_path) {
|
|
301
|
+
lines.push(`- Use the related comparison page before making a tool choice: ../${relativePersonalBrainLink(synthesis.related_comparison_path)}.`);
|
|
302
|
+
}
|
|
303
|
+
lines.push("- Keep unverified claims labeled: local snippets can explain why a tool entered the queue, but not whether it is currently mature, licensed correctly, or maintained.");
|
|
304
|
+
return lines.join("\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mdEscapeTable(value) {
|
|
308
|
+
return String(value ?? "").replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function rootLink(targetPagePath) {
|
|
312
|
+
const rel = relativePersonalBrainLink(targetPagePath);
|
|
313
|
+
return rel ? toPosix(rel) : "";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function dashboardLink(targetPagePath) {
|
|
317
|
+
const rel = relativePersonalBrainLink(targetPagePath);
|
|
318
|
+
return rel ? `../${toPosix(rel)}` : "";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sourceRefCount(record) {
|
|
322
|
+
return sourceRefsFor(record).length;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function highSignalTools(payload, limit = 12) {
|
|
326
|
+
return asArray(payload.tool_entities)
|
|
327
|
+
.map((tool) => ({
|
|
328
|
+
title: recordTitle(tool, "tool"),
|
|
329
|
+
path: tool.target_page_path,
|
|
330
|
+
github_url: tool.github_url,
|
|
331
|
+
category: tool.category || "uncategorized",
|
|
332
|
+
topics: asArray(tool.topics).filter(Boolean).map(String),
|
|
333
|
+
source_refs: sourceRefCount(tool),
|
|
334
|
+
confidence: tool.confidence_tier || tool.confidence || "medium",
|
|
335
|
+
summary: tool.summary || "",
|
|
336
|
+
}))
|
|
337
|
+
.sort((a, b) => (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
|
|
338
|
+
.slice(0, limit);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function topConcepts(payload, limit = 10) {
|
|
342
|
+
return asArray(payload.concepts)
|
|
343
|
+
.map((concept) => ({
|
|
344
|
+
title: recordTitle(concept, "concept"),
|
|
345
|
+
path: concept.target_page_path,
|
|
346
|
+
related_count: asArray(concept.related_entity_details).length || asArray(concept.related_entities).length,
|
|
347
|
+
source_refs: sourceRefCount(concept),
|
|
348
|
+
axes: asArray(concept.key_axes).map((axis) => axis.name || axis).filter(Boolean),
|
|
349
|
+
}))
|
|
350
|
+
.sort((a, b) => (b.related_count - a.related_count) || (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
|
|
351
|
+
.slice(0, limit);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function comparisonEntries(payload, limit = 10) {
|
|
355
|
+
return asArray(payload.comparisons)
|
|
356
|
+
.map((comparison) => ({
|
|
357
|
+
title: recordTitle(comparison, "comparison"),
|
|
358
|
+
topic: comparison.topic || comparison.comparison_id || recordTitle(comparison, "comparison"),
|
|
359
|
+
path: comparison.target_page_path,
|
|
360
|
+
rows: asArray(comparison.matrix_rows).length,
|
|
361
|
+
source_refs: sourceRefCount(comparison),
|
|
362
|
+
recommendations: asArray(comparison.recommendations),
|
|
363
|
+
}))
|
|
364
|
+
.sort((a, b) => (b.rows - a.rows) || (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
|
|
365
|
+
.slice(0, limit);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function synthesisEntries(payload, limit = 10) {
|
|
369
|
+
return asArray(payload.syntheses)
|
|
370
|
+
.map((synthesis) => ({
|
|
371
|
+
title: recordTitle(synthesis, "synthesis"),
|
|
372
|
+
topic: synthesis.topic || synthesis.synthesis_id || recordTitle(synthesis, "synthesis"),
|
|
373
|
+
path: synthesis.target_page_path,
|
|
374
|
+
candidates: asArray(synthesis.candidate_tools).length,
|
|
375
|
+
source_refs: sourceRefCount(synthesis),
|
|
376
|
+
recommendations: asArray(synthesis.recommendations),
|
|
377
|
+
}))
|
|
378
|
+
.sort((a, b) => (b.candidates - a.candidates) || (b.source_refs - a.source_refs) || a.title.localeCompare(b.title))
|
|
379
|
+
.slice(0, limit);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sourceEntries(payload, limit = 20) {
|
|
383
|
+
return asArray(payload.sources)
|
|
384
|
+
.map((source) => ({
|
|
385
|
+
title: source.source_logical_path || source.source_relative_path || source.source_id || "source",
|
|
386
|
+
path: source.target_page_path,
|
|
387
|
+
source_id: source.source_id,
|
|
388
|
+
quality_flags: asArray(source.source_quality && source.source_quality.quality_flags),
|
|
389
|
+
source_url: source.source_url || "",
|
|
390
|
+
source_hash: source.source_hash || "",
|
|
391
|
+
}))
|
|
392
|
+
.slice(0, limit);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function groupToolsByCategory(tools) {
|
|
396
|
+
const byCategory = new Map();
|
|
397
|
+
for (const tool of tools) {
|
|
398
|
+
const key = tool.category || "uncategorized";
|
|
399
|
+
if (!byCategory.has(key)) byCategory.set(key, []);
|
|
400
|
+
byCategory.get(key).push(tool);
|
|
401
|
+
}
|
|
402
|
+
return [...byCategory.entries()]
|
|
403
|
+
.map(([category, rows]) => ({ category, rows }))
|
|
404
|
+
.sort((a, b) => (b.rows.length - a.rows.length) || a.category.localeCompare(b.category));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function linkedListFromRoot(entries) {
|
|
408
|
+
const rows = asArray(entries).filter((entry) => entry && entry.path);
|
|
409
|
+
if (rows.length === 0) return "- None recorded.";
|
|
410
|
+
return rows.map((entry) => `- [${entry.title}](${rootLink(entry.path)}): ${entry.source_refs || 0} source reference(s).`).join("\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function linkedListFromDashboard(entries) {
|
|
414
|
+
const rows = asArray(entries).filter((entry) => entry && entry.path);
|
|
415
|
+
if (rows.length === 0) return "- None recorded.";
|
|
416
|
+
return rows.map((entry) => `- [${entry.title}](${dashboardLink(entry.path)}): ${entry.source_refs || 0} source reference(s).`).join("\n");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function dashboardFrontmatter(id, title, payload, generatedAt, relatedPages = []) {
|
|
420
|
+
return {
|
|
421
|
+
id,
|
|
422
|
+
title,
|
|
423
|
+
type: "dashboard",
|
|
424
|
+
created_at: generatedAt,
|
|
425
|
+
updated_at: generatedAt,
|
|
426
|
+
aliases: [title],
|
|
427
|
+
tags: ["dashboard", "navigation"],
|
|
428
|
+
related_pages: relatedPages,
|
|
429
|
+
source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
|
|
430
|
+
confidence: "medium",
|
|
431
|
+
review_status: "needs_review",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function renderResearchToolLandscapeDashboard(payload) {
|
|
436
|
+
const tools = highSignalTools(payload, 16);
|
|
437
|
+
const concepts = topConcepts(payload, 8);
|
|
438
|
+
const comparisons = comparisonEntries(payload, 6);
|
|
439
|
+
const syntheses = synthesisEntries(payload, 6);
|
|
440
|
+
const categoryRows = groupToolsByCategory(tools);
|
|
441
|
+
const table = [
|
|
442
|
+
"| Category | Candidate count | High-signal examples |",
|
|
443
|
+
"|---|---:|---|",
|
|
444
|
+
...categoryRows.map(({ category, rows }) => {
|
|
445
|
+
const examples = rows.slice(0, 4).map((tool) => tool.path ? `[${mdEscapeTable(tool.title)}](${dashboardLink(tool.path)})` : mdEscapeTable(tool.title)).join("<br>");
|
|
446
|
+
return `| ${mdEscapeTable(category)} | ${rows.length} | ${examples || "-"} |`;
|
|
447
|
+
}),
|
|
448
|
+
].join("\n");
|
|
449
|
+
const highSignalTable = [
|
|
450
|
+
"| Tool | Category | Local evidence | Topics | Confidence |",
|
|
451
|
+
"|---|---|---:|---|---|",
|
|
452
|
+
...tools.slice(0, 12).map((tool) => {
|
|
453
|
+
const label = tool.path ? `[${mdEscapeTable(tool.title)}](${dashboardLink(tool.path)})` : mdEscapeTable(tool.title);
|
|
454
|
+
return `| ${label} | ${mdEscapeTable(tool.category)} | ${tool.source_refs} | ${mdEscapeTable(tool.topics.slice(0, 4).join(", ") || "-")} | ${mdEscapeTable(tool.confidence)} |`;
|
|
455
|
+
}),
|
|
456
|
+
].join("\n");
|
|
457
|
+
const body = [
|
|
458
|
+
"# Research / Tool Landscape Dashboard",
|
|
459
|
+
"",
|
|
460
|
+
"## Purpose",
|
|
461
|
+
"",
|
|
462
|
+
"Use this page as the starting map for browsing locally extracted repository and tool evidence. The links stay inside the generated local wiki and do not imply external verification.",
|
|
463
|
+
"",
|
|
464
|
+
"## Top Concepts",
|
|
465
|
+
"",
|
|
466
|
+
linkedListFromDashboard(concepts),
|
|
467
|
+
"",
|
|
468
|
+
"## Tool Clusters",
|
|
469
|
+
"",
|
|
470
|
+
table,
|
|
471
|
+
"",
|
|
472
|
+
"## High-Signal Tools",
|
|
473
|
+
"",
|
|
474
|
+
highSignalTable,
|
|
475
|
+
"",
|
|
476
|
+
"## Comparison And Synthesis Entry Points",
|
|
477
|
+
"",
|
|
478
|
+
"Comparisons:",
|
|
479
|
+
"",
|
|
480
|
+
linkedListFromDashboard(comparisons),
|
|
481
|
+
"",
|
|
482
|
+
"Syntheses:",
|
|
483
|
+
"",
|
|
484
|
+
linkedListFromDashboard(syntheses),
|
|
485
|
+
"",
|
|
486
|
+
"## Review Notes",
|
|
487
|
+
"",
|
|
488
|
+
"- Prefer tools with multiple local source references before opening an external verification issue.",
|
|
489
|
+
"- Treat topic/category clustering as local evidence only.",
|
|
490
|
+
"- Use the decision-support dashboard before making adoption recommendations.",
|
|
491
|
+
"",
|
|
492
|
+
].join("\n");
|
|
493
|
+
return withPersonalBrainFrontmatter(
|
|
494
|
+
dashboardFrontmatter(
|
|
495
|
+
"dashboard_research_tool_landscape",
|
|
496
|
+
"Research / Tool Landscape Dashboard",
|
|
497
|
+
payload,
|
|
498
|
+
payload.generated_at || "",
|
|
499
|
+
["../index.md", ...concepts.map((entry) => dashboardLink(entry.path)).filter(Boolean)]
|
|
500
|
+
),
|
|
501
|
+
body
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function renderDecisionSupportDashboard(payload) {
|
|
506
|
+
const comparisons = comparisonEntries(payload, 8);
|
|
507
|
+
const syntheses = synthesisEntries(payload, 8);
|
|
508
|
+
const tools = highSignalTools(payload, 10);
|
|
509
|
+
const comparisonTable = [
|
|
510
|
+
"| Decision surface | Candidates | Local source refs | First recommendation cue |",
|
|
511
|
+
"|---|---:|---:|---|",
|
|
512
|
+
...comparisons.map((entry) => `| [${mdEscapeTable(entry.title)}](${dashboardLink(entry.path)}) | ${entry.rows} | ${entry.source_refs} | ${mdEscapeTable(entry.recommendations[0] || "review candidates")} |`),
|
|
513
|
+
].join("\n");
|
|
514
|
+
const shortlist = [
|
|
515
|
+
"| Candidate | Category | Local evidence | Decision note |",
|
|
516
|
+
"|---|---|---:|---|",
|
|
517
|
+
...tools.slice(0, 8).map((tool) => `| [${mdEscapeTable(tool.title)}](${dashboardLink(tool.path)}) | ${mdEscapeTable(tool.category)} | ${tool.source_refs} | Verify license, maintenance, security, and fit before adoption. |`),
|
|
518
|
+
].join("\n");
|
|
519
|
+
const body = [
|
|
520
|
+
"# Decision-Support Dashboard",
|
|
521
|
+
"",
|
|
522
|
+
"## Purpose",
|
|
523
|
+
"",
|
|
524
|
+
"Use this page when deciding which locally sourced candidates deserve deeper review. It links comparison, synthesis, and high-signal tool pages without fetching external metadata.",
|
|
525
|
+
"",
|
|
526
|
+
"## Comparison Starting Points",
|
|
527
|
+
"",
|
|
528
|
+
comparisons.length > 0 ? comparisonTable : "- No comparison pages were generated from the local evidence.",
|
|
529
|
+
"",
|
|
530
|
+
"## Synthesis Starting Points",
|
|
531
|
+
"",
|
|
532
|
+
linkedListFromDashboard(syntheses),
|
|
533
|
+
"",
|
|
534
|
+
"## Candidate Shortlist",
|
|
535
|
+
"",
|
|
536
|
+
shortlist,
|
|
537
|
+
"",
|
|
538
|
+
"## Decision Checklist",
|
|
539
|
+
"",
|
|
540
|
+
"- Confirm the page has multiple local source references or a strong extracted snippet.",
|
|
541
|
+
"- Check comparison caveats before adoption.",
|
|
542
|
+
"- Verify license, maintainer activity, security posture, and current repository identity outside this local-only report.",
|
|
543
|
+
"- Record human acceptance criteria before treating a tool as recommended.",
|
|
544
|
+
"",
|
|
545
|
+
].join("\n");
|
|
546
|
+
return withPersonalBrainFrontmatter(
|
|
547
|
+
dashboardFrontmatter(
|
|
548
|
+
"dashboard_decision_support",
|
|
549
|
+
"Decision-Support Dashboard",
|
|
550
|
+
payload,
|
|
551
|
+
payload.generated_at || "",
|
|
552
|
+
["../index.md", ...comparisons.map((entry) => dashboardLink(entry.path)).filter(Boolean)]
|
|
553
|
+
),
|
|
554
|
+
body
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function renderMaintenanceQualityDashboard(payload) {
|
|
559
|
+
const findings = asArray(payload.source_quality_findings);
|
|
560
|
+
const unsupported = asArray(payload.unsupported_items);
|
|
561
|
+
const sources = sourceEntries(payload, 10);
|
|
562
|
+
const maintenanceLinks = [];
|
|
563
|
+
if (findings.length > 0 || unsupported.length > 0) {
|
|
564
|
+
maintenanceLinks.push("[Extraction Quality Review](../maintenance/extraction-quality-review.md)");
|
|
565
|
+
}
|
|
566
|
+
const findingList = findings.slice(0, 12).map((finding) =>
|
|
567
|
+
`- ${finding.finding_id || finding.source_id || "finding"}: ${asArray(finding.quality_flags).join(", ") || finding.reason || "review required"}`
|
|
568
|
+
).join("\n") || "- No source-quality findings recorded.";
|
|
569
|
+
const unsupportedList = unsupported.slice(0, 12).map((item) =>
|
|
570
|
+
`- ${item.item_id || "unsupported"}: ${item.reason || item.raw_observation_summary || "review required"}`
|
|
571
|
+
).join("\n") || "- No unsupported extraction items recorded.";
|
|
572
|
+
const body = [
|
|
573
|
+
"# Maintenance / Quality Dashboard",
|
|
574
|
+
"",
|
|
575
|
+
"## Purpose",
|
|
576
|
+
"",
|
|
577
|
+
"Use this page to triage local extraction quality before relying on generated concept, comparison, synthesis, or tool pages.",
|
|
578
|
+
"",
|
|
579
|
+
"## Quality Snapshot",
|
|
580
|
+
"",
|
|
581
|
+
`- source-quality findings: ${findings.length}`,
|
|
582
|
+
`- unsupported items: ${unsupported.length}`,
|
|
583
|
+
`- source pages tracked: ${asArray(payload.sources).length}`,
|
|
584
|
+
`- tool candidates tracked: ${asArray(payload.tool_entities).length}`,
|
|
585
|
+
"",
|
|
586
|
+
"## Maintenance Entry Points",
|
|
587
|
+
"",
|
|
588
|
+
maintenanceLinks.length > 0 ? maintenanceLinks.map((link) => `- ${link}`).join("\n") : "- No dedicated maintenance review page was required for this extraction.",
|
|
589
|
+
"",
|
|
590
|
+
"## Source Findings",
|
|
591
|
+
"",
|
|
592
|
+
findingList,
|
|
593
|
+
"",
|
|
594
|
+
"## Unsupported Items",
|
|
595
|
+
"",
|
|
596
|
+
unsupportedList,
|
|
597
|
+
"",
|
|
598
|
+
"## Source Pages To Recheck",
|
|
599
|
+
"",
|
|
600
|
+
linkedListFromDashboard(sources.map((source) => ({ title: source.title, path: source.path, source_refs: source.quality_flags.length }))),
|
|
601
|
+
"",
|
|
602
|
+
"## Operating Notes",
|
|
603
|
+
"",
|
|
604
|
+
"- Fix source-quality warnings before treating dashboard rankings as final.",
|
|
605
|
+
"- Keep destructive cleanup, archive, and delete decisions outside this generated dashboard.",
|
|
606
|
+
"- Re-run lint after compile/apply to verify internal links and required fields.",
|
|
607
|
+
"",
|
|
608
|
+
].join("\n");
|
|
609
|
+
return withPersonalBrainFrontmatter(
|
|
610
|
+
dashboardFrontmatter("dashboard_maintenance_quality", "Maintenance / Quality Dashboard", payload, payload.generated_at || "", ["../index.md"]),
|
|
611
|
+
body
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function renderSourceCoverageDashboard(payload) {
|
|
616
|
+
const sources = sourceEntries(payload, 30);
|
|
617
|
+
const counts = payload.source_counts || {};
|
|
618
|
+
const sourceTable = [
|
|
619
|
+
"| Source | Quality flags | Source URL | Hash prefix |",
|
|
620
|
+
"|---|---|---|---|",
|
|
621
|
+
...sources.map((source) => {
|
|
622
|
+
const label = source.path ? `[${mdEscapeTable(source.title)}](${dashboardLink(source.path)})` : mdEscapeTable(source.title);
|
|
623
|
+
const sourceUrl = source.source_url ? `[source](${source.source_url})` : "-";
|
|
624
|
+
return `| ${label} | ${mdEscapeTable(source.quality_flags.join(", ") || "none")} | ${sourceUrl} | ${mdEscapeTable(source.source_hash.slice(0, 12) || "-")} |`;
|
|
625
|
+
}),
|
|
626
|
+
].join("\n");
|
|
627
|
+
const body = [
|
|
628
|
+
"# Source Coverage Dashboard",
|
|
629
|
+
"",
|
|
630
|
+
"## Purpose",
|
|
631
|
+
"",
|
|
632
|
+
"Use this page to understand which local files fed the generated local wiki and where source-level review should start.",
|
|
633
|
+
"",
|
|
634
|
+
"## Coverage Snapshot",
|
|
635
|
+
"",
|
|
636
|
+
`- sources scanned: ${counts.scanned ?? "(unknown)"}`,
|
|
637
|
+
`- sources indexed: ${counts.indexed ?? "(unknown)"}`,
|
|
638
|
+
`- source records: ${asArray(payload.sources).length}`,
|
|
639
|
+
`- tool candidates: ${asArray(payload.tool_entities).length}`,
|
|
640
|
+
`- concepts: ${asArray(payload.concepts).length}`,
|
|
641
|
+
`- comparisons: ${asArray(payload.comparisons).length}`,
|
|
642
|
+
`- syntheses: ${asArray(payload.syntheses).length}`,
|
|
643
|
+
"",
|
|
644
|
+
"## Source Pages",
|
|
645
|
+
"",
|
|
646
|
+
sourceTable,
|
|
647
|
+
"",
|
|
648
|
+
"## Coverage Notes",
|
|
649
|
+
"",
|
|
650
|
+
"- The dashboard is derived from local source registry and semantic extraction output only.",
|
|
651
|
+
"- Source hashes should remain stable across compile and enrichment/report-only flows.",
|
|
652
|
+
"- Investigate any lint provenance finding before using the generated graph as a decision record.",
|
|
653
|
+
"",
|
|
654
|
+
].join("\n");
|
|
655
|
+
return withPersonalBrainFrontmatter(
|
|
656
|
+
dashboardFrontmatter("dashboard_source_coverage", "Source Coverage Dashboard", payload, payload.generated_at || "", ["../index.md"]),
|
|
657
|
+
body
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderDashboardPage(name, payload) {
|
|
662
|
+
if (name === "research-tool-landscape") return renderResearchToolLandscapeDashboard(payload);
|
|
663
|
+
if (name === "decision-support") return renderDecisionSupportDashboard(payload);
|
|
664
|
+
if (name === "maintenance-quality") return renderMaintenanceQualityDashboard(payload);
|
|
665
|
+
if (name === "source-coverage") return renderSourceCoverageDashboard(payload);
|
|
666
|
+
return renderResearchToolLandscapeDashboard(payload);
|
|
667
|
+
}
|
|
668
|
+
|
|
207
669
|
function yamlScalar(value) {
|
|
208
670
|
return `"${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ").trim()}"`;
|
|
209
671
|
}
|
|
@@ -268,6 +730,10 @@ function renderSourcePage(source, generatedAt) {
|
|
|
268
730
|
"",
|
|
269
731
|
mdList(source.source_quality && source.source_quality.quality_flags),
|
|
270
732
|
"",
|
|
733
|
+
"## Thinness Policy",
|
|
734
|
+
"",
|
|
735
|
+
"This page is an accepted source/provenance anchor. Its purpose is to preserve local source identity, hash, quality flags, and provenance rather than to carry semantic analysis. Strict stub quality is measured on tool, concept, comparison, and synthesis pages.",
|
|
736
|
+
"",
|
|
271
737
|
"## Provenance",
|
|
272
738
|
"",
|
|
273
739
|
mdList(provenanceRefsFor(source)),
|
|
@@ -281,6 +747,7 @@ function renderSourcePage(source, generatedAt) {
|
|
|
281
747
|
updated_at: generatedAt,
|
|
282
748
|
aliases: source.source_logical_path ? [source.source_logical_path] : [],
|
|
283
749
|
tags: ["source", source.source_type || "local"],
|
|
750
|
+
thinness_policy: "accepted_source_provenance_anchor",
|
|
284
751
|
source_refs: sourceRefsFor(source),
|
|
285
752
|
confidence: source.source_quality && source.source_quality.low_confidence_extraction ? "low" : "medium",
|
|
286
753
|
review_status: source.source_quality && source.source_quality.low_confidence_extraction ? "needs_review" : "ready_for_review",
|
|
@@ -399,6 +866,10 @@ function renderConceptPage(concept, generatedAt) {
|
|
|
399
866
|
"",
|
|
400
867
|
summary,
|
|
401
868
|
"",
|
|
869
|
+
"## Decision Brief",
|
|
870
|
+
"",
|
|
871
|
+
conceptDecisionBrief(concept),
|
|
872
|
+
"",
|
|
402
873
|
"## Key Axes",
|
|
403
874
|
"",
|
|
404
875
|
conceptAxisList(concept),
|
|
@@ -464,6 +935,10 @@ function renderComparisonPage(comparison, generatedAt) {
|
|
|
464
935
|
"",
|
|
465
936
|
comparison.summary || `Local comparison for ${comparison.topic || comparison.comparison_id}.`,
|
|
466
937
|
"",
|
|
938
|
+
"## Decision Guidance",
|
|
939
|
+
"",
|
|
940
|
+
comparisonDecisionGuidance(comparison),
|
|
941
|
+
"",
|
|
467
942
|
"## Decision Axes",
|
|
468
943
|
"",
|
|
469
944
|
conceptAxisList({ key_axes: comparison.decision_axes }),
|
|
@@ -527,6 +1002,10 @@ function renderSynthesisPage(synthesis, generatedAt) {
|
|
|
527
1002
|
"",
|
|
528
1003
|
synthesis.summary || `Generated synthesis page for ${synthesis.topic || synthesis.synthesis_id}.`,
|
|
529
1004
|
"",
|
|
1005
|
+
"## Decision Narrative",
|
|
1006
|
+
"",
|
|
1007
|
+
synthesisDecisionNarrative(synthesis),
|
|
1008
|
+
"",
|
|
530
1009
|
"## Landscape Snapshot",
|
|
531
1010
|
"",
|
|
532
1011
|
candidateToolList(synthesis.candidate_tools),
|
|
@@ -587,15 +1066,133 @@ function renderSynthesisPage(synthesis, generatedAt) {
|
|
|
587
1066
|
|
|
588
1067
|
function renderRootPage(name, payload) {
|
|
589
1068
|
const extractionCounts = payload && payload.source_counts ? payload.source_counts : {};
|
|
1069
|
+
const concepts = topConcepts(payload, 8);
|
|
1070
|
+
const comparisons = comparisonEntries(payload, 6);
|
|
1071
|
+
const syntheses = synthesisEntries(payload, 6);
|
|
1072
|
+
const tools = highSignalTools(payload, 10);
|
|
1073
|
+
const sources = sourceEntries(payload, 8);
|
|
1074
|
+
const dashboards = [
|
|
1075
|
+
["Research / Tool Landscape", "dashboards/research-tool-landscape.md"],
|
|
1076
|
+
["Decision Support", "dashboards/decision-support.md"],
|
|
1077
|
+
["Maintenance / Quality", "dashboards/maintenance-quality.md"],
|
|
1078
|
+
["Source Coverage", "dashboards/source-coverage.md"],
|
|
1079
|
+
];
|
|
590
1080
|
const titles = {
|
|
591
|
-
index: "
|
|
592
|
-
overview: "
|
|
593
|
-
ontology: "
|
|
594
|
-
taxonomy: "
|
|
595
|
-
graph: "
|
|
596
|
-
log: "
|
|
1081
|
+
index: "Local Wiki Index",
|
|
1082
|
+
overview: "Local Wiki Overview",
|
|
1083
|
+
ontology: "Local Wiki Ontology",
|
|
1084
|
+
taxonomy: "Local Wiki Taxonomy",
|
|
1085
|
+
graph: "Local Wiki Graph",
|
|
1086
|
+
log: "Local Wiki Generation Log",
|
|
1087
|
+
};
|
|
1088
|
+
const title = titles[name] || "Local Wiki";
|
|
1089
|
+
const dashboardLinks = dashboards.map(([label, target]) => `- [${label}](${target})`).join("\n");
|
|
1090
|
+
const topToolLinks = tools.length > 0
|
|
1091
|
+
? tools.slice(0, 8).map((tool) => `- [${tool.title}](${rootLink(tool.path)}): ${tool.category}; ${tool.source_refs} source reference(s).`).join("\n")
|
|
1092
|
+
: "- None recorded.";
|
|
1093
|
+
const sourceLinks = sources.length > 0
|
|
1094
|
+
? sources.map((source) => `- [${source.title}](${rootLink(source.path)}): ${source.quality_flags.length > 0 ? source.quality_flags.join(", ") : "no quality flags"}.`).join("\n")
|
|
1095
|
+
: "- None recorded.";
|
|
1096
|
+
const taxonomyRows = [
|
|
1097
|
+
"| Area | Count | Starting points |",
|
|
1098
|
+
"|---|---:|---|",
|
|
1099
|
+
`| Sources | ${asArray(payload.sources).length} | [Source coverage dashboard](dashboards/source-coverage.md) |`,
|
|
1100
|
+
`| Tool entities | ${asArray(payload.tool_entities).length} | [Research / Tool Landscape](dashboards/research-tool-landscape.md) |`,
|
|
1101
|
+
`| Concepts | ${asArray(payload.concepts).length} | ${concepts.slice(0, 4).map((entry) => entry.path ? `[${mdEscapeTable(entry.title)}](${rootLink(entry.path)})` : mdEscapeTable(entry.title)).join("<br>") || "-"} |`,
|
|
1102
|
+
`| Comparisons | ${asArray(payload.comparisons).length} | ${comparisons.slice(0, 4).map((entry) => entry.path ? `[${mdEscapeTable(entry.title)}](${rootLink(entry.path)})` : mdEscapeTable(entry.title)).join("<br>") || "-"} |`,
|
|
1103
|
+
`| Syntheses | ${asArray(payload.syntheses).length} | ${syntheses.slice(0, 4).map((entry) => entry.path ? `[${mdEscapeTable(entry.title)}](${rootLink(entry.path)})` : mdEscapeTable(entry.title)).join("<br>") || "-"} |`,
|
|
1104
|
+
].join("\n");
|
|
1105
|
+
const graphRows = [
|
|
1106
|
+
"| Graph surface | Count | Navigation |",
|
|
1107
|
+
"|---|---:|---|",
|
|
1108
|
+
`| Relations | ${asArray(payload.relations).length} | Review relation append sections below. |`,
|
|
1109
|
+
`| Concepts | ${asArray(payload.concepts).length} | ${linkedListFromRoot(concepts.slice(0, 5)).replace(/\n/g, "<br>")} |`,
|
|
1110
|
+
`| Comparisons | ${asArray(payload.comparisons).length} | ${linkedListFromRoot(comparisons.slice(0, 5)).replace(/\n/g, "<br>")} |`,
|
|
1111
|
+
`| Syntheses | ${asArray(payload.syntheses).length} | ${linkedListFromRoot(syntheses.slice(0, 5)).replace(/\n/g, "<br>")} |`,
|
|
1112
|
+
].join("\n");
|
|
1113
|
+
const pageSpecificSections = {
|
|
1114
|
+
index: [
|
|
1115
|
+
"## Start Here",
|
|
1116
|
+
"",
|
|
1117
|
+
dashboardLinks,
|
|
1118
|
+
"",
|
|
1119
|
+
"## High-Signal Tool Entry Points",
|
|
1120
|
+
"",
|
|
1121
|
+
topToolLinks,
|
|
1122
|
+
"",
|
|
1123
|
+
"## Concept / Comparison / Synthesis Entry Points",
|
|
1124
|
+
"",
|
|
1125
|
+
"Concepts:",
|
|
1126
|
+
"",
|
|
1127
|
+
linkedListFromRoot(concepts),
|
|
1128
|
+
"",
|
|
1129
|
+
"Comparisons:",
|
|
1130
|
+
"",
|
|
1131
|
+
linkedListFromRoot(comparisons),
|
|
1132
|
+
"",
|
|
1133
|
+
"Syntheses:",
|
|
1134
|
+
"",
|
|
1135
|
+
linkedListFromRoot(syntheses),
|
|
1136
|
+
],
|
|
1137
|
+
overview: [
|
|
1138
|
+
"## Session Brief",
|
|
1139
|
+
"",
|
|
1140
|
+
"This generated local wiki is organized for local review. Start with dashboards, inspect top concepts, then use comparison and synthesis pages before adopting any tool.",
|
|
1141
|
+
"",
|
|
1142
|
+
"## Current Landscape",
|
|
1143
|
+
"",
|
|
1144
|
+
`- high-signal tools listed: ${tools.length}`,
|
|
1145
|
+
`- top concepts listed: ${concepts.length}`,
|
|
1146
|
+
`- comparison pages listed: ${comparisons.length}`,
|
|
1147
|
+
`- synthesis pages listed: ${syntheses.length}`,
|
|
1148
|
+
"",
|
|
1149
|
+
"## Recommended Browsing Path",
|
|
1150
|
+
"",
|
|
1151
|
+
"- [Research / Tool Landscape](dashboards/research-tool-landscape.md)",
|
|
1152
|
+
"- [Decision Support](dashboards/decision-support.md)",
|
|
1153
|
+
"- [Source Coverage](dashboards/source-coverage.md)",
|
|
1154
|
+
"- [Maintenance / Quality](dashboards/maintenance-quality.md)",
|
|
1155
|
+
],
|
|
1156
|
+
taxonomy: [
|
|
1157
|
+
"## Taxonomy Snapshot",
|
|
1158
|
+
"",
|
|
1159
|
+
taxonomyRows,
|
|
1160
|
+
"",
|
|
1161
|
+
"## Concept Clusters",
|
|
1162
|
+
"",
|
|
1163
|
+
concepts.length > 0
|
|
1164
|
+
? concepts.map((concept) => `- [${concept.title}](${rootLink(concept.path)}): ${concept.related_count} related tool candidate(s); axes: ${concept.axes.slice(0, 4).join(", ") || "none inferred"}.`).join("\n")
|
|
1165
|
+
: "- None recorded.",
|
|
1166
|
+
],
|
|
1167
|
+
graph: [
|
|
1168
|
+
"## Navigation Graph",
|
|
1169
|
+
"",
|
|
1170
|
+
graphRows,
|
|
1171
|
+
"",
|
|
1172
|
+
"## Tool Cluster Hubs",
|
|
1173
|
+
"",
|
|
1174
|
+
groupToolsByCategory(tools).slice(0, 8).map(({ category, rows }) =>
|
|
1175
|
+
`- ${category}: ${rows.slice(0, 4).map((tool) => `[${tool.title}](${rootLink(tool.path)})`).join(", ")}`
|
|
1176
|
+
).join("\n") || "- None recorded.",
|
|
1177
|
+
],
|
|
1178
|
+
log: [
|
|
1179
|
+
"## Review Trail",
|
|
1180
|
+
"",
|
|
1181
|
+
"- Compile operations were derived from the semantic extraction report.",
|
|
1182
|
+
"- Dashboard and navigation pages are generated from the same local evidence as entity/concept/comparison/synthesis pages.",
|
|
1183
|
+
"- Re-run lint after compile/apply to verify internal links and quality gates.",
|
|
1184
|
+
],
|
|
1185
|
+
ontology: [
|
|
1186
|
+
"## Ontology Navigation",
|
|
1187
|
+
"",
|
|
1188
|
+
"- source: local file/source registry page.",
|
|
1189
|
+
"- tool_entity: extracted GitHub repository or tool candidate page.",
|
|
1190
|
+
"- concept: local thematic grouping across tools and source evidence.",
|
|
1191
|
+
"- comparison: decision matrix for a concept cluster.",
|
|
1192
|
+
"- synthesis: higher-level review path for a cluster.",
|
|
1193
|
+
"- dashboard: curated generated navigation page for browsing and triage.",
|
|
1194
|
+
],
|
|
597
1195
|
};
|
|
598
|
-
const title = titles[name] || "Personal Brain";
|
|
599
1196
|
const body = [
|
|
600
1197
|
`# ${title}`,
|
|
601
1198
|
"",
|
|
@@ -615,8 +1212,15 @@ function renderRootPage(name, payload) {
|
|
|
615
1212
|
"- concepts/",
|
|
616
1213
|
"- comparisons/",
|
|
617
1214
|
"- syntheses/",
|
|
1215
|
+
"- dashboards/",
|
|
618
1216
|
"- maintenance/",
|
|
619
1217
|
"",
|
|
1218
|
+
...(pageSpecificSections[name] || []),
|
|
1219
|
+
"",
|
|
1220
|
+
"## Source Coverage",
|
|
1221
|
+
"",
|
|
1222
|
+
sourceLinks,
|
|
1223
|
+
"",
|
|
620
1224
|
].join("\n");
|
|
621
1225
|
return withPersonalBrainFrontmatter({
|
|
622
1226
|
id: `personal_brain_${name}`,
|
|
@@ -625,7 +1229,8 @@ function renderRootPage(name, payload) {
|
|
|
625
1229
|
created_at: payload.generated_at || "",
|
|
626
1230
|
updated_at: payload.generated_at || "",
|
|
627
1231
|
aliases: [name],
|
|
628
|
-
tags: ["
|
|
1232
|
+
tags: ["local-wiki", name],
|
|
1233
|
+
related_pages: dashboards.map(([, target]) => target),
|
|
629
1234
|
source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
|
|
630
1235
|
confidence: "medium",
|
|
631
1236
|
review_status: "needs_review",
|
|
@@ -694,7 +1299,7 @@ function operationsFromSemanticExtraction(payload) {
|
|
|
694
1299
|
source_hash: source.source_hash,
|
|
695
1300
|
target_page_path: source.target_page_path,
|
|
696
1301
|
content: renderSourcePage(source, payload.generated_at),
|
|
697
|
-
proposed_content_summary: `Create
|
|
1302
|
+
proposed_content_summary: `Create local wiki source page for ${source.source_logical_path || source.source_relative_path || source.source_id}.`,
|
|
698
1303
|
source_refs: sourceRefsFor(source),
|
|
699
1304
|
provenance_refs: provenanceRefsFor(source),
|
|
700
1305
|
evidence_refs: refsFor(source),
|
|
@@ -791,12 +1396,12 @@ function operationsFromSemanticExtraction(payload) {
|
|
|
791
1396
|
}
|
|
792
1397
|
|
|
793
1398
|
const rootPages = [
|
|
794
|
-
["index", "
|
|
795
|
-
["overview", "
|
|
796
|
-
["ontology", "
|
|
797
|
-
["taxonomy", "
|
|
798
|
-
["graph", "
|
|
799
|
-
["log", "
|
|
1399
|
+
["index", "wiki/index.md", "Create local wiki index from semantic extraction output."],
|
|
1400
|
+
["overview", "wiki/overview.md", "Create local wiki overview summarizing extracted local sources."],
|
|
1401
|
+
["ontology", "wiki/ontology.md", "Create ontology page describing source, tool_entity, concept, claim, relation, comparison, and synthesis records."],
|
|
1402
|
+
["taxonomy", "wiki/taxonomy.md", "Create taxonomy page for extracted categories and concepts."],
|
|
1403
|
+
["graph", "wiki/graph.md", "Create semantic graph summary page from extracted relations."],
|
|
1404
|
+
["log", "wiki/log.md", "Create generation log page for extraction and compile preview evidence."],
|
|
800
1405
|
];
|
|
801
1406
|
for (const [name, target, summary] of rootPages) {
|
|
802
1407
|
pushCreatePageOperation(operations, {
|
|
@@ -812,13 +1417,34 @@ function operationsFromSemanticExtraction(payload) {
|
|
|
812
1417
|
});
|
|
813
1418
|
}
|
|
814
1419
|
|
|
1420
|
+
const dashboardPages = [
|
|
1421
|
+
["research-tool-landscape", "wiki/dashboards/research-tool-landscape.md", "Create research/tool landscape dashboard from local semantic extraction output."],
|
|
1422
|
+
["decision-support", "wiki/dashboards/decision-support.md", "Create decision-support dashboard from local comparison and synthesis evidence."],
|
|
1423
|
+
["maintenance-quality", "wiki/dashboards/maintenance-quality.md", "Create maintenance/quality dashboard from local extraction findings."],
|
|
1424
|
+
["source-coverage", "wiki/dashboards/source-coverage.md", "Create source coverage dashboard from local source registry evidence."],
|
|
1425
|
+
];
|
|
1426
|
+
for (const [name, target, summary] of dashboardPages) {
|
|
1427
|
+
pushCreatePageOperation(operations, {
|
|
1428
|
+
operation_id: `pb-dashboard-${name}`,
|
|
1429
|
+
source_id: rootSourceId,
|
|
1430
|
+
source_hash: rootSourceHash,
|
|
1431
|
+
target_page_path: target,
|
|
1432
|
+
content: renderDashboardPage(name, payload),
|
|
1433
|
+
proposed_content_summary: summary,
|
|
1434
|
+
source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
|
|
1435
|
+
evidence_refs: [`record_type:${payload.record_type}`, `dashboard:${name}`],
|
|
1436
|
+
confidence: "medium",
|
|
1437
|
+
review_status: "needs_review",
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
815
1441
|
for (const relation of asArray(payload.relations)) {
|
|
816
1442
|
operations.push({
|
|
817
1443
|
operation_id: `pb-relation-${String(operations.length + 1).padStart(3, "0")}`,
|
|
818
1444
|
operation_type: "add_relation",
|
|
819
1445
|
source_id: relation.source_id || rootSourceId,
|
|
820
1446
|
source_hash: rootSourceHash,
|
|
821
|
-
target_page_path: "
|
|
1447
|
+
target_page_path: "wiki/graph.md",
|
|
822
1448
|
content_sections: [
|
|
823
1449
|
{
|
|
824
1450
|
title: `Relation: ${relation.relation_type || relation.relation_id}`,
|
|
@@ -848,7 +1474,7 @@ function operationsFromSemanticExtraction(payload) {
|
|
|
848
1474
|
operation_id: "pb-maintenance-extraction-quality-review",
|
|
849
1475
|
source_id: rootSourceId,
|
|
850
1476
|
source_hash: rootSourceHash,
|
|
851
|
-
target_page_path: "
|
|
1477
|
+
target_page_path: "wiki/maintenance/extraction-quality-review.md",
|
|
852
1478
|
content: renderMaintenancePage(payload),
|
|
853
1479
|
proposed_content_summary: `Create maintenance review page for ${qualityFindingCount} source-quality finding(s) and ${unsupportedCount} unsupported extraction item(s).`,
|
|
854
1480
|
evidence_refs: [`source_quality_findings:${qualityFindingCount}`, `unsupported_items:${unsupportedCount}`],
|
|
@@ -1000,7 +1626,7 @@ function fallbackOperationContent(operation) {
|
|
|
1000
1626
|
"",
|
|
1001
1627
|
"## Generated Operation",
|
|
1002
1628
|
"",
|
|
1003
|
-
operation.proposed_content_summary || "Generated
|
|
1629
|
+
operation.proposed_content_summary || "Generated local wiki content.",
|
|
1004
1630
|
"",
|
|
1005
1631
|
"## Evidence",
|
|
1006
1632
|
"",
|
|
@@ -1010,16 +1636,21 @@ function fallbackOperationContent(operation) {
|
|
|
1010
1636
|
}
|
|
1011
1637
|
|
|
1012
1638
|
function normalizeTargetPath(rawTargetPath, projectPath) {
|
|
1013
|
-
const
|
|
1639
|
+
const canonicalWikiRoot = getCanonicalWikiPath(projectPath);
|
|
1640
|
+
const legacyPersonalBrainRoot = getLegacyPersonalBrainPath(projectPath);
|
|
1014
1641
|
const targetValue = String(rawTargetPath || "");
|
|
1015
1642
|
const resolvedTarget = targetValue ? path.resolve(projectPath, targetValue) : "";
|
|
1016
1643
|
const withinProject = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, projectPath);
|
|
1017
|
-
const
|
|
1644
|
+
const withinCanonicalWiki = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, canonicalWikiRoot);
|
|
1645
|
+
const withinLegacyPersonalBrain = Boolean(resolvedTarget) && isPathInsideOrEqual(resolvedTarget, legacyPersonalBrainRoot);
|
|
1646
|
+
const withinWikiContent = withinCanonicalWiki || withinLegacyPersonalBrain;
|
|
1018
1647
|
return {
|
|
1019
1648
|
resolvedTarget,
|
|
1020
1649
|
targetPathNormalized: withinProject ? toPosix(path.relative(projectPath, resolvedTarget)) : toPosix(resolvedTarget),
|
|
1021
1650
|
withinProject,
|
|
1022
|
-
|
|
1651
|
+
withinCanonicalWiki,
|
|
1652
|
+
withinLegacyPersonalBrain,
|
|
1653
|
+
withinWikiContent,
|
|
1023
1654
|
};
|
|
1024
1655
|
}
|
|
1025
1656
|
|
|
@@ -1036,7 +1667,9 @@ function normalizeOperation(rawOperation, index, projectPath) {
|
|
|
1036
1667
|
target_page_path: String(rawOperation.target_page_path || rawOperation.targetPagePath || ""),
|
|
1037
1668
|
target_path_normalized: target.targetPathNormalized,
|
|
1038
1669
|
target_path_within_project: target.withinProject,
|
|
1039
|
-
target_path_within_personal_brain: target.
|
|
1670
|
+
target_path_within_personal_brain: target.withinWikiContent,
|
|
1671
|
+
target_path_within_canonical_wiki: target.withinCanonicalWiki,
|
|
1672
|
+
target_path_within_legacy_personal_brain: target.withinLegacyPersonalBrain,
|
|
1040
1673
|
operation_type: operationType || "unsupported_operation",
|
|
1041
1674
|
requested_operation_type: operationType || "(missing)",
|
|
1042
1675
|
proposed_content_summary: String(rawOperation.proposed_content_summary || rawOperation.proposedContentSummary || ""),
|
|
@@ -1153,6 +1786,8 @@ function renderApplyPlan({ projectPath, planPath, reportPath, generatedAt, opera
|
|
|
1153
1786
|
target_path_normalized: operation.target_path_normalized,
|
|
1154
1787
|
target_path_within_project: operation.target_path_within_project,
|
|
1155
1788
|
target_path_within_personal_brain: operation.target_path_within_personal_brain,
|
|
1789
|
+
target_path_within_canonical_wiki: operation.target_path_within_canonical_wiki,
|
|
1790
|
+
target_path_within_legacy_personal_brain: operation.target_path_within_legacy_personal_brain,
|
|
1156
1791
|
content: operation.content,
|
|
1157
1792
|
content_sections: operation.content_sections,
|
|
1158
1793
|
source_refs: operation.source_refs,
|
|
@@ -1214,12 +1849,14 @@ function aggregateApplyTargets(operations, projectPath) {
|
|
|
1214
1849
|
const targets = new Map();
|
|
1215
1850
|
for (const operation of operations) {
|
|
1216
1851
|
const target = normalizeTargetPath(operation.target_page_path, projectPath);
|
|
1217
|
-
if (!target.withinProject || !target.
|
|
1218
|
-
throw new ValidationError(`Unsafe
|
|
1852
|
+
if (!target.withinProject || !target.withinWikiContent) {
|
|
1853
|
+
throw new ValidationError(`Unsafe wiki target path: ${operation.target_page_path}. No project files were changed.`);
|
|
1219
1854
|
}
|
|
1220
1855
|
const existing = targets.get(target.resolvedTarget) || {
|
|
1221
1856
|
path: target.resolvedTarget,
|
|
1222
1857
|
normalized: target.targetPathNormalized,
|
|
1858
|
+
withinCanonicalWiki: target.withinCanonicalWiki,
|
|
1859
|
+
withinLegacyPersonalBrain: target.withinLegacyPersonalBrain,
|
|
1223
1860
|
sections: [],
|
|
1224
1861
|
operationIds: [],
|
|
1225
1862
|
};
|
|
@@ -1233,6 +1870,111 @@ function aggregateApplyTargets(operations, projectPath) {
|
|
|
1233
1870
|
return [...targets.values()].sort((a, b) => a.normalized.localeCompare(b.normalized));
|
|
1234
1871
|
}
|
|
1235
1872
|
|
|
1873
|
+
function scaffoldFileIfMissing(filePath, content, projectPath, rootPath, created, skipped) {
|
|
1874
|
+
if (!isPathInsideOrEqual(filePath, rootPath)) {
|
|
1875
|
+
throw new ValidationError(`Unsafe scaffold target path: ${filePath}. No project files were changed.`);
|
|
1876
|
+
}
|
|
1877
|
+
if (fs.existsSync(filePath)) {
|
|
1878
|
+
skipped.push(toPosix(path.relative(projectPath, filePath)));
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1882
|
+
fs.writeFileSync(filePath, ensureTrailingNewline(content), "utf-8");
|
|
1883
|
+
created.push(toPosix(path.relative(projectPath, filePath)));
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function scaffoldWikiPageFrontmatter(fields, body) {
|
|
1887
|
+
const generatedAt = new Date().toISOString();
|
|
1888
|
+
return withPersonalBrainFrontmatter({
|
|
1889
|
+
created_at: generatedAt,
|
|
1890
|
+
updated_at: generatedAt,
|
|
1891
|
+
...fields,
|
|
1892
|
+
}, body);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function scaffoldCanonicalWikiStructure(projectPath) {
|
|
1896
|
+
const canonicalRoot = getCanonicalWikiPath(projectPath);
|
|
1897
|
+
const created = [];
|
|
1898
|
+
const skipped = [];
|
|
1899
|
+
const directories = [
|
|
1900
|
+
"timelines",
|
|
1901
|
+
"queries",
|
|
1902
|
+
"decisions",
|
|
1903
|
+
"meeting-notes",
|
|
1904
|
+
"templates",
|
|
1905
|
+
"maintenance",
|
|
1906
|
+
];
|
|
1907
|
+
fs.mkdirSync(canonicalRoot, { recursive: true });
|
|
1908
|
+
for (const dir of directories) {
|
|
1909
|
+
const dirPath = path.join(canonicalRoot, dir);
|
|
1910
|
+
if (!isPathInsideOrEqual(dirPath, canonicalRoot)) {
|
|
1911
|
+
throw new ValidationError(`Unsafe wiki directory path: ${dirPath}. No project files were changed.`);
|
|
1912
|
+
}
|
|
1913
|
+
if (!fs.existsSync(dirPath)) {
|
|
1914
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1915
|
+
created.push(toPosix(path.relative(projectPath, dirPath)) + "/");
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
scaffoldFileIfMissing(
|
|
1919
|
+
path.join(canonicalRoot, "maintenance.md"),
|
|
1920
|
+
scaffoldWikiPageFrontmatter(
|
|
1921
|
+
{
|
|
1922
|
+
id: "root_maintenance",
|
|
1923
|
+
title: "Maintenance",
|
|
1924
|
+
type: "maintenance",
|
|
1925
|
+
aliases: ["Maintenance"],
|
|
1926
|
+
tags: ["maintenance", "local-wiki"],
|
|
1927
|
+
related_pages: ["index.md", "maintenance/extraction-quality-review.md"],
|
|
1928
|
+
source_refs: ["sdtk_wiki_scaffold"],
|
|
1929
|
+
confidence: "medium",
|
|
1930
|
+
review_status: "needs_review",
|
|
1931
|
+
},
|
|
1932
|
+
"# Maintenance\n\n## Purpose\n\nTrack local wiki maintenance tasks, quality checks, and review follow-ups.\n\n## Source Quality Findings\n\n- Review generated lint, discover, compile, and enrichment reports under `.sdtk/wiki/reports`.\n\n## Unsupported Items\n\n- None recorded in the scaffold.\n\n## Open Items\n\n- Promote maintenance notes into dedicated pages under `wiki/maintenance/` when review produces durable decisions."
|
|
1933
|
+
),
|
|
1934
|
+
projectPath,
|
|
1935
|
+
canonicalRoot,
|
|
1936
|
+
created,
|
|
1937
|
+
skipped
|
|
1938
|
+
);
|
|
1939
|
+
scaffoldFileIfMissing(
|
|
1940
|
+
path.join(canonicalRoot, "inbox.md"),
|
|
1941
|
+
scaffoldWikiPageFrontmatter(
|
|
1942
|
+
{
|
|
1943
|
+
id: "root_inbox",
|
|
1944
|
+
title: "Inbox",
|
|
1945
|
+
type: "root",
|
|
1946
|
+
aliases: ["Inbox"],
|
|
1947
|
+
tags: ["inbox", "local-wiki"],
|
|
1948
|
+
related_pages: ["index.md"],
|
|
1949
|
+
source_refs: [],
|
|
1950
|
+
confidence: "medium",
|
|
1951
|
+
review_status: "needs_review",
|
|
1952
|
+
},
|
|
1953
|
+
"# Inbox\n\n## Purpose\n\nCapture uncategorized local notes before they are promoted into source, decision, query, timeline, or maintenance pages.\n\n## Inbox\n\n- Add local notes here before structured review."
|
|
1954
|
+
),
|
|
1955
|
+
projectPath,
|
|
1956
|
+
canonicalRoot,
|
|
1957
|
+
created,
|
|
1958
|
+
skipped
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
const rootDocs = [
|
|
1962
|
+
["README.md", "# Local Wiki\n\nThis project uses `wiki/` as the human-facing SDTK-WIKI output. Internal state, reports, provenance, raw registry, graph, and logs stay under `.sdtk/wiki`."],
|
|
1963
|
+
["AGENTS.md", "# Agent Operating Notes\n\nCodex agents should read `wiki/index.md` first, then use `wiki/dashboards/` for navigation. Do not mutate `.sdtk/wiki` internal state by hand."],
|
|
1964
|
+
["CLAUDE.md", "# Claude Operating Notes\n\nClaude agents should use `wiki/` as the canonical local wiki and treat `.sdtk/wiki` as SDTK-WIKI internal state."],
|
|
1965
|
+
];
|
|
1966
|
+
for (const [name, content] of rootDocs) {
|
|
1967
|
+
const filePath = path.join(projectPath, name);
|
|
1968
|
+
if (fs.existsSync(filePath)) {
|
|
1969
|
+
skipped.push(name);
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
fs.writeFileSync(filePath, ensureTrailingNewline(content), "utf-8");
|
|
1973
|
+
created.push(name);
|
|
1974
|
+
}
|
|
1975
|
+
return { created, skipped };
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1236
1978
|
function runWikiCompileApply({ projectPath, planArg }) {
|
|
1237
1979
|
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
1238
1980
|
if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
|
|
@@ -1251,9 +1993,12 @@ function runWikiCompileApply({ projectPath, planArg }) {
|
|
|
1251
1993
|
const toCreate = [];
|
|
1252
1994
|
|
|
1253
1995
|
for (const target of targets) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1996
|
+
if (target.withinCanonicalWiki) {
|
|
1997
|
+
assertCanonicalWikiWritePath(target.path, resolvedProjectPath);
|
|
1998
|
+
} else if (target.withinLegacyPersonalBrain) {
|
|
1999
|
+
assertWikiWorkspaceWritePath(target.path, resolvedProjectPath);
|
|
2000
|
+
} else {
|
|
2001
|
+
throw new ValidationError(`Refusing to write outside project wiki output: ${target.normalized}. No project files were changed.`);
|
|
1257
2002
|
}
|
|
1258
2003
|
if (!fs.existsSync(target.path)) {
|
|
1259
2004
|
toCreate.push(target);
|
|
@@ -1272,6 +2017,7 @@ function runWikiCompileApply({ projectPath, planArg }) {
|
|
|
1272
2017
|
}
|
|
1273
2018
|
|
|
1274
2019
|
const created = [];
|
|
2020
|
+
const scaffold = scaffoldCanonicalWikiStructure(resolvedProjectPath);
|
|
1275
2021
|
for (const target of toCreate) {
|
|
1276
2022
|
fs.mkdirSync(path.dirname(target.path), { recursive: true });
|
|
1277
2023
|
fs.writeFileSync(target.path, target.content, "utf-8");
|
|
@@ -1283,6 +2029,8 @@ function runWikiCompileApply({ projectPath, planArg }) {
|
|
|
1283
2029
|
planPath,
|
|
1284
2030
|
created,
|
|
1285
2031
|
unchanged,
|
|
2032
|
+
scaffoldCreated: scaffold.created,
|
|
2033
|
+
scaffoldSkipped: scaffold.skipped,
|
|
1286
2034
|
sourceQualityWarningCount: Number(sourceQualitySummary.warning_count || sourceQualitySummary.findings_count || 0),
|
|
1287
2035
|
};
|
|
1288
2036
|
} catch (error) {
|