Wikantik began as a fork of Apache JSPWiki and has been re-architected into something its ancestor never was: an agent-grade knowledge platform where every page is simultaneously a human-readable document, a retrieval target, a node in two distinct graphs, and a set of machine-callable tools. This page is the deep, current (pre-2.1.0) reference for how the system is built — the modules, the data model, the retrieval and knowledge layers, the agent surface, the rendering and security pipelines — followed by an honest assessment of where it is strong, where it is weak, and where it should go next.
The guiding principle, repeated throughout the design, is human–machine parity: a human editing in the browser and an AI agent calling an MCP tool go through the same save pipeline, the same validation, the same permission checks, and read from the same retrieval index. There is no separate "API content." That single decision shapes almost everything below.
┌──────────────────────────────────────────┐
Humans ──browser──▶ │ React SPA (Vite/TS) + SSR head/meta │
└───────────────┬──────────────────────────┘
│ HTTP
Agents ──MCP/HTTP──▶ ┌──────────────▼──────────────────────────┐
Crawlers ─REST/RDF──▶ │ Servlet filter pipeline │
│ CSRF · CORS · CSP · auth · SPA routing │
└──────────────┬──────────────────────────┘
┌───────────────────────────┼───────────────────────────────┐
▼ ▼ ▼ ▼ ▼
/api/* REST /wikantik- /knowledge- /scim/v2/* /sparql · /id/*
/admin/* admin-mcp mcp /export/* (RDF)
│ (26 tools) (19–20 tools) │ │
└───────────────────────────┬────────────┴──────────────────┘
▼
┌───────────────────────────────────────────┐
│ WikiEngine (orchestrator) │
│ PageManager · RenderingManager · Search │
│ FilterManager · PluginManager · Attach… │
└───────────────┬───────────────────────────┘
┌──────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ Page corpus │ │ PostgreSQL │ │ Ontology (Jena TDB2) │
│ Markdown + YAML │ │ users · policy · │ │ RDF/OWL T-Box + │
│ (file provider, │ │ KG (kg_*) · │ │ projected A-Box │
│ versioned) │ │ pgvector embeds · │ │ (SPARQL/SHACL) │
│ │ │ citations │ │ │
└──────────────────┘ └────────────────────┘ └──────────────────────┘
The wiki deploys as a single WAR into Tomcat 11. It is a modular monolith: ~20 Maven modules with strict dependency boundaries (enforced by an ArchUnit decomposition test), not a microservice fleet. Heavy AI work (embedding, entity extraction, LLM judging) is delegated to external services (Ollama / an OpenAI-compatible endpoint) and to a companion CLI, so the engine itself stays a normal JEE application.
Modules are layered: wikantik-api defines the contracts (ports); everything else depends inward toward it. The ArchUnit DecompositionArchTest freezes the allowed getManager() call sites and the dependency direction so the decoupling cannot silently rot.
| Module | Layer | Responsibility |
|---|---|---|
wikantik-bom | build | Bill-of-materials pinning shared dependency versions. |
wikantik-api | ports/domain | Manager interfaces, frontmatter + schema model, Page Graph + Knowledge Graph + bundle contracts. No implementations. |
wikantik-main | engine | WikiEngine, rendering, providers, auth, search, references, entity extraction, math parser, derived-page reflow. |
wikantik-event | core | Decoupled WikiEvent bus. |
wikantik-util | core | Helpers, crypto utilities. |
wikantik-cache / -cache-memcached | core | EhCache render/object caches; Memcached adapter for distributed deploys. |
wikantik-http | edge | Servlet filters: CSRF, CORS, CSP, security headers, SPA routing, the /wiki/{slug}?format=md\|json content filter. |
wikantik-rest | edge | REST /api/* (25 resources, incl. POST /api/ingest, GET /api/bundle) and admin /admin/* (audit, drift, ontology, derived, kg-policy). Public RDF servlets. |
wikantik-admin-mcp | agent | MCP server at /wikantik-admin-mcp — 26 write/analytics/KG-curation tools. |
wikantik-knowledge | agent + brain | MCP server at /knowledge-mcp (19–20 read tools) and the KG service: pgvector embeddings, co-mention graph, hybrid retriever, the context-bundle assembler. |
wikantik-tools | agent | OpenAPI 3.1 tool server /tools/* (2 tools) for non-MCP clients. |
wikantik-scim | agent | SCIM 2.0 provisioning /scim/v2/* — IdP-driven Users + Groups. |
wikantik-ontology | knowledge | Apache Jena: the wikantik: T-Box (wikantik.ttl), SHACL shapes, Postgres→RDF projectors, TDB2 store. |
wikantik-ingest | ingestion | Pure Tika/flexmark document extraction for derived pages (isolates PDFBox/POI from the engine). |
wikantik-extract-cli | tooling | Offline entity-extractor + the derived-page batch ingester. |
wikantik-observability | ops | In-app health checks, Prometheus metrics, request correlation (the deployment monitoring stack lives in a separate jakemon repo). |
wikantik-frontend | UI | React SPA (Vite/TS): reader, editor, admin panel, KG + Page Graph viewers. |
wikantik-war | bundle | Packages the React build + wires every servlet/filter into one deployable. |
wikantik-wikipages | content | Default pages shipped with a fresh install. |
wikantik-it-tests | test | Cargo-launched Selenide + REST + custom-provider integration suites. |
wikantik-api ◀─────────── (everyone depends inward on the ports)
▲
│ implements
wikantik-main ◀── http · rest · admin-mcp · knowledge · tools · scim · ontology
▲
└── war ──packages──▶ frontend + all servlets/filters
A recurring source of confusion — deliberately disambiguated in the codebase — is that Wikantik maintains three distinct edge types. Conflating them is treated as a code smell.
canonical_id (rename-stable identity in frontmatter) and cluster: (hub membership). Surfaces: /page-graph, /admin/page-graph/*.kg_* tables. Surfaces: /admin/knowledge-graph/*, /knowledge-mcp.cite:// markup, parsed at save into the citations table, version-pinned and span-hashed so staleness can be detected and surfaced (/admin/drift/citations, list_stale_citations).The bare word "graph" is avoided in identifiers; code always says Page Graph, Knowledge Graph, or kg_*/pagegraph.
Retrieval is hybrid: lexical BM25 (Apache Lucene, full-page index) fused with dense vector search (pgvector / Lucene-HNSW) via reciprocal rank fusion, with a fail-closed fallback to BM25 if the dense side is unavailable.
query
│
├─▶ BM25 (Lucene) ┐
│ ├─ reciprocal rank fusion ─▶ candidates
├─▶ dense ANN (pgvector / ┘ │
│ lucene-hnsw) ▼
│ de-dup · version-pin · cite
└─▶ (KG rerank: wired but boost=0 — see critique) │
▼
context bundle (GET /api/bundle,
assemble_bundle MCP) — ranked,
cited sections, NO answer synthesis
Two measured levers, not guesses, moved global section recall@12 from ~0.60 to ~0.74:
ContentChunker force-emits its merge-forward buffer at every heading boundary so early/first-H2 sections keep their own heading_path (they were previously mis-attributed and mis-cited); plus a sub-floor fragment merge and small overlap.EmbeddingTextBuilder.forDocument prepends Page: {title} | Cluster: {cluster} | Section: {heading} + the frontmatter summary before embedding. This is the reason title/cluster/summary are first-class retrieval levers and not just SEO metadata.The context bundle is RAG-as-a-Service done deliberately: it returns a ranked, de-duplicated, version-pinned, citation-bearing set of sections — and never synthesizes an answer (ADR-0001). Its default source is a global dense+BM25 chunk hybrid, not a page-gated retrieve, because page-gating drops relevant sections whose page ranks outside the top-N.
Levers that were measured and rejected (and left off by default): the LLM listwise reranker, HyDE, doc2query, and KG graph rerank — all either failed to move recall or actively hurt it.
The Knowledge Graph is a property graph over wiki content stored in PostgreSQL: entities and edges extracted by an LLM (a reasoning model run with thinking disabled for clean structured JSON), embedded with pgvector, with a human-in-the-loop proposal workflow before anything is written back. Cluster-primary inclusion policy keeps it default-exclude with a kg_include: frontmatter override.
Above the KG sits a formal RDF/OWL ontology (wikantik-ontology, Apache Jena):
wikantik.ttl): 9 entity + 5 content classes, the 21 KG predicates with domain/range, public mappings to schema.org / SKOS / Dublin Core / PROV-O, and a SKOS concept scheme — plus SHACL shapes.The ontology is, in effect, a projection of the same knowledge the KG holds — the SEO JSON-LD @type on each page is even re-sourced from the ontology's inferred schema.org type, with a test asserting the two faces can't silently drift.
Agents are first-class clients, not an afterthought. Every surface enforces the same ACLs as the human UI.
| Endpoint | Protocol | What |
|---|---|---|
/wikantik-admin-mcp | MCP (Streamable HTTP) | 26 write/analytics/KG-curation tools (incl. admin-bypass reads, orphan listing, real-traffic query log). |
/knowledge-mcp | MCP | 19–20 read tools: hybrid retrieval, KG traversal, schema discovery, structural-spine nav, agent-grade page projection, batched reads, get_ontology, sparql_query, list_stale_citations, and assemble_bundle. |
/tools/* | OpenAPI 3.1 | 2 tools (search_wiki, get_page) for OpenWebUI-style non-MCP clients. |
/scim/v2/* | SCIM 2.0 | IdP-driven Users + Groups provisioning (SCIM Groups never grant Admin). |
/api/*, /admin/* | REST/JSON | 25 + 13 resources; GET /api/bundle, POST /api/ingest, /api/changes?since= feed. |
/sparql, /id/{type}/{id}, /export/* | RDF | Public read-only ontology: SPARQL, per-resource JSON-LD/Turtle dereferencing, full dumps. |
Wikantik deliberately splits its state across three stores so each does what it is good at:
PageProvider (file-system + versioning provider). This is the source of truth for content and gives free version history. Production keeps its corpus independent of deploys (content edits go through MCP/REST, not a redeploy).kg_* Knowledge Graph, pgvector embeddings, and the citations table.The frontmatter is the contract that ties these together: a server-authoritative FrontmatterSchema validates every save (malformed YAML 422s; field-value issues are advisory warnings so the existing corpus still saves), and those same fields drive retrieval embeddings, JSON-LD, feeds, and the News Sitemap.
Markdown source
│ MarkdownParser → Flexmark AST
├─ pre/post filters (FilterManager): structural spine, schema validation,
│ math validation, citation parsing, frontmatter → KG projection
├─ plugins [{Plugin}]() (auto-normalized to [{Plugin}]() for Flexmark)
▼
MarkdownRenderer → HTML ──▶ SSR (title/meta/JSON-LD head) + React SPA hydrate
The same content is served three ways: rendered HTML for browsers (with an SSR head carrying <title>, meta, and JSON-LD so crawlers and the React app agree), raw ?format=md|json for RAG ingestion, and projected for-agent views via MCP. SSR + the SPA must agree on the head, which is why a soft-404 class of bug (SPA refetch wiping the SSR body) is guarded explicitly.
policy_grants table, admin-managed) as the default authorization source, with a file-policy fallback and a bootstrap-admin override for first setup.view/comment/edit/modify/upload/rename/delete) and wiki permissions (createPages/createGroups/editPreferences/…), enforced uniformly across REST, MCP, and the UI; inline [{ALLOW view Admin}]() ACLs in page bodies.ObjectInputFilter allowlists), NIST 800-63B password rules with a common-password blocklist, SameSite=Lax auth cookies with a remember-me re-auth filter, and session rotation on SSO login (fixation defense).An honest architecture page names its own debts.
$…$ prose-pairs but misses currency inside tables or long spans that don't form a stopword pair (e.g. $3,150 … $10,000), so some pages still mis-render numbers as math. The fix is per-page escaping, not yet systematized.assemble_bundle check is the real gate), and that gap is closed by ongoing curation, not by the architecture itself.