sdtk-wiki-kit 0.1.2 → 0.1.3

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 CHANGED
@@ -177,13 +177,20 @@ sdtk-wiki ingest <source-root> [--project-path <path>]
177
177
  sdtk-wiki ingest --source-root <source-root> [--project-path <path>]
178
178
  ```
179
179
 
180
- Runs semantic extraction over a local Markdown source root and writes the latest
180
+ Runs semantic extraction over a local Markdown source root plus supported JSON
181
+ source records and writes the latest
181
182
  `semantic-extraction-dry-run-*.json` report under `.sdtk/wiki/reports`.
182
183
 
183
184
  This is the beginner-friendly facade for `sdtk-wiki wiki extract --dry-run`.
184
185
  It does not mutate source files, personal-brain pages, raw/provenance state,
185
186
  graph outputs, web sources, Ask state, or `.sdtk/atlas`.
186
187
 
188
+ Supported JSON repository records may be an array or an object containing a
189
+ `records`, `repos`, `repositories`, `items`, or `data` array. Each record can
190
+ include `repo_url`, `owner`, `repo_name`, `message_text_snippet`,
191
+ `source_link`, `topics`, and `confidence`. JSON records are local-only evidence:
192
+ SDTK-WIKI does not fetch GitHub, Facebook, or web URLs.
193
+
187
194
  ### Lint
188
195
 
189
196
  ```powershell
@@ -235,10 +242,10 @@ boundary.
235
242
  sdtk-wiki wiki extract --project-path <path> --source-root <path> --dry-run
236
243
  ```
237
244
 
238
- Reads local Markdown sources and writes a semantic extraction JSON report under
239
- `.sdtk/wiki/reports`. The report can identify local source records, GitHub
240
- tool candidates, concept candidates, relations, comparisons, syntheses, and
241
- source-quality findings.
245
+ Reads local Markdown sources and supported JSON repository records and writes a
246
+ semantic extraction JSON report under `.sdtk/wiki/reports`. The report can
247
+ identify local source records, GitHub tool candidates, concept candidates,
248
+ relations, comparisons, syntheses, and source-quality findings.
242
249
 
243
250
  Safety:
244
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-wiki-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Project-local wiki and knowledge graph toolkit for SDTK workspaces.",
5
5
  "bin": {
6
6
  "sdtk-wiki": "bin/sdtk-wiki.js"
@@ -21,6 +21,7 @@ Output:
21
21
  Behavior:
22
22
  Findings are written to the report and do not auto-modify wiki or source files.
23
23
  Source-quality checks report mojibake-like text, missing source URLs, weak titles, duplicate repo/source candidates, low-confidence extraction, and raw/graph/provenance coverage mismatch.
24
+ Personal-brain quality metrics report frontmatter coverage, required sections, source refs, internal links, stub ratio, giant pages, density, and source evidence coverage.
24
25
  Completed lint runs exit 0 even when findings exist.
25
26
  Missing workspace or fatal report-write failures exit non-zero.
26
27
 
@@ -97,7 +97,7 @@ Usage:
97
97
  sdtk-wiki ingest --source-root <path> [--project-path <path>]
98
98
 
99
99
  Purpose:
100
- Run local semantic extraction over a Markdown source root and write a report under .sdtk/wiki/reports.
100
+ Run local semantic extraction over Markdown and supported JSON source records and write a report under .sdtk/wiki/reports.
101
101
 
102
102
  Safety:
103
103
  Local sources only.
@@ -44,7 +44,7 @@ Writes:
44
44
 
45
45
  Safety:
46
46
  Dry-run report only.
47
- Local Markdown source roots only; remote URLs are rejected.
47
+ Local Markdown and supported JSON source roots only; remote URLs are rejected.
48
48
  No personal-brain pages, managed pages, raw sources, provenance state, atlas compatibility files, web fetch, Ask, compile/apply, prune, delete, archive, or source files are modified.`);
49
49
  return 0;
50
50
  }
@@ -106,13 +106,150 @@ function mdList(values) {
106
106
  return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- None recorded.";
107
107
  }
108
108
 
109
+ function mdLinkedList(values) {
110
+ const items = asArray(values).filter(Boolean);
111
+ if (items.length === 0) return "- None recorded.";
112
+ return items.map((item) => {
113
+ const text = String(item);
114
+ if (/^https?:\/\//i.test(text)) return `- [${text}](${text})`;
115
+ return `- ${text}`;
116
+ }).join("\n");
117
+ }
118
+
119
+ function quoteBlock(value) {
120
+ const text = String(value || "").replace(/\r?\n/g, " ").trim();
121
+ return text ? `> ${text}` : "> No extracted snippet recorded in the local source.";
122
+ }
123
+
124
+ function topicLabelList(entity) {
125
+ const topics = [...new Set(asArray(entity.topics).filter(Boolean).map(String))];
126
+ return topics.length > 0 ? topics.map((topic) => `- ${topic}`).join("\n") : "- No topic labels extracted from local sources.";
127
+ }
128
+
129
+ function relatedRepoList(entity) {
130
+ const repos = asArray(entity.related_repos);
131
+ if (repos.length === 0) return "- No related repositories inferred from local topic overlap.";
132
+ return repos.map((repo) => {
133
+ const label = repo.name || repo.entity_id || "related repo";
134
+ const suffix = asArray(repo.shared_topics).length > 0 ? ` (shared topics: ${asArray(repo.shared_topics).join(", ")})` : "";
135
+ return repo.github_url ? `- [${label}](${repo.github_url})${suffix}` : `- ${label}${suffix}`;
136
+ }).join("\n");
137
+ }
138
+
139
+ function relativePersonalBrainLink(targetPagePath) {
140
+ const text = toPosix(targetPagePath || "");
141
+ const marker = ".sdtk/wiki/personal-brain/";
142
+ const idx = text.indexOf(marker);
143
+ return idx >= 0 ? text.slice(idx + marker.length) : text;
144
+ }
145
+
146
+ function conceptAxisList(concept) {
147
+ const axes = asArray(concept.key_axes);
148
+ if (axes.length === 0) return "- No key axes were inferred from local evidence.";
149
+ return axes.map((axis) => `- ${axis.name}: ${axis.evidence_count || 1} local evidence reference(s).`).join("\n");
150
+ }
151
+
152
+ function conceptImplementationList(concept) {
153
+ const entities = asArray(concept.related_entity_details);
154
+ if (entities.length === 0) return "- No implementation examples were extracted from local sources.";
155
+ return entities.slice(0, 10).map((entity) => {
156
+ const label = entity.repo_owner && entity.repo_name ? `${entity.repo_owner}/${entity.repo_name}` : (entity.name || "tool candidate");
157
+ const link = entity.target_page_path ? relativePersonalBrainLink(entity.target_page_path) : "";
158
+ const target = link ? `[${label}](../${link})` : label;
159
+ const url = entity.github_url ? `; ${entity.github_url}` : "";
160
+ return `- ${target}: ${entity.summary || `Local candidate in ${entity.category || "uncategorized"}`}${url}`;
161
+ }).join("\n");
162
+ }
163
+
164
+ function conceptPatternList(concept) {
165
+ const patterns = asArray(concept.patterns);
166
+ if (patterns.length === 0) return "- No repeated pattern was inferred from local evidence.";
167
+ return patterns.map((pattern) => `- ${pattern.pattern}: ${pattern.evidence}`).join("\n");
168
+ }
169
+
170
+ function conceptRelatedPagesList(concept) {
171
+ const pages = asArray(concept.related_entity_details)
172
+ .map((entity) => entity.target_page_path)
173
+ .filter(Boolean)
174
+ .map((target) => `../${relativePersonalBrainLink(target)}`);
175
+ return mdList([...new Set(pages)].slice(0, 12));
176
+ }
177
+
178
+ function confidenceSummaryList(summary) {
179
+ if (!summary || typeof summary !== "object") return "- No source confidence summary recorded.";
180
+ const rows = Object.entries(summary).filter(([, count]) => Number(count) > 0);
181
+ return rows.length > 0 ? rows.map(([tier, count]) => `- ${tier}: ${count}`).join("\n") : "- No source confidence summary recorded.";
182
+ }
183
+
184
+ function comparisonMatrix(comparison) {
185
+ const rows = asArray(comparison.matrix_rows);
186
+ if (rows.length === 0) return "| Candidate | Category | Local evidence | Strengths | Caveats | Recommendation |\n|---|---|---:|---|---|---|\n| None recorded | - | 0 | - | - | - |";
187
+ const table = [
188
+ "| Candidate | Category | Local evidence | Strengths | Caveats | Recommendation |",
189
+ "|---|---|---:|---|---|---|",
190
+ ];
191
+ for (const row of rows) {
192
+ const candidate = row.github_url ? `[${row.name || row.entity_id}](${row.github_url})` : (row.name || row.entity_id || "candidate");
193
+ table.push(`| ${candidate} | ${row.category || "uncategorized"} | ${row.source_ref_count || 0} | ${asArray(row.strengths).slice(0, 3).join("<br>") || "-"} | ${asArray(row.caveats).slice(0, 3).join("<br>") || "-"} | ${row.local_recommendation || "review"} |`);
194
+ }
195
+ return table.join("\n");
196
+ }
197
+
198
+ function candidateToolList(records) {
199
+ const rows = asArray(records);
200
+ if (rows.length === 0) return "- No candidate tools recorded.";
201
+ return rows.slice(0, 12).map((row) => {
202
+ const label = row.github_url ? `[${row.name || row.entity_id}](${row.github_url})` : (row.name || row.entity_id || "candidate");
203
+ return `- ${label}: ${row.local_recommendation || "review"}; confidence ${row.source_confidence || row.confidence_tier || "unknown"}.`;
204
+ }).join("\n");
205
+ }
206
+
207
+ function yamlScalar(value) {
208
+ return `"${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ").trim()}"`;
209
+ }
210
+
211
+ function yamlInlineList(values) {
212
+ const items = [...new Set(asArray(values).filter(Boolean).map(String))];
213
+ return `[${items.map(yamlScalar).join(", ")}]`;
214
+ }
215
+
216
+ function frontmatterBlock(fields) {
217
+ const lines = ["---"];
218
+ for (const [key, value] of Object.entries(fields)) {
219
+ if (Array.isArray(value)) {
220
+ lines.push(`${key}: ${yamlInlineList(value)}`);
221
+ } else {
222
+ lines.push(`${key}: ${yamlScalar(value)}`);
223
+ }
224
+ }
225
+ lines.push("---", "");
226
+ return lines.join("\n");
227
+ }
228
+
229
+ function withPersonalBrainFrontmatter(fields, body) {
230
+ return `${frontmatterBlock({
231
+ schema_version: "1",
232
+ product: "SDTK-WIKI",
233
+ managed_by: "sdtk-wiki",
234
+ status: "generated",
235
+ aliases: [],
236
+ tags: [],
237
+ related_pages: [],
238
+ source_refs: [],
239
+ confidence: "medium",
240
+ review_status: "needs_review",
241
+ ...fields,
242
+ })}${String(body || "").replace(/^\s+/, "")}`;
243
+ }
244
+
109
245
  function recordTitle(record, fallback) {
110
246
  return String(record.title || record.name || record.topic || record.entity_id || record.concept_id || record.source_id || fallback || "Generated page");
111
247
  }
112
248
 
113
- function renderSourcePage(source) {
114
- return [
115
- `# ${recordTitle(source, "Source")}`,
249
+ function renderSourcePage(source, generatedAt) {
250
+ const title = recordTitle(source, "Source");
251
+ const body = [
252
+ `# ${title}`,
116
253
  "",
117
254
  "## Summary",
118
255
  "",
@@ -136,93 +273,316 @@ function renderSourcePage(source) {
136
273
  mdList(provenanceRefsFor(source)),
137
274
  "",
138
275
  ].join("\n");
276
+ return withPersonalBrainFrontmatter({
277
+ id: source.source_id,
278
+ title,
279
+ type: "source",
280
+ created_at: generatedAt,
281
+ updated_at: generatedAt,
282
+ aliases: source.source_logical_path ? [source.source_logical_path] : [],
283
+ tags: ["source", source.source_type || "local"],
284
+ source_refs: sourceRefsFor(source),
285
+ confidence: source.source_quality && source.source_quality.low_confidence_extraction ? "low" : "medium",
286
+ review_status: source.source_quality && source.source_quality.low_confidence_extraction ? "needs_review" : "ready_for_review",
287
+ }, body);
139
288
  }
140
289
 
141
- function renderToolEntityPage(entity) {
142
- return [
143
- `# ${recordTitle(entity, "Tool entity")}`,
290
+ function renderToolEntityPage(entity, generatedAt) {
291
+ const title = recordTitle(entity, "Tool entity");
292
+ const evidenceRecords = asArray(entity.evidence_records);
293
+ const firstSnippet = asArray(entity.evidence_snippets)[0] || (evidenceRecords[0] && evidenceRecords[0].snippet);
294
+ const discoverySources = [
295
+ ...asArray(entity.source_links),
296
+ ...asArray(entity.discovery_sources),
297
+ ...evidenceRecords.map((record) => record.source_link || record.source_logical_path).filter(Boolean),
298
+ ];
299
+ const sourceCount = sourceRefsFor(entity).length;
300
+ const whyItMatters = entity.summary
301
+ ? `Local evidence presents ${title} as relevant to ${entity.category || "the extracted tool landscape"}. ${entity.summary}`
302
+ : `Local evidence links ${title} to ${entity.category || "the extracted tool landscape"} with ${sourceCount} source reference(s).`;
303
+ const whenToUse = asArray(entity.topics).length > 0
304
+ ? `Review when exploring ${asArray(entity.topics).slice(0, 4).join(", ")} options from the local source set.`
305
+ : "Review when evaluating whether this repository should become a maintained tool note.";
306
+ const overlap = asArray(entity.related_repos).length > 0
307
+ ? "Related repositories were inferred from shared local topic labels or category evidence. Treat this as a review cue, not external verification."
308
+ : "No overlap was inferred from local topic labels in this slice.";
309
+ const body = [
310
+ `# ${title}`,
144
311
  "",
145
312
  "## Summary",
146
313
  "",
147
314
  entity.summary || `Generated tool entity page for ${entity.name || entity.entity_id}.`,
148
315
  "",
149
- "## Repository",
316
+ "## Key Facts",
150
317
  "",
151
- `- owner: ${entity.repo_owner || "(missing)"}`,
152
- `- repo: ${entity.repo_name || "(missing)"}`,
153
- `- url: ${entity.github_url || "(missing)"}`,
154
- `- category: ${entity.category || "(unknown)"}`,
318
+ `- Canonical URL: ${entity.github_url || "(missing)"}`,
319
+ `- Owner: ${entity.repo_owner || "(missing)"}`,
320
+ `- Repo name: ${entity.repo_name || "(missing)"}`,
321
+ `- Category: ${entity.category || "(unknown)"}`,
322
+ `- Confidence: ${entity.confidence_tier || entity.confidence || "medium"}`,
323
+ `- Local source refs: ${sourceCount}`,
155
324
  "",
156
- "## Evidence",
325
+ "## Discovery Source",
326
+ "",
327
+ mdLinkedList([...new Set(discoverySources.filter(Boolean).map(String))]),
328
+ "",
329
+ "## Extracted Snippet",
330
+ "",
331
+ quoteBlock(firstSnippet),
332
+ "",
333
+ "## Topic Labels",
334
+ "",
335
+ topicLabelList(entity),
336
+ "",
337
+ "## Why It Matters",
338
+ "",
339
+ whyItMatters,
340
+ "",
341
+ "## When To Use",
342
+ "",
343
+ whenToUse,
344
+ "",
345
+ "## Related Repos",
346
+ "",
347
+ relatedRepoList(entity),
348
+ "",
349
+ "## Overlaps / Differences",
350
+ "",
351
+ overlap,
352
+ "",
353
+ "## Open Questions",
354
+ "",
355
+ "- Verify license, maintenance status, and project maturity before relying on this tool.",
356
+ "- Confirm whether the local topic labels match the repository's actual current scope.",
357
+ "- Decide whether this should remain a tool entity or be reclassified after review.",
358
+ "",
359
+ "## Local Evidence Records",
157
360
  "",
158
- mdList(sourceRefsFor(entity)),
361
+ evidenceRecords.length > 0
362
+ ? evidenceRecords.map((record) => [
363
+ `- source_id: ${record.source_id || "(missing)"}`,
364
+ ` - source: ${record.source_link || record.source_logical_path || "(missing)"}`,
365
+ ` - topics: ${asArray(record.topics).join(", ") || "(none)"}`,
366
+ ` - confidence: ${record.confidence_tier || record.confidence || "(unknown)"}`,
367
+ ].join("\n")).join("\n")
368
+ : "- No structured evidence records recorded.",
159
369
  "",
160
370
  "## Provenance",
161
371
  "",
162
372
  mdList(provenanceRefsFor(entity)),
163
373
  "",
164
374
  ].join("\n");
375
+ return withPersonalBrainFrontmatter({
376
+ id: entity.entity_id,
377
+ title,
378
+ type: "tool_entity",
379
+ created_at: generatedAt,
380
+ updated_at: generatedAt,
381
+ aliases: [entity.repo_name, entity.github_url].filter(Boolean),
382
+ tags: ["tool", entity.category, ...asArray(entity.topics)].filter(Boolean),
383
+ source_refs: sourceRefsFor(entity),
384
+ confidence: entity.confidence_tier || entity.confidence || "medium",
385
+ review_status: "ready_for_review",
386
+ }, body);
165
387
  }
166
388
 
167
- function renderConceptPage(concept) {
168
- return [
169
- `# ${recordTitle(concept, "Concept")}`,
389
+ function renderConceptPage(concept, generatedAt) {
390
+ const title = recordTitle(concept, "Concept");
391
+ const relatedCount = asArray(concept.related_entity_details).length || asArray(concept.related_entities).length;
392
+ const sourceCount = sourceRefsFor(concept).length;
393
+ const summary = concept.definition
394
+ || `Local sources contain ${relatedCount} tool candidate(s) and ${sourceCount} source reference(s) related to ${title}.`;
395
+ const body = [
396
+ `# ${title}`,
397
+ "",
398
+ "## Summary",
399
+ "",
400
+ summary,
401
+ "",
402
+ "## Key Axes",
403
+ "",
404
+ conceptAxisList(concept),
405
+ "",
406
+ "## Implementations / Examples",
170
407
  "",
171
- "## Definition",
408
+ conceptImplementationList(concept),
172
409
  "",
173
- concept.definition || `Generated concept page for ${concept.name || concept.concept_id}.`,
410
+ "## Patterns",
411
+ "",
412
+ conceptPatternList(concept),
413
+ "",
414
+ "## Recommendations / Caveats",
415
+ "",
416
+ "- Treat this concept page as local-evidence synthesis, not external verification.",
417
+ "- Prioritize examples with multiple source references or stronger extraction confidence.",
418
+ "- Review source-quality warnings before making architecture or tool adoption decisions.",
419
+ "",
420
+ "## Open Questions",
421
+ "",
422
+ "- Which extracted implementation is mature enough for real project adoption?",
423
+ "- Are the local topic labels accurate after human review?",
424
+ "- Should this concept be split, merged, or promoted after comparison/synthesis enrichment?",
174
425
  "",
175
426
  "## Aliases",
176
427
  "",
177
428
  mdList(concept.aliases),
178
429
  "",
179
- "## Related Entities",
430
+ "## Related Pages",
180
431
  "",
181
- mdList(concept.related_entities),
432
+ conceptRelatedPagesList(concept),
182
433
  "",
183
- "## Sources",
434
+ "## Source References",
184
435
  "",
185
436
  mdList(sourceRefsFor(concept)),
186
437
  "",
438
+ "## Provenance",
439
+ "",
440
+ mdList(provenanceRefsFor(concept)),
441
+ "",
187
442
  ].join("\n");
443
+ return withPersonalBrainFrontmatter({
444
+ id: concept.concept_id,
445
+ title,
446
+ type: "concept",
447
+ created_at: generatedAt,
448
+ updated_at: generatedAt,
449
+ aliases: concept.aliases,
450
+ tags: ["concept"],
451
+ related_pages: asArray(concept.related_entity_details).map((entity) => entity.target_page_path ? `../${relativePersonalBrainLink(entity.target_page_path)}` : null).filter(Boolean).slice(0, 12),
452
+ source_refs: sourceRefsFor(concept),
453
+ confidence: concept.confidence_tier || concept.confidence || "medium",
454
+ review_status: "ready_for_review",
455
+ }, body);
188
456
  }
189
457
 
190
- function renderComparisonPage(comparison) {
191
- return [
192
- `# ${recordTitle(comparison, "Comparison")}`,
458
+ function renderComparisonPage(comparison, generatedAt) {
459
+ const title = recordTitle(comparison, "Comparison");
460
+ const body = [
461
+ `# ${title}`,
462
+ "",
463
+ "## Summary",
464
+ "",
465
+ comparison.summary || `Local comparison for ${comparison.topic || comparison.comparison_id}.`,
466
+ "",
467
+ "## Decision Axes",
468
+ "",
469
+ conceptAxisList({ key_axes: comparison.decision_axes }),
470
+ "",
471
+ "## Comparison Matrix",
472
+ "",
473
+ comparisonMatrix(comparison),
474
+ "",
475
+ "## Candidate Tools / Repos",
476
+ "",
477
+ candidateToolList(comparison.matrix_rows),
478
+ "",
479
+ "## Recommendations",
193
480
  "",
194
- "## Compared Entities",
481
+ mdList(comparison.recommendations),
195
482
  "",
196
- mdList(comparison.compared_entities),
483
+ "## Caveats",
197
484
  "",
198
- "## Criteria",
485
+ mdList(comparison.caveats),
199
486
  "",
200
- mdList(comparison.criteria),
487
+ "## Source Confidence",
201
488
  "",
202
- "## Sources",
489
+ confidenceSummaryList(comparison.source_confidence_summary),
490
+ "",
491
+ "## Open Questions",
492
+ "",
493
+ "- Which candidate should be verified first with external source checks?",
494
+ "- Which criteria matter most for the target SDTK use case?",
495
+ "- Are any candidates duplicates, abandoned, or misclassified in the local source set?",
496
+ "",
497
+ "## Source References",
203
498
  "",
204
499
  mdList(sourceRefsFor(comparison)),
205
500
  "",
501
+ "## Provenance",
502
+ "",
503
+ mdList(provenanceRefsFor(comparison)),
504
+ "",
206
505
  ].join("\n");
506
+ return withPersonalBrainFrontmatter({
507
+ id: comparison.comparison_id,
508
+ title,
509
+ type: "comparison",
510
+ created_at: generatedAt,
511
+ updated_at: generatedAt,
512
+ aliases: [comparison.topic].filter(Boolean),
513
+ tags: ["comparison"],
514
+ related_pages: asArray(comparison.compared_entity_details).map((entity) => entity.target_page_path ? `../${relativePersonalBrainLink(entity.target_page_path)}` : null).filter(Boolean).slice(0, 12),
515
+ source_refs: sourceRefsFor(comparison),
516
+ confidence: comparison.confidence_tier || comparison.confidence || "medium",
517
+ review_status: "needs_review",
518
+ }, body);
207
519
  }
208
520
 
209
- function renderSynthesisPage(synthesis) {
210
- return [
211
- `# ${recordTitle(synthesis, "Synthesis")}`,
521
+ function renderSynthesisPage(synthesis, generatedAt) {
522
+ const title = recordTitle(synthesis, "Synthesis");
523
+ const body = [
524
+ `# ${title}`,
212
525
  "",
213
526
  "## Summary",
214
527
  "",
215
528
  synthesis.summary || `Generated synthesis page for ${synthesis.topic || synthesis.synthesis_id}.`,
216
529
  "",
217
- "## Recommendations",
530
+ "## Landscape Snapshot",
531
+ "",
532
+ candidateToolList(synthesis.candidate_tools),
533
+ "",
534
+ "## Key Patterns",
535
+ "",
536
+ conceptPatternList({ patterns: synthesis.patterns }),
537
+ "",
538
+ "## Decision Axes",
539
+ "",
540
+ conceptAxisList({ key_axes: synthesis.landscape_axes }),
541
+ "",
542
+ "## Recommended Review Path",
218
543
  "",
219
544
  mdList(synthesis.recommendations),
220
545
  "",
221
- "## Sources",
546
+ "## Caveats",
547
+ "",
548
+ mdList(synthesis.caveats),
549
+ "",
550
+ "## Source Confidence",
551
+ "",
552
+ confidenceSummaryList(synthesis.source_confidence_summary),
553
+ "",
554
+ "## Related Comparisons",
555
+ "",
556
+ synthesis.related_comparison_path ? mdList([`../${relativePersonalBrainLink(synthesis.related_comparison_path)}`]) : "- None recorded.",
557
+ "",
558
+ "## Open Questions",
559
+ "",
560
+ "- Which comparison criteria should become product acceptance gates?",
561
+ "- Which local candidates need external verification before adoption?",
562
+ "- Does this cluster need a dedicated deeper synthesis pass?",
563
+ "",
564
+ "## Source References",
222
565
  "",
223
566
  mdList(sourceRefsFor(synthesis)),
224
567
  "",
568
+ "## Provenance",
569
+ "",
570
+ mdList(provenanceRefsFor(synthesis)),
571
+ "",
225
572
  ].join("\n");
573
+ return withPersonalBrainFrontmatter({
574
+ id: synthesis.synthesis_id,
575
+ title,
576
+ type: "synthesis",
577
+ created_at: generatedAt,
578
+ updated_at: generatedAt,
579
+ aliases: [synthesis.topic].filter(Boolean),
580
+ tags: ["synthesis"],
581
+ related_pages: synthesis.related_comparison_path ? [`../${relativePersonalBrainLink(synthesis.related_comparison_path)}`] : [],
582
+ source_refs: sourceRefsFor(synthesis),
583
+ confidence: synthesis.confidence_tier || synthesis.confidence || "medium",
584
+ review_status: "needs_review",
585
+ }, body);
226
586
  }
227
587
 
228
588
  function renderRootPage(name, payload) {
@@ -235,8 +595,9 @@ function renderRootPage(name, payload) {
235
595
  graph: "Personal Brain Graph",
236
596
  log: "Personal Brain Generation Log",
237
597
  };
238
- return [
239
- `# ${titles[name] || "Personal Brain"}`,
598
+ const title = titles[name] || "Personal Brain";
599
+ const body = [
600
+ `# ${title}`,
240
601
  "",
241
602
  "## Generation Context",
242
603
  "",
@@ -257,10 +618,22 @@ function renderRootPage(name, payload) {
257
618
  "- maintenance/",
258
619
  "",
259
620
  ].join("\n");
621
+ return withPersonalBrainFrontmatter({
622
+ id: `personal_brain_${name}`,
623
+ title,
624
+ type: "root",
625
+ created_at: payload.generated_at || "",
626
+ updated_at: payload.generated_at || "",
627
+ aliases: [name],
628
+ tags: ["personal-brain", name],
629
+ source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
630
+ confidence: "medium",
631
+ review_status: "needs_review",
632
+ }, body);
260
633
  }
261
634
 
262
635
  function renderMaintenancePage(payload) {
263
- return [
636
+ const body = [
264
637
  "# Extraction Quality Review",
265
638
  "",
266
639
  "## Source Quality Findings",
@@ -272,6 +645,18 @@ function renderMaintenancePage(payload) {
272
645
  mdList(asArray(payload.unsupported_items).map((item) => `${item.item_id || "unsupported"}: ${item.reason || item.raw_observation_summary || "review required"}`)),
273
646
  "",
274
647
  ].join("\n");
648
+ return withPersonalBrainFrontmatter({
649
+ id: "maintenance_extraction_quality_review",
650
+ title: "Extraction Quality Review",
651
+ type: "maintenance",
652
+ created_at: payload.generated_at || "",
653
+ updated_at: payload.generated_at || "",
654
+ aliases: ["source quality", "extraction quality"],
655
+ tags: ["maintenance", "quality"],
656
+ source_refs: asArray(payload.sources).map((source) => source.source_id).filter(Boolean).slice(0, 50),
657
+ confidence: asArray(payload.unsupported_items).length > 0 ? "low" : "medium",
658
+ review_status: "needs_review",
659
+ }, body);
275
660
  }
276
661
 
277
662
  function pushCreatePageOperation(operations, raw) {
@@ -308,7 +693,7 @@ function operationsFromSemanticExtraction(payload) {
308
693
  source_id: source.source_id,
309
694
  source_hash: source.source_hash,
310
695
  target_page_path: source.target_page_path,
311
- content: renderSourcePage(source),
696
+ content: renderSourcePage(source, payload.generated_at),
312
697
  proposed_content_summary: `Create personal-brain source page for ${source.source_logical_path || source.source_relative_path || source.source_id}.`,
313
698
  source_refs: sourceRefsFor(source),
314
699
  provenance_refs: provenanceRefsFor(source),
@@ -325,7 +710,7 @@ function operationsFromSemanticExtraction(payload) {
325
710
  source_id: sourceId,
326
711
  source_hash: sourceHashFor(sourceById, sourceId),
327
712
  target_page_path: entity.target_page_path,
328
- content: renderToolEntityPage(entity),
713
+ content: renderToolEntityPage(entity, payload.generated_at),
329
714
  proposed_content_summary: `Create tool entity page for ${entity.name || entity.entity_id}.`,
330
715
  source_refs: sourceRefsFor(entity),
331
716
  provenance_refs: provenanceRefsFor(entity),
@@ -361,7 +746,7 @@ function operationsFromSemanticExtraction(payload) {
361
746
  source_id: sourceId,
362
747
  source_hash: sourceHashFor(sourceById, sourceId),
363
748
  target_page_path: concept.target_page_path,
364
- content: renderConceptPage(concept),
749
+ content: renderConceptPage(concept, payload.generated_at),
365
750
  proposed_content_summary: `Create concept page for ${concept.name || concept.concept_id}.`,
366
751
  source_refs: sourceRefsFor(concept),
367
752
  provenance_refs: provenanceRefsFor(concept),
@@ -378,7 +763,7 @@ function operationsFromSemanticExtraction(payload) {
378
763
  source_id: sourceId,
379
764
  source_hash: sourceHashFor(sourceById, sourceId),
380
765
  target_page_path: comparison.target_page_path,
381
- content: renderComparisonPage(comparison),
766
+ content: renderComparisonPage(comparison, payload.generated_at),
382
767
  proposed_content_summary: `Create comparison page for ${comparison.topic || comparison.comparison_id}.`,
383
768
  source_refs: sourceRefsFor(comparison),
384
769
  provenance_refs: provenanceRefsFor(comparison),
@@ -395,7 +780,7 @@ function operationsFromSemanticExtraction(payload) {
395
780
  source_id: sourceId,
396
781
  source_hash: sourceHashFor(sourceById, sourceId),
397
782
  target_page_path: synthesis.target_page_path,
398
- content: renderSynthesisPage(synthesis),
783
+ content: renderSynthesisPage(synthesis, payload.generated_at),
399
784
  proposed_content_summary: `Create synthesis page for ${synthesis.topic || synthesis.synthesis_id}.`,
400
785
  source_refs: sourceRefsFor(synthesis),
401
786
  provenance_refs: provenanceRefsFor(synthesis),