diff --git a/plans/research-core-adaptation-v2/milestone-0-execution-dag.md b/plans/research-core-adaptation-v2/milestone-0-execution-dag.md new file mode 100644 index 000000000..db883387b --- /dev/null +++ b/plans/research-core-adaptation-v2/milestone-0-execution-dag.md @@ -0,0 +1,69 @@ +# Milestone 0 Execution DAG + +```yaml +objective: reconcile Research Core Adaptation V2 with the checked-out Nashsu core without changing production behavior +root_owner: current Codex root session +requested_model: GPT-5.6 Sol High +model_verification: unavailable in this task surface + +nodes: + - id: package-intake + kind: root + depends_on: [] + outcome: verify the complete handoff and extract its constraints and code anchors + status: completed + + - id: repository-baseline + kind: root + depends_on: [package-intake] + outcome: acquire the canonical repository, pin the exact version, and create a feature branch + status: completed + + - id: scout-frontend-ingest + kind: root + archetype: scout-equivalent + depends_on: [repository-baseline] + outcome: map templates, schema routing, source import, queues, review persistence, page merge, and tests + write_scope: read-only + status: completed + + - id: scout-backend-api + kind: root + archetype: scout-equivalent + depends_on: [repository-baseline] + outcome: map Rust persistence, local API routing, graph/search behavior, and project resolution + write_scope: read-only + status: completed + + - id: scout-mcp-models + kind: root + archetype: scout-equivalent + depends_on: [repository-baseline] + outcome: map MCP registration and binding, model routing, and integration-test surfaces + write_scope: read-only + status: completed + + - id: baseline-validation + kind: root + depends_on: [scout-frontend-ingest, scout-backend-api, scout-mcp-models] + outcome: run existing builds and tests and attempt the canonical desktop launch + status: completed-with-findings + + - id: root-reconciliation + kind: root + depends_on: [baseline-validation] + outcome: reconcile reports, identify contradicted assumptions, and choose a proposed PR sequence + status: completed + + - id: milestone-0-report + kind: root + depends_on: [root-reconciliation] + outcome: publish the root-owned intake report; make no production change + status: completed +``` + +Delegation note: the installed Codex CLI reports stable `multi_agent`, but this +task runtime exposes no spawn/subagent tool. Following the handoff fallback, the +three non-overlapping read-only scouting passes were performed sequentially by +the root. No alternate orchestration runtime was installed. + diff --git a/plans/research-core-adaptation-v2/milestone-0-fresh-intake.md b/plans/research-core-adaptation-v2/milestone-0-fresh-intake.md new file mode 100644 index 000000000..4d3138aee --- /dev/null +++ b/plans/research-core-adaptation-v2/milestone-0-fresh-intake.md @@ -0,0 +1,153 @@ +# Nashsu Research Core Adaptation V2 — Milestone 0 Fresh Intake + +## Status + +The implementation map is coherent enough to scope later work, but the baseline +is not fully green. No production code, dependency, lockfile, schema, or runtime +was changed. Milestone 1 was not started. + +Nashsu/llm_wiki remains the sole canonical product and knowledge core. +AtomicStrata, AutoSci, OpenKB, Synto, Open Knowledge CLI, llm-wiki-okf, OKF, and +additional orchestration runtimes remain outside the implementation boundary. +OpenDeepWiki remains a future lazy, repository-scoped, non-canonical adjunct. + +## Exact baseline + +- Repository: `https://github.com/nashsu/llm_wiki.git` +- Baseline branch: `main` +- Intake branch: `codex/research-core-m0-intake` +- Commit: `969e7e8d9e2903af0e3ed0be85c97639511e845c` +- Describe: `v0.6.4-8-g969e7e8` +- Commit: `fix: harden provider and import routing` +- Commit date: `2026-07-16T17:04:21+08:00` +- App/package/Cargo/Tauri version: `0.6.4` +- MCP package version: `0.4.25` +- Handoff: all 25 manifest entries matched their declared SHA-256 and byte size. + +The workspace initially contained no Nashsu checkout, so the current upstream +repository was cloned before inspection. + +## Orchestration and capability check + +- Codex CLI: `0.142.5` +- CLI feature state: `multi_agent = stable/true`; + `multi_agent_v2 = under-development/false`; + `enable_fanout = under-development/false`. +- Official Codex documentation describes direct subagents, a default nesting + depth of one, and built-in `worker` and `explorer` roles. +- This task runtime exposed no spawn/subagent tool, so no worker was delegated. + The root performed three sequential, isolated, read-only scout-equivalent + passes: frontend/ingest, Rust/API, and MCP/model routing. +- The requested GPT-5.6 Sol High identity/reasoning setting is not inspectable + or configurable from this task surface, so it cannot be independently + verified. + +The compact DAG is in `milestone-0-execution-dag.md`. + +## Root implementation map + +| Capability | Current authority and behavior | Adaptation consequence | +| --- | --- | --- | +| Template materialization | `src/lib/templates.ts` defines schema, purpose, and extra directories. `src-tauri/src/commands/project.rs` creates the base project; `src/components/project/create-project-dialog.tsx` then writes the selected template files/directories. | Research Profile v2 must update the existing two-stage path, not add a creator. | +| Schema routing | `src/lib/wiki-schema.ts` parses the `## Page Types` table dynamically and maps type to directory. In ingest, generated blocks are checked against this routing before write. Other schema prose mostly guides prompts. | Add `repository` to the Research table; do not build a generic schema engine. | +| Fixed page-type surfaces | `src/lib/wiki-page-types.ts`, knowledge-tree and activity icon maps, `src/lib/wiki-type-style.ts`, and i18n labels contain known-type lists. Graph affinity and review-page creation also have fixed maps but safe fallbacks. | Milestone 1 must update user-visible/generation lists and test dynamic fallback surfaces. | +| Source import | `src/lib/source-lifecycle.ts` copies files into `raw/sources`, preprocesses them, and immediately calls `enqueueSourceIngest` when an ingest model is usable. | Candidate registration must be inserted before enqueue for Research projects only; reuse import/copy logic. | +| Ingest queue | `src/lib/ingest-queue.ts::enqueueBatch`; state is project-scoped in `.llm-wiki/ingest-queue.json`. It supports pause, restart recovery, retry, cancellation, stable project identity, and serial processing. | Candidate batch include/watch/exclude should feed this queue; no second queue. | +| Ingest and merge | `src/lib/ingest.ts` performs analysis/generation, validates schema routing, creates review items, and writes through `mergePageContent`. `src/lib/page-merge.ts` unions `sources`, `tags`, and `related`, locks `type`, `title`, and `created`, and writes history backups. | Corpus bootstrap and Research writes must use normal ingest/merge/review behavior. | +| Review persistence | `src/stores/review-store.ts`; `.llm-wiki/review.json` through `src/lib/persist.ts` and autosave. The Rust API reads and patches the same file. | Extend existing review/activity patterns; do not add a review store. | +| Search/index | Rust search in `src-tauri/src/commands/search.rs` backs desktop invoke and API search. Index rebuild groups pages dynamically from frontmatter. Embeddings are optional. | `repository` should become searchable without a new index; add acceptance coverage. | +| Graph | Desktop uses the TypeScript wiki graph path. API/MCP use a separate Rust `build_graph` in `api_server.rs`; the API currently excludes `query` nodes. Both derive types dynamically. | Repository visibility should work, but graph parity is an existing architectural discrepancy to resolve or document. | +| Model routing | Desktop chat and ingest use `src/lib/llm-task-routing.ts` with separate `chat` and `ingest` presets. Image captioning is passed the ingest-side configuration. Embedding has separate embedding settings. Rust API/MCP chat uses `project_llm_config`, global config, and project overrides but does not apply `taskModelRouting.chatPresetId`. | Preserve current desktop routing; add parity tests before relying on MCP chat for acceptance. | +| Local API | `src-tauri/src/api_server.rs`, port `19828`, prefix `/api/v1`; exposes health, projects, files, reviews, search, graph, source rescan, chat, and cancellation with auth, bind, rate, size, and path controls. | Extend the API only when a required Research operation cannot use an existing endpoint. | +| MCP | `mcp-server` is a thin HTTP client over the desktop API. It registers 10 tools: status, projects, set-project, files, read-file, reviews, search, chat, graph, and rescan-sources. | Keep MCP thin; no filesystem scan or independent retrieval implementation. | +| Project selection | Stable UUID in `.llm-wiki/project.json`; global `app-state.json` registry; API accepts stable ID, exact path, or `current`; MCP can pin a process to a project and rejects conflicting overrides. | Candidate and corpus state must key by stable project identity and remain project-scoped. | + +Representative `.llm-wiki/` state includes `project.json`, +`ingest-queue.json`, `ingest-cache.json`, `ingest-progress/`, +`ingest-warnings.log`, `review.json`, `lint.json`, `file-snapshot.json`, +`file-change-queue.json`, `dedup-queue.json`, `dedup-not-duplicates.json`, +`scheduled-import-db.json`, `image-caption-cache.json`, `conversations.json`, +`chats/`, `chat-preferences.json`, `history/`, `page-history/`, `lancedb/`, and +project `skills/`. Candidate screening needs one new small project-scoped file, +not a replacement for any of these. + +## Handoff anchor reconciliation + +All named high-value anchors still exist. The paths are useful, but three +assumptions require correction: + +1. Schema routing is already substantially dynamic. Adding a page type does not + require Rust index/search/graph type registration, although fixed generation + and UI presentation lists still need updates. +2. Desktop and API/MCP graph generation are not one shared backend. The API has + its own Rust graph builder and intentionally drops `query` nodes. +3. Desktop task-model routing and API/MCP chat routing are not equivalent. The + Rust API does not currently consume the chat task preset. + +Additional baseline findings: + +- The Research template's shared frontmatter example lists only base types even + though the same schema defines `thesis`, `methodology`, and `finding`. +- Import currently means copy plus immediate enqueue; there is no candidate + registration/screening state. +- `npm ci` at the root is not reproducible from the checked-in lockfile: + `@emnapi/core@1.11.2` and `@emnapi/runtime@1.11.2` are missing and + `@emnapi/wasi-threads` resolves at an invalid locked version. Dependencies + were materialized with `npm install --package-lock=false`; the lockfile was + preserved. +- Existing tests strongly cover page merge preservation and TypeScript task + routing. Research template form state is tested, but full Research project + materialization is not. MCP client tests prove HTTP calls, but there is no + explicit architectural regression test forbidding direct filesystem scans. + +## Validation evidence + +| Command | Result | +| --- | --- | +| `npm run test:mocks -- --reporter=dot` | PASS: 117 files, 1,707 tests. | +| `npm run build` | PASS: TypeScript and Vite production build; existing chunk/dynamic-import warnings only. | +| `npm run mcp:test` | PASS: 20 tests. | +| `npm run test:llm` | FAIL: 6 failed, 3 passed, 70 skipped. Three fake embedding-server tests receive `undefined`; three Origin tests send `http://localhost` instead of the endpoint origin. External provider suites were skipped because credentials/endpoints were unavailable. | +| `cargo test -q` | PASS: 338 passed, 1 ignored; existing compiler warnings only. | +| `npm run tauri dev` | PASS: desktop binary launched; Vite served `http://localhost:1420/`; Clip Server listened on `127.0.0.1:19827`; API listened on `127.0.0.1:19828/api/v1`. | +| Local API smoke | PASS: health 200 (`version: 0.6.4`), CORS preflight 204. Projects returned the expected 401 because the fresh state requires auth but has no token configured; MCP is disabled. | + +The first Rust/launch attempts failed when the data volume had about 118 MiB +free. After space was restored, both completed. The build consumed most of the +new headroom and left about 1 GiB free. + +The computer-use skill is installed, but this task surface does not expose its +required action tool. The app and services were launched and probed, but +click-driven creation/import/review/search/graph flows were therefore not +manually exercised. + +## Proposed PR sequence + +1. **M0 baseline stabilization:** add the canonical-core ADR, repair root + lockfile reproducibility, document the full local stack, add missing Research + materialization/MCP-boundary regressions, and decide whether the six + real-contract failures are code or stale test expectations. Add an + authenticated full-stack fixture for API/MCP acceptance. +2. **M1 Research Profile v2:** add only `repository` and + `wiki/repositories/`; keep flat frontmatter; update the fixed surfaces above; + add creation, routing, rendering, search/graph visibility, and safe-merge + tests. +3. **M2 candidate intake:** add a small Research-only project store and + include/watch/exclude UI; register without ingest; batch-enqueue through the + existing queue. +4. **M3 screening and progress:** structured screening output, migration and + restart tests, and existing activity/review integration. +5. **M4 bounded corpus bootstrap:** inventory, group, cluster synthesis, and + global synthesis through normal compiled pages and merge/review. +6. **M5 MCP/API parity:** expose only missing Research operations, resolve + graph/model-routing parity, and keep MCP as an API client. +7. **M6 OpenDeepWiki adjunct:** lazy repository registration and + commit-pinned evidence only; never canonical. + +## Root decision and remaining uncertainty + +The extension seams are clear and no new core runtime or generic abstraction is +justified. Before Milestone 1, disposition the six real-contract failures and +perform one authenticated, click-driven baseline pass through current +Research creation, import, review, search, graph, API, and MCP behavior. The +remaining evidence gaps are not permission to broaden Milestone 1. diff --git a/plans/research-core-adaptation-v2/milestone-1-execution-dag.md b/plans/research-core-adaptation-v2/milestone-1-execution-dag.md new file mode 100644 index 000000000..0d78366e6 --- /dev/null +++ b/plans/research-core-adaptation-v2/milestone-1-execution-dag.md @@ -0,0 +1,62 @@ +# Milestone 1 Execution DAG + +```yaml +objective: extend the existing Research template with the repository page type and directory while preserving all existing projects and templates +root_owner: current Codex root session + +nodes: + - id: root-scope + kind: root + depends_on: [] + outcome: pin accepted seams and exact fixed page-type surfaces from Milestone 0 + status: completed + + - id: tests-red + kind: root + archetype: verifier + depends_on: [root-scope] + outcome: add failing behavior tests for creation, routing, rendering, search/graph visibility, and safe merge + write_scope: test files only + status: completed + + - id: profile-builder + kind: root + archetype: builder + depends_on: [tests-red] + outcome: add only the Research repository type/directory and update required fixed surfaces + write_scope: accepted template and page-type presentation files + status: completed + + - id: focused-verification + kind: root + archetype: verifier + depends_on: [profile-builder] + outcome: pass focused tests and typechecking + write_scope: read-only except generated test/build artifacts + status: completed + + - id: independent-review + kind: root + archetype: reviewer + depends_on: [focused-verification] + outcome: review the complete diff independently against repository standards and the handoff + write_scope: read-only + status: completed + + - id: root-integration + kind: root + depends_on: [independent-review] + outcome: resolve findings and run the full verification matrix + status: completed + + - id: commit + kind: root + depends_on: [root-integration] + outcome: commit the accepted Milestone 0 evidence and Milestone 1 implementation + status: completed +``` + +Delegation note: this task runtime exposes no subagent/spawn tool. The root +therefore collapses builder, verifier, and reviewer roles into explicit +sequential passes with non-overlapping phases. Architecture and integration +remain root-owned. diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index 5cf59f503..63f3f9a43 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -2184,6 +2184,39 @@ mod tests { assert!(!is_public_project_rel("wiki/.draft.md")); } + #[test] + fn api_graph_includes_repository_pages_with_their_declared_type() { + let root = test_project_dir(); + let repository_dir = root.join("wiki/repositories"); + let concept_dir = root.join("wiki/concepts"); + fs::create_dir_all(&repository_dir).unwrap(); + fs::create_dir_all(&concept_dir).unwrap(); + fs::write( + repository_dir.join("nashsu-llm-wiki.md"), + "---\ntype: repository\ntitle: Nashsu / llm_wiki\n---\n\n[[knowledge-core]]", + ) + .unwrap(); + fs::write( + concept_dir.join("knowledge-core.md"), + "---\ntype: concept\ntitle: Knowledge Core\n---\n\n[[nashsu-llm-wiki]]", + ) + .unwrap(); + + let (nodes, edges) = build_graph(root.to_str().unwrap()).unwrap(); + let repository = nodes + .iter() + .find(|node| node.id == "nashsu-llm-wiki") + .unwrap(); + + assert_eq!(repository.node_type, "repository"); + assert_eq!( + repository.path, + "wiki/repositories/nashsu-llm-wiki.md" + ); + assert_eq!(edges.len(), 1); + let _ = fs::remove_dir_all(root); + } + #[test] fn review_query_defaults_to_unresolved_items() { let root = test_project_dir(); diff --git a/src-tauri/src/commands/search.rs b/src-tauri/src/commands/search.rs index 654bf3b0f..b2de6e36b 100644 --- a/src-tauri/src/commands/search.rs +++ b/src-tauri/src/commands/search.rs @@ -2038,6 +2038,34 @@ mod tests { let _ = fs::remove_dir_all(root); } + #[tokio::test] + async fn keyword_search_includes_repository_pages_in_custom_wiki_directories() { + let root = tmp_project(); + write_page( + &root, + "wiki/repositories/nashsu-llm-wiki.md", + "---\ntype: repository\ntitle: Nashsu / llm_wiki\n---\n\nCanonical research knowledge core implementation.", + ); + + let out = search_project_inner( + root.to_string_lossy().to_string(), + "canonical research knowledge core".into(), + 20, + false, + None, + ) + .await + .unwrap(); + + assert_eq!(out.results.len(), 1); + assert_eq!( + out.results[0].path, + "wiki/repositories/nashsu-llm-wiki.md" + ); + assert_eq!(out.results[0].title, "Nashsu / llm_wiki"); + let _ = fs::remove_dir_all(root); + } + #[tokio::test] async fn keyword_search_always_blends_available_graph_neighbors() { let root = tmp_project(); diff --git a/src/components/chat/chat-message.tsx b/src/components/chat/chat-message.tsx index 7fc7913f5..1b3756275 100644 --- a/src/components/chat/chat-message.tsx +++ b/src/components/chat/chat-message.tsx @@ -9,7 +9,7 @@ import "katex/dist/katex.min.css" import { Bot, User, FileText, BookmarkPlus, ChevronDown, ChevronRight, RefreshCw, Copy, Check, Users, Lightbulb, BookOpen, HelpCircle, GitMerge, BarChart3, Layout, Globe, - TrendingUp, Target, Sparkles, Image as ImageIcon, FileSearch, Terminal, + TrendingUp, Target, GitBranch, Sparkles, Image as ImageIcon, FileSearch, Terminal, } from "lucide-react" import { openUrl } from "@tauri-apps/plugin-opener" import { useWikiStore } from "@/stores/wiki-store" @@ -655,6 +655,7 @@ const REF_TYPE_CONFIG: Record finding: { icon: TrendingUp, color: "text-purple-500" }, thesis: { icon: Target, color: "text-rose-500" }, methodology: { icon: BookOpen, color: "text-teal-500" }, + repository: { icon: GitBranch, color: "text-sky-500" }, overview: { icon: Layout, color: "text-yellow-500" }, clip: { icon: Globe, color: "text-blue-400" }, external: { icon: Globe, color: "text-sky-500" }, diff --git a/src/components/graph/graph-view.tsx b/src/components/graph/graph-view.tsx index dffa28278..6d94dcf24 100644 --- a/src/components/graph/graph-view.tsx +++ b/src/components/graph/graph-view.tsx @@ -35,6 +35,7 @@ const NODE_TYPE_COLORS: Record = { finding: "#a855f7", // purple-500 thesis: "#f43f5e", // rose-500 methodology: "#14b8a6", // teal-500 + repository: "#38bdf8", // sky-400 other: "#94a3b8", // slate-400 } @@ -728,6 +729,7 @@ export function GraphView() { finding: t("graph.nodeTypeLabels.finding"), thesis: t("graph.nodeTypeLabels.thesis"), methodology: t("graph.nodeTypeLabels.methodology"), + repository: t("graph.nodeTypeLabels.repository"), other: t("graph.nodeTypeLabels.other"), }) }, [t]) diff --git a/src/components/layout/activity-panel.tsx b/src/components/layout/activity-panel.tsx index 55b81e235..0e2e1ae49 100644 --- a/src/components/layout/activity-panel.tsx +++ b/src/components/layout/activity-panel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react" import { ChevronUp, ChevronDown, Loader2, CheckCircle2, AlertCircle, FileText, Users, Lightbulb, BookOpen, GitMerge, BarChart3, HelpCircle, Layout, - RotateCcw, X, Clock, TrendingUp, Target, Pause, Play, + RotateCcw, X, Clock, TrendingUp, Target, GitBranch, Pause, Play, } from "lucide-react" import { useTranslation } from "react-i18next" import { useActivityStore, type ActivityItem } from "@/stores/activity-store" @@ -38,6 +38,7 @@ const FILE_TYPE_ICONS: Record = { findings: TrendingUp, thesis: Target, methodology: BookOpen, + repositories: GitBranch, overview: Layout, } @@ -51,6 +52,7 @@ const WIKI_TYPE_ICON_KEYS: Record = { finding: "findings", thesis: "thesis", methodology: "methodology", + repository: "repositories", overview: "overview", } diff --git a/src/components/layout/knowledge-tree.tsx b/src/components/layout/knowledge-tree.tsx index fd780b81c..3b768fdc9 100644 --- a/src/components/layout/knowledge-tree.tsx +++ b/src/components/layout/knowledge-tree.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react" import { - FileText, Users, Lightbulb, BookOpen, HelpCircle, GitMerge, BarChart3, TrendingUp, Target, ChevronRight, ChevronDown, Layout, Globe, Trash2, + FileText, Users, Lightbulb, BookOpen, HelpCircle, GitMerge, BarChart3, TrendingUp, Target, GitBranch, ChevronRight, ChevronDown, Layout, Globe, Trash2, } from "lucide-react" import { ScrollArea } from "@/components/ui/scroll-area" import { Button } from "@/components/ui/button" @@ -27,12 +27,13 @@ const TYPE_CONFIG: Record> = { - entity: { concept: 1.2, entity: 0.8, source: 1.0, synthesis: 1.0, query: 0.8 }, - concept: { entity: 1.2, concept: 0.8, source: 1.0, synthesis: 1.2, query: 1.0 }, - source: { entity: 1.0, concept: 1.0, source: 0.5, query: 0.8, synthesis: 1.0 }, - query: { concept: 1.0, entity: 0.8, synthesis: 1.0, source: 0.8, query: 0.5 }, - synthesis: { concept: 1.2, entity: 1.0, source: 1.0, query: 1.0, synthesis: 0.8 }, + entity: { concept: 1.2, entity: 0.8, source: 1.0, synthesis: 1.0, query: 0.8, repository: 0.8 }, + concept: { entity: 1.2, concept: 0.8, source: 1.0, synthesis: 1.2, query: 1.0, repository: 1.0 }, + source: { entity: 1.0, concept: 1.0, source: 0.5, query: 0.8, synthesis: 1.0, repository: 1.2 }, + query: { concept: 1.0, entity: 0.8, synthesis: 1.0, source: 0.8, query: 0.5, repository: 0.8 }, + synthesis: { concept: 1.2, entity: 1.0, source: 1.0, query: 1.0, synthesis: 0.8, repository: 1.0 }, + repository: { source: 1.2, concept: 1.0, entity: 0.8, synthesis: 1.0, repository: 0.5 }, } // --------------------------------------------------------------------------- diff --git a/src/lib/ingest.ts b/src/lib/ingest.ts index ef42a4a46..b80e77a3a 100644 --- a/src/lib/ingest.ts +++ b/src/lib/ingest.ts @@ -2353,7 +2353,7 @@ function buildAggregateRepairPrompt( languageRule(sourceContext), "", "Generate ONLY the requested aggregate FILE blocks listed below.", - "Do not generate entity, concept, source summary, query, comparison, or synthesis pages.", + "Do not generate entity, concept, source summary, query, comparison, synthesis, or repository pages.", "", "Requested paths:", ...paths.map((path) => `- ${path}`), diff --git a/src/lib/page-merge.test.ts b/src/lib/page-merge.test.ts index 02bce00de..bd998a88d 100644 --- a/src/lib/page-merge.test.ts +++ b/src/lib/page-merge.test.ts @@ -182,6 +182,51 @@ describe("mergePageContent — LLM merge", () => { expect(out).toContain("type: entity") expect(out).not.toContain("type: concept") }) + + it("preserves flat repository identity and analysis metadata", async () => { + const existing = PAGE( + [ + "type: repository", + "title: Nashsu / llm_wiki", + "created: 2026-07-16", + 'repo_url: "https://github.com/nashsu/llm_wiki"', + 'repo_owner: "nashsu"', + 'repo_name: "llm_wiki"', + 'branch: "main"', + 'pinned_commit: "969e7e8"', + "analysis_provider: opendeepwiki", + "analysis_status: unregistered", + 'external_repository_id: ""', + 'last_analyzed_at: ""', + "tags: [implementation]", + ].join("\n"), + "Existing verified repository notes with enough detail to retain.", + ) + const incoming = PAGE( + "type: repository\ntitle: Nashsu / llm_wiki\ntags: [typescript]", + "Incoming repository notes with additional implementation details.", + ) + const merger = vi.fn().mockResolvedValue(PAGE( + "type: repository\ntitle: Nashsu / llm_wiki\ntags: [typescript]", + "Existing verified repository notes with enough detail to retain. Incoming repository notes with additional implementation details.", + )) + + const out = await mergePageContent(incoming, existing, merger, { + ...baseOpts, + pagePath: "wiki/repositories/nashsu-llm-wiki.md", + }) + + expect(out).toContain('repo_url: "https://github.com/nashsu/llm_wiki"') + expect(out).toContain('repo_owner: "nashsu"') + expect(out).toContain('repo_name: "llm_wiki"') + expect(out).toContain('branch: "main"') + expect(out).toContain('pinned_commit: "969e7e8"') + expect(out).toContain("analysis_provider: opendeepwiki") + expect(out).toContain("analysis_status: unregistered") + expect(out).toContain('external_repository_id: ""') + expect(out).toContain('last_analyzed_at: ""') + expect(out).toMatch(/tags:\s*\[\s*"implementation",\s*"typescript"\s*\]/) + }) }) // ────────────────────────────────────────────────────────────────── @@ -209,6 +254,35 @@ describe("mergePageContent — LLM failure fallback", () => { expect(out).toContain("new body content") }) + it("preserves repository metadata when the LLM merge falls back", async () => { + const existing = PAGE( + [ + "type: repository", + "title: Nashsu / llm_wiki", + 'repo_url: "https://github.com/nashsu/llm_wiki"', + 'pinned_commit: "969e7e8"', + "analysis_status: ready", + "tags: [implementation]", + ].join("\n"), + "Existing repository evidence.", + ) + const incoming = PAGE( + "type: repository\ntitle: Nashsu / llm_wiki\ntags: [typescript]", + "Incoming repository evidence.", + ) + const merger = vi.fn().mockRejectedValue(new Error("offline")) + + const out = await mergePageContent(incoming, existing, merger, { + ...baseOpts, + pagePath: "wiki/repositories/nashsu-llm-wiki.md", + }) + + expect(out).toContain('repo_url: "https://github.com/nashsu/llm_wiki"') + expect(out).toContain('pinned_commit: "969e7e8"') + expect(out).toContain("analysis_status: ready") + expect(out).toMatch(/tags:\s*\[\s*"implementation",\s*"typescript"\s*\]/) + }) + it("rejects LLM output that shrinks body below 70% of max(old, new)", async () => { const longBody = "long body content ".repeat(200) // ~3600 chars const existing = PAGE("type: entity\ntitle: Foo", longBody) diff --git a/src/lib/page-merge.ts b/src/lib/page-merge.ts index aabc4898c..5ad9508f5 100644 --- a/src/lib/page-merge.ts +++ b/src/lib/page-merge.ts @@ -42,6 +42,17 @@ const UNION_FIELDS = ["sources", "tags", "related"] as const * separately. */ const LOCKED_FIELDS = ["type", "title", "created"] as const +const REPOSITORY_PRESERVED_FIELDS = [ + "repo_url", + "repo_owner", + "repo_name", + "branch", + "pinned_commit", + "analysis_provider", + "analysis_status", + "external_repository_id", + "last_analyzed_at", +] as const /** * Body length safety threshold. If the LLM's merged body is shorter @@ -105,7 +116,7 @@ export async function mergePageContent( if (newContent === existingContent) return existingContent // Step 1 — always-on: union the array frontmatter fields. - const arrayMerged = mergeArrayFieldsIntoContent( + let arrayMerged = mergeArrayFieldsIntoContent( newContent, existingContent, [...UNION_FIELDS], @@ -115,6 +126,11 @@ export async function mergePageContent( // differed). The array merge has already produced the right output; // skip the LLM. const oldParsed = parseFrontmatter(existingContent) + arrayMerged = preserveRepositoryFields( + arrayMerged, + existingContent, + oldParsed.frontmatter?.type, + ) const arrayMergedParsed = parseFrontmatter(arrayMerged) if (opts.replaceExistingBody) { await tryBackup(opts, existingContent) @@ -187,6 +203,7 @@ export async function mergePageContent( final = setFrontmatterScalar(final, field, existingValue) } } + final = preserveRepositoryFields(final, existingContent, oldParsed.frontmatter?.type) // Re-apply union merges on top of the LLM's frontmatter using // arrayMerged (which is already existing+incoming union) as the // reference. The LLM may have output a subset of array values — @@ -218,6 +235,34 @@ function defaultToday(): string { return new Date().toISOString().slice(0, 10) } +function preserveRepositoryFields( + content: string, + existingContent: string, + existingType: unknown, +): string { + if (existingType !== "repository") return content + + let preserved = content + for (const field of REPOSITORY_PRESERVED_FIELDS) { + const rawValue = getFrontmatterScalarRaw(existingContent, field) + if (rawValue !== null) { + preserved = setFrontmatterScalar(preserved, field, rawValue) + } + } + return preserved +} + +function getFrontmatterScalarRaw( + content: string, + fieldName: string, +): string | null { + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/) + if (!fmMatch) return null + const escapedName = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + const lineMatch = fmMatch[1].match(new RegExp(`^${escapedName}:\\s*([^\\n]*)$`, "m")) + return lineMatch?.[1] ?? null +} + /** * Set a scalar frontmatter field to `value` in the inline form * `field: value`. If the field already exists in the frontmatter, diff --git a/src/lib/project-template.test.ts b/src/lib/project-template.test.ts new file mode 100644 index 000000000..0c6edd331 --- /dev/null +++ b/src/lib/project-template.test.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const fsMocks = vi.hoisted(() => ({ + writeFile: vi.fn(), + createDirectory: vi.fn(), +})) + +vi.mock("@/commands/fs", () => fsMocks) + +import { materializeProjectTemplate } from "./project-template" + +beforeEach(() => { + fsMocks.writeFile.mockReset() + fsMocks.createDirectory.mockReset() +}) + +describe("materializeProjectTemplate", () => { + it("creates the Research repository directory and writes the v2 schema", async () => { + await materializeProjectTemplate("/project", "research") + + expect(fsMocks.createDirectory).toHaveBeenCalledWith("/project/wiki/repositories") + expect(fsMocks.writeFile).toHaveBeenCalledWith( + "/project/schema.md", + expect.stringContaining("| repository | wiki/repositories/ |"), + ) + + const schema = fsMocks.writeFile.mock.calls.find(([path]) => path === "/project/schema.md")?.[1] + expect(schema).toContain("type: repository") + expect(schema).toContain("source_kind: paper") + expect(schema).toContain('source_papers: ["[[paper-a]]", "[[paper-b]]"]') + expect(schema).toContain("evidence_kind: direct") + expect(schema).toContain('repo_url: ""') + expect(schema).toContain('pinned_commit: ""') + expect(schema).not.toContain("repository:\n") + }) + + it("does not add the Research repository type to other templates", async () => { + await materializeProjectTemplate("/project", "general") + + expect(fsMocks.createDirectory).not.toHaveBeenCalledWith("/project/wiki/repositories") + const schema = fsMocks.writeFile.mock.calls.find(([path]) => path === "/project/schema.md")?.[1] + expect(schema).not.toContain("| repository |") + }) +}) diff --git a/src/lib/project-template.ts b/src/lib/project-template.ts new file mode 100644 index 000000000..50e4270b7 --- /dev/null +++ b/src/lib/project-template.ts @@ -0,0 +1,18 @@ +import { createDirectory, writeFile } from "@/commands/fs" +import { normalizePath } from "@/lib/path-utils" +import { getTemplate } from "@/lib/templates" + +export async function materializeProjectTemplate( + projectPath: string, + templateId: string, +): Promise { + const pp = normalizePath(projectPath) + const template = getTemplate(templateId) + + await writeFile(`${pp}/schema.md`, template.schema) + await writeFile(`${pp}/purpose.md`, template.purpose) + for (const dir of template.extraDirs) { + await createDirectory(`${pp}/${dir}`) + } +} + diff --git a/src/lib/research-profile-v2.test.ts b/src/lib/research-profile-v2.test.ts new file mode 100644 index 000000000..d65cccc12 --- /dev/null +++ b/src/lib/research-profile-v2.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest" +import en from "@/i18n/en.json" +import zh from "@/i18n/zh.json" + +describe("Research Profile v2 translations", () => { + it("labels repository pages across sidebar, activity, and graph rendering", () => { + expect(en.sidebar.typeLabels.repository).toBe("Repositories") + expect(en.activity.fileTypes.repository).toBe("Repository") + expect(en.graph.nodeTypeLabels.repository).toBe("Repository") + + expect(zh.sidebar.typeLabels.repository).toBe("代码仓库") + expect(zh.activity.fileTypes.repository).toBe("代码仓库") + expect(zh.graph.nodeTypeLabels.repository).toBe("代码仓库") + }) +}) + diff --git a/src/lib/review-create-page.test.ts b/src/lib/review-create-page.test.ts index 88f7c4828..294776e83 100644 --- a/src/lib/review-create-page.test.ts +++ b/src/lib/review-create-page.test.ts @@ -46,5 +46,19 @@ describe("createReviewPageDrafts", () => { { title: "Policy version gap", pageType: "query", dir: "queries" }, ]) }) -}) + it("routes an explicit repository page action to the Research directory", () => { + const drafts = createReviewPageDrafts( + review({ + type: "suggestion", + title: "Create: Nashsu llm_wiki repository", + description: "Save the associated software repository.", + }), + "Create repository page", + ) + + expect(drafts).toEqual([ + { title: "Nashsu llm_wiki", pageType: "repository", dir: "repositories" }, + ]) + }) +}) diff --git a/src/lib/review-create-page.ts b/src/lib/review-create-page.ts index 1ccbf6287..9180a7d49 100644 --- a/src/lib/review-create-page.ts +++ b/src/lib/review-create-page.ts @@ -1,6 +1,6 @@ import type { ReviewItem } from "@/stores/review-store" -export type ReviewPageType = "entity" | "concept" | "comparison" | "synthesis" | "query" +export type ReviewPageType = "entity" | "concept" | "comparison" | "synthesis" | "query" | "repository" export interface ReviewPageDraft { title: string @@ -11,13 +11,14 @@ export interface ReviewPageDraft { const ACTION_PREFIX_RE = /^(Create|Save|Add|Missing page|Missing pages|缺失页面|缺少页面|创建|保存|新增)[::\s-]*/i const ENTITY_RE = /\b(entity|entities)\b|实体/i const CONCEPT_RE = /\b(concept|concepts)\b|概念/i +const REPOSITORY_RE = /\b(repository|repositories|repo|repos)\b|代码仓库|仓库/i function cleanCandidateTitle(value: string): string { return value .replace(ACTION_PREFIX_RE, "") .replace(/^(missing|缺失|缺少)\s*/i, "") .replace(/\s*(page|pages|页面|页)\s*$/i, "") - .replace(/\s*(entity|entities|concept|concepts|实体|概念)\s*(page|pages|页面|页)?\s*$/i, "") + .replace(/\s*(entity|entities|concept|concepts|repository|repositories|repo|repos|实体|概念|代码仓库|仓库)\s*(page|pages|页面|页)?\s*$/i, "") .replace(/^[\s"'“”‘’`[\]【】()()]+|[\s"'“”‘’`[\]【】()()::.。]+$/g, "") .trim() } @@ -57,6 +58,7 @@ function extractMissingPageCandidates(text: string): string[] { function detectPageType(action: string, reviewType: ReviewItem["type"], text: string): ReviewPageType { const combined = `${action}\n${text}` if (ENTITY_RE.test(combined)) return "entity" + if (REPOSITORY_RE.test(combined)) return "repository" if (CONCEPT_RE.test(combined)) return "concept" if (/comparison|compare|比较/i.test(combined)) return "comparison" if (/synthesis|综合/i.test(combined)) return "synthesis" @@ -76,6 +78,8 @@ function dirForPageType(pageType: ReviewPageType): string { return "comparisons" case "synthesis": return "synthesis" + case "repository": + return "repositories" case "query": default: return "queries" diff --git a/src/lib/templates.ts b/src/lib/templates.ts index a16d58e95..e2886e8f8 100644 --- a/src/lib/templates.ts +++ b/src/lib/templates.ts @@ -71,7 +71,7 @@ const researchTemplate: WikiTemplate = { name: "Research", description: "Deep-dive research with hypothesis tracking and methodology notes", icon: "🔬", - extraDirs: ["wiki/methodology", "wiki/findings", "wiki/thesis"], + extraDirs: ["wiki/methodology", "wiki/findings", "wiki/thesis", "wiki/repositories"], schema: `# Wiki Schema — Research Deep-Dive ## Page Types @@ -82,6 +82,7 @@ ${BASE_SCHEMA_TYPES} | thesis | wiki/thesis/ | Working hypothesis and its evolution over time | | methodology | wiki/methodology/ | Research methods, protocols, and study designs | | finding | wiki/findings/ | Individual empirical results or observations | +| repository | wiki/repositories/ | Associated software repositories and verified implementation knowledge | ## Naming Conventions @@ -89,22 +90,159 @@ ${BASE_NAMING} - Theses: hypothesis as slug (e.g., \`scaling-improves-reasoning.md\`) - Methodologies: method name (e.g., \`systematic-review.md\`, \`ablation-study.md\`) - Findings: descriptive slug (e.g., \`larger-models-better-few-shot.md\`) +- Repositories: \`owner-repository.md\` (e.g., \`nashsu-llm-wiki.md\`) ## Frontmatter ${BASE_FRONTMATTER} +Source pages representing papers may also include optional flat fields: +\`\`\`yaml +source_kind: paper +authors: [Author A, Author B] +year: "2026" +venue: "" +url: "" +doi: "" +arxiv_id: "" +repository_url: "" +screening_relevance: core +abstraction_level: workflow +contribution_type: [system, method, evaluation] +\`\`\` + +Paper source page body: +\`\`\`markdown +## Problem and context + +## Main contribution + +## Method + +## Evidence and results + +## Limitations + +## Relevance to this project + +## Related concepts and methodologies + +## Associated repositories +\`\`\` + Thesis pages also include: \`\`\`yaml confidence: low | medium | high status: speculative | supported | refuted | settled \`\`\` -Finding pages also include: +Methodology pages may also include optional flat fields: +\`\`\`yaml +source_papers: ["[[paper-a]]", "[[paper-b]]"] +implementation_repositories: ["[[owner-repo]]"] +maturity: emerging +\`\`\` + +Methodology page body: +\`\`\`markdown +## Definition + +## Problem addressed + +## Mechanism + +## Variants + +## Evidence + +## Constraints and failure modes + +## Implementations + +## Project implications +\`\`\` + +Finding pages use flat evidence fields: \`\`\`yaml source: "[[source-slug]]" +source_pages: "12-14" +source_figure: "Figure 3" +source_table: "" +evidence_kind: direct confidence: low | medium | high replicated: true | false | null +repository: "[[owner-repo]]" +repo_commit: "" +repo_path: "src/module/file.ts" +repo_symbol: "FunctionOrClass" +repo_lines: "120-178" +\`\`\` + +Allowed \`evidence_kind\` values: +\`\`\`text +direct | inferred | contextual +\`\`\` + +Finding page body: +\`\`\`markdown +## Finding + +## Conditions + +## Evidence + +## Interpretation + +## Limitations + +## Project implication +\`\`\` + +Repository pages use flat frontmatter: +\`\`\`yaml +--- +type: repository +title: Owner / Repository +tags: [implementation] +related: [] +created: YYYY-MM-DD +updated: YYYY-MM-DD +repo_url: "" +repo_owner: "" +repo_name: "" +branch: "" +pinned_commit: "" +analysis_provider: opendeepwiki +analysis_status: unregistered +external_repository_id: "" +last_analyzed_at: "" +--- +\`\`\` + +Allowed \`analysis_status\` values: +\`\`\`text +unregistered | registered | processing | ready | failed | stale +\`\`\` + +Repository page body: +\`\`\`markdown +## Purpose + +## Relevance to this project + +## Associated papers and methodologies + +## Architecture + +## Building blocks + +## Entry points + +## Verified source evidence + +## Paper-to-code alignment + +## Open questions \`\`\` ## Index Format @@ -121,6 +259,7 @@ ${BASE_CROSSREF} - Findings link back to their source via the \`source:\` frontmatter field - Thesis pages reference supporting and refuting findings via \`related:\` - Methodology pages are cited by the findings that used them +- Repository pages link to associated sources, findings, methodologies, and concepts via \`related:\` ## Contradiction Handling @@ -128,10 +267,16 @@ ${BASE_CONTRADICTION} ## Research-Specific Conventions +- Treat papers as source pages; missing optional bibliographic fields do not block a useful local source - Keep the thesis pages updated as evidence accumulates — they are living documents - Every finding should assess replication status when known +- Use methodology pages for reusable methods, processes, architectures, evaluation protocols, and design patterns - Methodology pages explain the *why* (rationale) not just the *how* +- Use finding pages for evidence-bearing results, constraints, negative results, or independently useful observations - Distinguish between direct evidence and inference in finding pages +- Pin a commit before repository evidence is treated as durable +- Create a repository page only when a source explicitly associates the repository and no matching page exists +- Never claim a repository implements a method without repository verification `, purpose: `# Project Purpose — Research Deep-Dive diff --git a/src/lib/wiki-graph.test.ts b/src/lib/wiki-graph.test.ts new file mode 100644 index 000000000..66223259b --- /dev/null +++ b/src/lib/wiki-graph.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const fsMocks = vi.hoisted(() => ({ + listDirectory: vi.fn(), + readFile: vi.fn(), +})) + +vi.mock("@/commands/fs", () => fsMocks) +vi.mock("./graph-relevance", () => ({ + buildRetrievalGraph: vi.fn().mockRejectedValue(new Error("not needed")), + calculateRelevance: vi.fn(), +})) + +import { buildWikiGraph } from "./wiki-graph" + +beforeEach(() => { + fsMocks.listDirectory.mockReset() + fsMocks.readFile.mockReset() +}) + +describe("buildWikiGraph", () => { + it("keeps repository pages visible as typed graph nodes", async () => { + fsMocks.listDirectory.mockResolvedValue([ + { + name: "repositories", + path: "/project/wiki/repositories", + is_dir: true, + children: [ + { + name: "nashsu-llm-wiki.md", + path: "/project/wiki/repositories/nashsu-llm-wiki.md", + is_dir: false, + }, + ], + }, + { + name: "concepts", + path: "/project/wiki/concepts", + is_dir: true, + children: [ + { + name: "knowledge-core.md", + path: "/project/wiki/concepts/knowledge-core.md", + is_dir: false, + }, + ], + }, + ]) + fsMocks.readFile.mockImplementation(async (path: string) => { + if (path.endsWith("nashsu-llm-wiki.md")) { + return "---\ntype: repository\ntitle: Nashsu / llm_wiki\n---\n\n[[knowledge-core]]" + } + return "---\ntype: concept\ntitle: Knowledge Core\n---\n\n[[nashsu-llm-wiki]]" + }) + + const graph = await buildWikiGraph("/project") + + expect(graph.nodes).toEqual(expect.arrayContaining([ + expect.objectContaining({ + id: "nashsu-llm-wiki", + type: "repository", + path: "/project/wiki/repositories/nashsu-llm-wiki.md", + }), + ])) + expect(graph.edges).toEqual([ + expect.objectContaining({ + source: "nashsu-llm-wiki", + target: "knowledge-core", + }), + ]) + }) +}) + diff --git a/src/lib/wiki-page-types.test.ts b/src/lib/wiki-page-types.test.ts index fa35809e2..756788deb 100644 --- a/src/lib/wiki-page-types.test.ts +++ b/src/lib/wiki-page-types.test.ts @@ -15,6 +15,7 @@ describe("inferWikiTypeFromPath", () => { expect(inferWikiTypeFromPath("/project/wiki/findings/result.md")).toBe("finding") expect(inferWikiTypeFromPath("/project/wiki/thesis/main-claim.md")).toBe("thesis") expect(inferWikiTypeFromPath("/project/wiki/methodology/systematic-review.md")).toBe("methodology") + expect(inferWikiTypeFromPath("/project/wiki/repositories/nashsu-llm-wiki.md")).toBe("repository") }) it("handles Windows separators and overview pages", () => { @@ -33,6 +34,7 @@ describe("wikiTypeLabel", () => { expect(wikiTypeLabel("finding")).toBe("Finding") expect(wikiTypeLabel("thesis")).toBe("Thesis") expect(wikiTypeLabel("methodology")).toBe("Methodology") + expect(wikiTypeLabel("repository")).toBe("Repository") expect(wikiTypeLabel("custom-topic")).toBe("Custom Topic") }) @@ -40,5 +42,6 @@ describe("wikiTypeLabel", () => { expect(GENERATION_WIKI_TYPES).toContain("finding") expect(GENERATION_WIKI_TYPES).toContain("thesis") expect(GENERATION_WIKI_TYPES).toContain("methodology") + expect(GENERATION_WIKI_TYPES).toContain("repository") }) }) diff --git a/src/lib/wiki-page-types.ts b/src/lib/wiki-page-types.ts index 5c445bb6c..9f3f1ff6d 100644 --- a/src/lib/wiki-page-types.ts +++ b/src/lib/wiki-page-types.ts @@ -8,6 +8,7 @@ export const GENERATION_WIKI_TYPES = [ "thesis", "methodology", "finding", + "repository", ] as const const WIKI_TYPE_DIRS: Array<{ dir: string; type: string }> = [ @@ -20,6 +21,7 @@ const WIKI_TYPE_DIRS: Array<{ dir: string; type: string }> = [ { dir: "findings", type: "finding" }, { dir: "thesis", type: "thesis" }, { dir: "methodology", type: "methodology" }, + { dir: "repositories", type: "repository" }, ] export function inferWikiTypeFromPath(path: string, fileName?: string): string | null { @@ -40,6 +42,7 @@ export function wikiTypeLabel(type: string): string { if (type === "thesis") return "Thesis" if (type === "methodology") return "Methodology" if (type === "finding") return "Finding" + if (type === "repository") return "Repository" return type .split(/[-_\s]+/) .filter(Boolean) diff --git a/src/lib/wiki-schema.test.ts b/src/lib/wiki-schema.test.ts index 1f291b529..61dbee3bf 100644 --- a/src/lib/wiki-schema.test.ts +++ b/src/lib/wiki-schema.test.ts @@ -3,6 +3,7 @@ import { parseWikiSchemaRouting, validateWikiPageRouting, } from "./wiki-schema" +import { getTemplate } from "./templates" const SCHEMA = `# Wiki Schema @@ -96,3 +97,36 @@ describe("validateWikiPageRouting", () => { expect(validateWikiPageRouting("wiki/concepts/no-type.md", "# No Type", routing)).toBeNull() }) }) + +describe("Research Profile v2 routing", () => { + const routing = parseWikiSchemaRouting(getTemplate("research").schema) + const repositoryPage = [ + "---", + "type: repository", + "title: Nashsu / llm_wiki", + "---", + "", + "# Nashsu / llm_wiki", + ].join("\n") + + it("routes repository pages to wiki/repositories", () => { + expect(routing.typeDirs.repository).toBe("wiki/repositories") + expect( + validateWikiPageRouting( + "wiki/repositories/nashsu-llm-wiki.md", + repositoryPage, + routing, + ), + ).toBeNull() + }) + + it("rejects repository pages written outside their schema directory", () => { + expect( + validateWikiPageRouting( + "wiki/sources/nashsu-llm-wiki.md", + repositoryPage, + routing, + )?.message, + ).toContain('type "repository" must be under "wiki/repositories/"') + }) +}) diff --git a/src/lib/wiki-type-style.test.ts b/src/lib/wiki-type-style.test.ts index 7d99dcd2c..3b8950373 100644 --- a/src/lib/wiki-type-style.test.ts +++ b/src/lib/wiki-type-style.test.ts @@ -38,7 +38,7 @@ describe("getWikiTypeStyle", () => { it("covers every documented page type", () => { const expected = [ "entity", "concept", "query", "source", - "thesis", "finding", "methodology", "event", "overview", + "thesis", "finding", "methodology", "repository", "event", "overview", ] for (const t of expected) { const style = getWikiTypeStyle(t) diff --git a/src/lib/wiki-type-style.ts b/src/lib/wiki-type-style.ts index 1537d87d4..18303820e 100644 --- a/src/lib/wiki-type-style.ts +++ b/src/lib/wiki-type-style.ts @@ -7,6 +7,7 @@ import { Target, TrendingUp, BookOpen, + GitBranch, Calendar, Hash, } from "lucide-react" @@ -71,6 +72,12 @@ export const WIKI_TYPE_STYLES: Record = { chipClass: "bg-teal-500/15 text-teal-700 dark:text-teal-300", dotClass: "bg-teal-500", }, + repository: { + label: "Repository", + icon: GitBranch, + chipClass: "bg-sky-500/15 text-sky-700 dark:text-sky-300", + dotClass: "bg-sky-500", + }, event: { label: "Event", icon: Calendar,