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 (2.4.x) 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)
│ (27 tools) (21 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: 22 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, ClusterPath. 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, backpressure, rate limiting, the /wiki/{slug}?format=md\|json content filter. |
wikantik-rest | edge | REST /api/* (33 servlets, incl. POST /api/ingest, GET /api/bundle, GET /api/briefing) and admin /admin/* (26 servlets: audit, drift, ontology, derived, kg-policy, connectors, cluster rename). Public RDF servlets. |
wikantik-mcp-core | agent | Shared MCP substrate — McpTool, endpoint bootstrap, access filter, and the two tools both MCP servers register. Extracted to break the wikantik-knowledge → wikantik-admin-mcp module cycle. |
wikantik-admin-mcp | agent | MCP server at /wikantik-admin-mcp — 27 write/analytics/KG-curation tools. |
wikantik-knowledge | agent + brain | MCP server at /knowledge-mcp (21 read tools when fully wired) 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-connectors | ingestion | External-source connector runtime — seven connector types syncing into derived pages via a shared SyncOrchestrator, with DB-backed configs and an encrypted credential store. |
wikantik-extract-cli | tooling | Offline entity-extractor, the derived-page batch ingester, and CorpusDivergenceCli. |
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. Not a Maven module — wikantik-war drives the npm build and bundles the output. |
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 · mcp-core · 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.
Cluster membership is a projection of frontmatter, not a filesystem hierarchy (ADR-0009). A cluster exists if and only if exactly one page carries type: hub plus a cluster: value — the hub page is the authoritative declaration. Non-hub pages may hold several memberships (cluster: is scalar-or-list; the first entry is primary and drives breadcrumbs, JSON-LD articleSection, sidebar placement and the embedding prefix). Sub-clusters are parent/child, one level deep, permanently.
Comparison is segment-aware through a single ClusterPath class — a bare startsWith would report machine-learning-ops as a descendant of machine-learning and silently merge two unrelated clusters. Membership is transitive and resolved at query time, so re-parenting needs no reindex. Structural conflicts (duplicate declaration, headless cluster, undeclared cluster, clusterless hub, multi-cluster hub) surface on the /admin/drift burn-down rather than blocking saves; rename_cluster rewrites a cluster across every member plan-first.
The rejected alternative is recorded deliberately: a path in frontmatter is data — validatable, re-projectable, multi-valued, revertable per page — while a path in the filesystem would bind page identity, slug history, OLD/, attachments, URLs and wikilinks to one axis.
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
│
▼
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: the LLM listwise reranker, HyDE, doc2query (all left off by default), and the KG graph rerank — which was deleted outright in 2026-07. None moved recall; some 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; policy lookup walks cluster ancestors (most specific wins) and resolves fail-closed across multiple memberships — an explicit EXCLUDE on any membership wins outright.
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) | 27 write/analytics/KG-curation tools (incl. admin-bypass reads, orphan listing, real-traffic query log, bulk cluster rename). |
/knowledge-mcp | MCP | 21 read tools when fully wired: hybrid retrieval, KG traversal, schema discovery, structural-spine nav, agent-grade page projection, batched reads, get_ontology, sparql_query, list_stale_citations, get_briefing, and assemble_bundle. Several register conditionally on what a deployment has wired. |
/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 | 33 + 26 servlets; GET /api/bundle, GET /api/briefing, 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. Schema is versioned as numbered idempotent migrations (currently V001–V049).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).503 + Retry-After rather than letting it queue.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.