From knowledge-web
Build Obsidian knowledge vaults from structured research data. Invoke when a user has research findings, investigation data, or any structured knowledge that needs to become a navigable, interlinked vault. Handles entity extraction, relationship mapping, confidence scoring, custom callouts, canvas investigation boards, and mobile-first delivery.
npx claudepluginhub bdmorin/the-no-shop --plugin knowledge-webThis skill is limited to using the following tools:
Transform structured research into navigable Obsidian knowledge vaults. Touch a fact, follow its connections. Every claim sourced. Every relationship bidirectional. Every uncertainty explicit.
Creates new Angular apps using Angular CLI with flags for routing, SSR, SCSS, prefixes, and AI config. Follows best practices for modern TypeScript/Angular development. Use when starting Angular projects.
Generates Angular code and provides architectural guidance for projects, components, services, reactivity with signals, forms, dependency injection, routing, SSR, ARIA accessibility, animations, Tailwind styling, testing, and CLI tooling.
Executes ctx7 CLI to fetch up-to-date library documentation, manage AI coding skills (install/search/generate/remove/suggest), and configure Context7 MCP. Useful for current API refs, skill handling, or agent setup.
Transform structured research into navigable Obsidian knowledge vaults. Touch a fact, follow its connections. Every claim sourced. Every relationship bidirectional. Every uncertainty explicit.
Gather raw research as markdown files. No structure required — capture first, structure second.
research/
2026-03-06-topic-subtopic.md # Timestamped, standalone findings
2026-03-06-another-finding.md # Each file is a research artifact
Every finding gets written down WITH ITS SOURCE. An unsourced finding is a lead, not a fact.
From raw research, extract discrete entities. Six canonical types:
| Type | What It Is | Key Fields |
|---|---|---|
| person | Any individual | role, born, died, status, aliases[], criminal_record[], leads[] |
| place | Physical location | subtype, address, coordinates, features[], timeline{} |
| organization | Named group/company | subtype, address, phone, status |
| event | Something with a date | date, type, people[], artifacts[], location |
| artifact | Evidence/document | subtype, date, text, significance |
| pattern | Analytical assessment | stages[], elements[], evidence[], significance |
Patterns are first-class entities, not annotations. A pattern like "supply chain vulnerability" gets its own node with its own relationships. Patterns are how raw facts become intelligence.
Each entity needs:
smith-john, acme-corp). Unique across graph.Every connection between entities is a typed, directed edge:
{
"from": "entity-id-1",
"to": "entity-id-2",
"type": "owned_operated",
"years": "1968-1986",
"evidence": "Court records"
}
Common relationship types (adapt to domain):
owned_operated, employed_by, business_associate, parent_of, founded,
front_for, located_on, produced_by, distributed_by, investigated,
testified_in, plaintiff_in, defended_by, grew_up_at, resided_at
Every entity and relationship gets scored:
| Level | Meaning | Use When |
|---|---|---|
| VERIFIED | Multiple independent sources confirm | Court records + news archives + testimony agree |
| LIKELY | Strong evidence, minor gaps | Single authoritative source (court filing) |
| UNCERTAIN | Plausible but incomplete | Single uncorroborated source, circumstantial |
| RETRACTED | Previously held, now contradicted | Superseded — retain with correction for lineage |
| ASSESSED | Analytical product | Patterns, conclusions, synthesized judgments |
Apply six principles before the graph is complete:
Output is a single JSON file:
{
"meta": {
"project": "Project Name",
"created": "2026-03-07",
"methodology": "Description of approach",
"classification": "OPEN SOURCE | RESTRICTED | etc.",
"tracks": {
"full": "Complete graph for analysts",
"filtered": "Subset for specific audience"
}
},
"entities": {
"people": [],
"places": [],
"organizations": [],
"artifacts": [],
"events": [],
"patterns": [],
"open_questions": [],
"relationships": []
},
"search_seeds": {}
}
Run the conversion script to generate the complete Obsidian vault from the knowledge graph. The script is deterministic — same JSON in, same vault out. The JSON is the source of truth; the vault is a derived view.
vault/
.obsidian/
snippets/<topic>-theme.css # Custom callouts + mobile CSS
app.json # Reading mode, shortest-path links
appearance.json # Dark theme, snippet enabled
community-plugins.json # Dataview, Templater
Home.md # Map of Content — landing page
People/ # Person notes + Index.md
Places/ # Location notes
Organizations/ # Org/company notes
Events/ # Date-prefixed event notes + Timeline.md
Artifacts/ # Evidence/document notes
Patterns/ # Analytical pattern notes
Research/ # Open questions, search seeds, FOIA targets
Sources/ # Source index (auto-generated)
Templates/ # One per entity type
Canvas/ # Investigation boards
Define domain-specific callout types in CSS. Each is one rule:
.callout[data-callout="your-type"] {
--callout-color: R, G, B;
--callout-icon: lucide-icon-name;
}
Used in notes as > [!your-type] Title.
Domain callout palettes:
| Domain | Callouts |
|---|---|
| Investigation | evidence (green), testimony (purple), suspect (red), open-question (amber), pattern (blue), source (gray), timeline (teal), foia (orange), classified (dark red), therapeutic (pink) |
| Medical | finding (green), adverse-event (red), methodology (blue), hypothesis (amber), clinical-note (gray), contraindication (orange) |
| Competitive Intel | market-signal (green), risk (red), opportunity (blue), speculation (amber), financial (gray) |
| Genealogy | record (green), oral-tradition (purple), estimate (amber), conflict (red), migration (blue) |
| Journalism | on-record (green), background (blue), off-record (amber), disputed (red), document (gray) |
The script (Bun/TypeScript) follows this structure:
entityMap (id → entity), entityFolder (id → folder)from and to directions[[wikilink]] resolves to an existing fileconst relsByEntity = new Map<string, Rel[]>();
for (const rel of relationships) {
if (!relsByEntity.has(rel.from)) relsByEntity.set(rel.from, []);
if (!relsByEntity.has(rel.to)) relsByEntity.set(rel.to, []);
relsByEntity.get(rel.from)!.push(rel);
relsByEntity.get(rel.to)!.push(rel);
}
Skip this and entities that are only targets of relationships show ZERO connections. Half your web is invisible.
function placeRing(ids: string[], radius: number, cx: number, cy: number) {
for (let i = 0; i < ids.length; i++) {
const angle = (2 * Math.PI * i) / ids.length - Math.PI / 2;
positions.set(ids[i], {
x: cx + radius * Math.cos(angle),
y: cy + radius * Math.sin(angle)
});
}
}
// Center: primary subject. Inner ring (r=600): direct associates.
// Middle ring (r=1200): secondary connections. Outer ring (r=2000): context.
Ring assignment is an ANALYTICAL decision — which entities go where reflects investigative proximity.
function wikilink(id: string): string {
const entity = entityMap.get(id);
if (!entity) return `\`${id}\``; // Fallback: code format, not broken link
return `[[${sanitizeFilename(entity.name)}]]`;
}
Unknown IDs render as inline code, not broken links. Dead links are dead ends.
Filename sanitization: Entity names with /\:*?"<>| must be cleaned. Slashes → hyphens, colons → hyphens, pipes → hyphens. Parentheses and single quotes are fine.
YAML frontmatter: Use JSON.stringify() per value. No raw wikilinks in frontmatter arrays — Obsidian treats them as strings, not links. Put clickable links in the note body.
Dataview dependency: Queries only work after plugin install. Home.md must serve as a non-Dataview fallback with full wikilink navigation.
Mobile CSS: No :hover states. Min tap target 44px. Full-width callouts. Vertical flow only. Test on actual iOS device or at 375px viewport.
Canvas node sizing: 300x120 per card. Inner ring 7 nodes max at r=600. Scale radius with node count.
Event filenames: Use {date} {description-truncated-60}.md for sortability + uniqueness. Pipe wikilinks for clean display: [[filename|Display Text]].
JSON.txt quirk: Knowledge graph files sometimes arrive with wrong extensions. Parse by content, not extension.
Link integrity: After generation, verify ALL wikilinks resolve. One broken link = one investigation dead end.
The vault supports compartmentalization:
track field to entities, filter during generation.After vault generation:
grep -roh '\[\[[^]|]*' vault/ | sort -u vs find vault/ -name '*.md' | sed 's|.*/||;s|\.md$||' | sort -u)A complete working example exists at /Users/bdmorin/cowork/roselawn/:
data/knowledge-graph.json.txt — 97-entity knowledge graphscripts/kg-to-obsidian.ts — Bun/TypeScript conversion scriptvault/ — Generated 80-file Obsidian vaultdocs/retrospective.md — Full methodology retrospectiveThe script is domain-agnostic: replace entity data, adjust callout types and role taxonomies, regenerate.