Skip to content

Library API reference

manni graph is a CLI first, and everything it does is reachable as a library. The package publishes three entry points, and which one you want depends on where the code runs.

ImportPlatformWhat it carries
@hawkeyexl/manniNodeThe whole family. graph is one namespace on it.
@hawkeyexl/manni/graph/runtimeBrowser or NodeRetrieval over a built graph. No node: imports, no dependencies, one file.
@hawkeyexl/manni/graph/embedNodeThe local embedders, behind the optional @huggingface/transformers peer.

The split is not cosmetic. A node: import reaching the runtime’s module graph is a released bug rather than a type error. Nothing in tsc knows which half it is compiling. Anything the runtime needs from Node is taken as an argument instead.

import { graph } from "@hawkeyexl/manni";
const result = await graph.runBuild({ paths: ["docs"] });
console.log(result.outPath, result.docs, result.quads);

Each namespace is one tool, so a name cannot collide with a sibling’s and a reader can see which tool it belongs to.

Every verb is a function that takes options and returns a report. They do no CLI plumbing and read no argv, so they can be called directly and tested directly.

ExportPurpose
runBuildDerive the graph and write Turtle. Returns { outPath, docs, quads, warnings }.
runFillPropose and write graph: fields through a provider.
runQueryMatch a triple pattern against a built graph.
runStatsCounts, orphans, broken links, hubs, and coverage.
runSearchRank nodes for a text query, with renderSearch for the report.
runTraverseWalk the graph from a node, with renderTraverse for the report.
runEmbedCompute vector sidecars, with renderEmbed and the lower-level embedText.
runExportReserialize into jsonld, iirds, or search.

runBuild takes a BuildOptions and returns a BuildResult. Two of their fields carry the content strategy:

FieldTypePurpose
BuildOptions.authoringboolean, optionalOverrides graph.authoring. false leaves author-facing content out, as --no-authoring does.
BuildResult.strategyRecordsnumberHow many strategy records the build typed. 0 when none is in the build.
const readers = await graph.runBuild({ collection: ["site"], authoring: false });
console.log(readers.strategyRecords); // 0

GraphError is the operational failure the CLI maps to exit 2. Catch it to tell “manni graph could not run” from “the graph did not pass”.

The stages runBuild composes, each usable on its own.

ExportPurpose
analyzeDocOne document to a DocModel: frontmatter, headings with slugs and nesting, links with their resolution, images, code blocks.
deriveGraphDocModel[] to Quad[]. This is the vocabulary mapping, and the place a new predicate is decided.
collectGitHistoryRevision history and commit agents, from git.
emitTurtle, emitJsonLd, emitRdfXmlSerializers. The Turtle one is canonical and hand-written rather than a library’s writer, because byte-identical output is the product contract.
loadGraph, storeToQuads, expandTerm, compactIriRead a built graph back, and move between full IRIs and prefixed names.
buildSearchIndex, emitSearchIndex, partitionByLanguage, documentsByLanguageThe search artifacts.
projectPackage, writeZipThe iiRDS container.

Every node gets a deterministic IRI, so there are no blank nodes and two runs agree.

resolveBaseIri, normalizeDocPath, conceptSlug, encodeSegment, and the minters mintDocIri, mintSectionIri, mintConceptIri, mintSchemeIri, mintProductIri, mintAgentIri, mintBuildActivityIri, mintGraphIri.

manni graph stats measures how much of the vocabulary a corpus actually fills. The lists it measures against are exported, so a dashboard reads the same definition the gate does rather than a copy of it.

ExportPurpose
COVERAGE_FIELDSEvery measured document-level field, each with the IRI whose presence counts as filled.
COVERAGE_FIELD_NAMESThe same list, names only. These are the keys a per-field stats.coverageThreshold map accepts.
SECTION_COVERAGE_FIELDSThe section-level rows, a subset of the above. Reported, never gated.
CoverageFieldThe row type.
import { graph } from "@hawkeyexl/manni";
for (const field of graph.COVERAGE_FIELDS) {
console.log(field.field, field.iri);
}

There are thirteen measured document-level fields. Two predicates are left unmeasured on purpose, and Coverage says which and why.

ExportPurpose
NS, PREFIXESThe namespace IRIs and their prefixes, including graph: itself.
frontmatterSchema, FRONTMATTER_SCHEMA_IDThe page vocabulary a graph: block is validated against, bundled into the build.
TOPIC_TYPE_IRIS, SOFTWARE_LIFECYCLE_IRIS, SOFTWARE_SUBJECT_IRISThe iiRDS maps behind type, about-product-lifecycle and about-product-aspect.
GRAPH_NOT_APPLICABLE_TO_VARIANT, GRAPH_NOT_SOFTWARE_SUBJECTThe two negative-scope predicates.

loadConfig, loadRunConfig, parseConfig and defaultConfig, with the GraphConfig, DeriveSource and FillField types. discoverFiles and resolveDocumentSet turn paths, collections and excludes into the document set a verb reads.

applyGraphFields, existingGraphFields, existingMetaProvenance and frontmatterKind are the frontmatter writer fill uses, if you need to write a graph: block yourself.

selectProvider, constructProvider, makeProvider, providerSpecFor, resolveProviderIdentity and assertProviderSelection resolve a provider the way graph fill does. MockProvider is re-exported from @hawkeyexl/inference so a test can drive the offline seam without a second dependency. manni reimplements no provider, no inference cache, and no price table.

@hawkeyexl/manni/graph/runtime is the retrieval half, built platform: neutral and shipped as one file you can drop in with a script tag. It loads a graph.jsonld, walks it with scope rules honored, resolves node text, and assembles the bundle an inference engine consumes.

It never generates. It returns { context, citations, trace } and stops. Wiring a model is the host’s job, and every result carries the trace that produced it.

import { GraphIndex, traverse, createFetchResolver, assemble }
from "@hawkeyexl/manni/graph/runtime";
const graph = GraphIndex.fromJsonLd(await (await fetch("/graph/graph.jsonld")).text());
const { nodes, trace } = traverse(graph, {
seeds: ["https://example.com/graph/doc/docs/configuration.md"],
depth: 2,
variant: "SP-X100",
});
const resolver = createFetchResolver(graph, { baseUrl: "/raw/", trace });
const bundle = await assemble(resolver, nodes, { trace, maxChars: 12000 });
AreaExports
The graphGraphIndex, GraphNode, Value
Walkingtraverse, impact, reverseReferences, resolveVariant, resolveSubject, scopeExclusion
TextcreateFetchResolver, sliceSection, sectionOwnText, documentPreamble, documentSectionOrder, sectionOccurrence, sectionOccurrences, splitFragment
Assemblyassemble, with RetrievalBundle, Citation, ContextBlock, Refusal
SearchcreateLexicalIndex, createVectorIndex, findEntry, rrfMerge, searchIndexDigest
VectorsencodeVectorIndex, decodeVectorIndex, normalize, VectorIndexError, VectorMismatchError
LocalizationparseLocalizations, emitLocalizations, searchIndexFilename, vectorIndexFilename, LOCALIZATIONS_FILENAME, UNDETERMINED
TracingcreateTrace, reachedNodes, QueryTrace, Hop, ScopeExclusion
RDF/JSmatchQuads, rdfjsQuads, namedNode, literal, defaultGraph

sliceSection is worth one line of its own. Heading text is not unique, so slicing a specific section node needs its occurrence. Without one it silently returns the first same-titled heading’s body.

createRetriever serves the chunk artifact to a script, with each passage’s proof. It returns data and calls no model besides the query embedder.

The in-docs playground runs lookup, retrieve, traverse and impact against this site’s artifact. It shows the call and its returned values, with an optional WebLLM model to answer questions about the result in the browser.

import { createRetriever } from "@hawkeyexl/manni/graph/runtime";
const docs = await createRetriever({ manifest: "/graph/chunks.json" });
const result = await docs.lookup("How do I rotate the encryption key?");
OptionTypeDefaultMeaning
manifestURL string, or a parsed manifestrequiredchunks.json. The graph, the search index and the rows resolve from its directory.
storeVectorStorethe static store over the rows’ vectorsPass createHttpVectorStore(...) for a hosted vector DB.
chunksChunkSourcethe chunk files the manifest namesPass your own to read rows from a vector DB by id.
embedan embedder, or falsegraph/embed with each language’s modelfalse forces lexical entry. A model loads once, when a language that needs it is first queried.
fetchfunctionthe global fetchFor hosts without one, or for tests.

The four verbs are the CLI’s, over the same artifact, and they answer alike.

MemberSame asReturns
lookup(question)graph lookupthe lookup result
retrieve(question, { lang, variant, softwareSubject, depth, maxChars, mode, where, withhold })graph retrievea bundle
traverse(node, { depth, predicates, reverse, lang, variant, softwareSubject, limit, maxChars, where, withhold })graph traverse --contexta bundle
impact(node, { depth, lang, variant, softwareSubject, limit, maxChars, where, withhold })graph traverse --impact --contexta bundle
manifestn/athe parsed manifest

Every option is optional. where is a list of { field, value }, and every condition must hold, as with --where. withhold lists drifted, failing or both, as --withhold does. It tightens the floor the artifact was exported with, for that call, and never lowers it. lookup serves the exported floor. A refusal comes back in the result. Two errors throw instead:

  • StaleArtifactError, when the graph or a row was built from another graph than the manifest names.
  • ModelMismatchError, when the embedder is not the model the queried language’s rows were embedded with. A language is checked only when it is queried.

Each language in chunks.json names its own model, and the question is embedded with the queried language’s. A host that cannot load that model falls back to lexical entry. So does a language with no model, when another language has one. trace.entry.fallback then reads "lexical". The default embedder loads from the graph/embed file beside the runtime, so a bundler that does not ship it gets the fallback.

The rows are read lazily, one language at a time.

A host supplies two seams, and manni ships an implementation of each.

interface VectorStore {
query(q: { text: string; vector: number[] | null; k: number;
language: string | null; where: { field: string; value: string }[] }):
Promise<Array<{ id: string; score: number; row?: ChunkRow }>>;
}
interface ChunkSource {
get(ids: string[], language: string | null): Promise<ChunkRow[]>;
}
  • createStaticVectorStore(rows) ranks rows by cosine in memory, among those that meet where. It needs no service.
  • createHttpVectorStore({ url, fetch }) POSTs { text, vector, k, language, where } as JSON and expects { matches: [{ id, score, row }] }. row is optional. Without it, the id is read through the ChunkSource. An id no row holds is dropped and traced. vector is null when the host did not embed, so the server can embed instead.
  • createChunkFileSource(textOrUrl, { fetch }) reads one chunks.<tag>.jsonl, given as its text or its URL.
  • parseChunkRows(text) returns a JSONL file’s rows, for ingestion into a vector DB. parseChunksManifest(input) checks that a document is a chunk manifest. Both throw on a document missing a field the cores read.

The cores the verbs call, lookup and retrieve, take the context and the options in one object. A host with seams of its own can call them directly.

AreaExports
The script surfacecreateRetriever, with CreateRetrieverOptions, Retriever and RetrieverFetch
The coreslookup, retrieve, with RetrieverContext, EmbeddingModel, LookupOptions and RetrieveOptions
The seamsVectorStore, VectorStoreQuery, VectorStoreMatch, ChunkSource, WhereCondition, createStaticVectorStore, createHttpVectorStore, createChunkFileSource
The artifactparseChunkRows, parseChunksManifest, ChunkRow, ChunksManifest, ProvenanceRecord and its parts
ResultsLookupResult, LookupMatch, LookupReached, LookupEdge, PassageBundle, PassageBlock, RetrievalTrace, WithheldNode, FilteredNode
ErrorsStaleArtifactError, ModelMismatchError

RefusalReason gains withheld, lang-required, unknown-node and unknown-scope.

@hawkeyexl/manni/graph/embed carries createLocalEmbedder, createMockEmbedder, EmbedderUnavailableError, DEFAULT_MODEL, MODEL_PROFILES, profileFor and withPrefix. The local embedder needs the optional @huggingface/transformers peer; the mock one runs offline and is what regenerates the committed fixtures.

A mock is not coverage of the thing it stands in for. createLocalEmbedder once shipped a hardcoded device that threw on every real call, and the mocks certified it for a whole release. Anything that mocks an external library here ships the real-path test in the same change.

These are the symbols the package entry point publishes today. The CLI is the supported surface, and the library follows it. A verb’s options and report shape change when the flag surface does, under the same semantic-versioning rules. The browser runtime’s neutrality is a guarantee rather than an accident, and a bundle-purity test enforces it on every build.