Skip to Content

How the pieces fit

The glossary defines each noun on its own — entity, seed page, seed, graph store, cluster. This page draws the graph between them: which file owns which fact, what writes to what, and how a handful of committed inputs flow through the engine into one derived output. If you’ve ever asked “wait, is that a config thing or a reference-table thing?”, this is the page that answers it.

The whole model is four kinds of thing:

  • policy — the rules (gtmesh.config.yaml),
  • facts & relationships — your domain, authored into the committed graph as entity nodes (gtmesh graph upsert), plus a reference table (reference/signals.csv) and curated term lists (seeds/<class>.csv),
  • market signal — demand pulled from a provider (data/raw/, “the bag”),
  • derived state — the mesh the engine computes: demand-minted page nodes, every edge, and each node’s lifecycle status, serialized into that same committed graph (graph/nodes.jsonl + graph/edges.tsv).

The graph is the one committed store. You never hand-edit it — you author your domain into it through validated CLI verbs (gtmesh graph upsert for entities, gtmesh page add for source-less pages), and the engine writes the derived mesh through apply / seal / lifecycle. Policy, the reference tables, the term lists, and the bag you own directly as files.

Who owns which fact

The single most useful table in the docs. When you’re not sure where a fact lives, it lives here:

SurfaceOwns (the fact)Written byRead by
gtmesh.config.yamlPolicy. Taxonomy (sections, axes, intents), page_types, link_rules, links (how authority flows: weights + up-link caps), scoring, classification knobsyou / the operator, via gtmesh configevery engine stage
entity nodes (in the graph)Entities + their relationships & facts. Domain labels (class + one per domain axis: category, brand, …), aliases, facet props, and fact props (name, source_url, specs) — authored onto a node, validated on writeyou / operator / harvest, via gtmesh graph upsert <id>classification (resolve a keyword → entity; the fold key), the mesh (up-links)
source=seed page nodes (in the graph)Which source-less pages exist. Apex / pillar / typed hubs that no keyword would mint — plus the topic each owns or joins, its page_type and editorial identity, written as source: seed nodesyou / operator, via gtmesh page adddesired-state union (they exist because the topology needs them)
seeds/<class>.csvCurated demand terms for a class — the term + its provenance/section/page_type. Never metrics.the harvest skill (human-reviewed)discovery drafting (joins metrics from the bag by target_keyword)
reference/signals.csvKeyword pattern → roleyou / operatorclassification
gtmesh.config.yaml (scope:)The demand floor and exclusions (what parks to backlog)you / operatorplan’s in-scope test
data/raw/ (the bag)Market signal — the demand metrics (volume, difficulty, CPC), immutable & timestampedgtmesh pull demand (or stage admit)plan (read frozen; never re-fetched)
.gtmesh/staging/ (staged)Pulled-but-not-admitted demand — local exploration, not part of the buildany gtmesh pull <channel> --stagegtmesh stage list/admit only — plan ignores it
data/raw/performance/ (the performance snapshot)Observed performance — clicks / impressions / CTR / position per URL and per query, both trailing windows, timestampedgtmesh pull performance --source gscgtmesh topic performance (a read-time join) — never plan/apply
data/raw/ai/<provider>/ (the AI-visibility snapshot)AI citations — per-platform counts of public AI answers citing the site, one snapshot per configured providergtmesh pull aigtmesh topic performance / gtmesh ui (read-time) — never plan/apply
data/raw/links/<provider>/ (the backlink snapshot)External authority — per-page backlink/referring-domain counts, site totals + Domain Rating history, and the referring-domains list, one snapshot per configured link indexgtmesh pull linksgtmesh ui (a read-time join — the Links tab, the Authority card, the per-topic rollup) — never plan/apply
graph/{nodes.jsonl,edges.tsv}The committed graph. Holds both what you authored (entity + seed nodes above) and the derived mesh — every demand-minted page node, every edge, each node’s statusauthored nodes via graph upsert / page add; the derived mesh via apply / seal / lifecyclethe SSG (via site.manifest.json), the skills, every downstream reader
graph/meta.jsonSchema version — which engine schema your committed state is on ({ "schema_version": N }; absent ⇒ 0)gtmesh upgrade only (the migration chain)upgrade, to know which migrations to run

Two kinds of thing share that committed graph. Some nodes and props you author — entity nodes and their facts (graph upsert), the source-less seed pages (page add); the rest the engine derives — demand-minted page nodes, every edge, each node’s status. You hand-edit neither: the authoring verbs write-then-validate, and apply / seal / lifecycle own the derived mesh. That the committed graph is the state file, and plan only ever reads it, is invariant #2.

The three questions everyone asks first

These come up on every first build. Answering them is most of the mental model.

“Is authoring an entity purely about relationships, and a seed page about which pages get created?” Close, with one correction. An entity — a node you author with gtmesh graph upsert <id> --label <class/axis> --prop name=…,source_url=… — is your domain graph: the things (brands, products, categories) and how they relate (class, cluster membership, aliases, facts). It does not, by itself, create a page. What creates a page is either demand (a keyword in the bag, classified) or a decree (gtmesh page add, which writes a source: seed node). So: the entity node supplies the facts and identity a page resolves against; page add decrees the source-less pages (the hubs/landings) into existence. Running page add on an id that already has an entity node keeps its facts — the node simply gains the page label and a page-type structure. A demand-backed page needs neither decree nor an entity — just a keyword — but resolving it to an entity is what lets it fold, link up, and get a typed hub. See Folding demand into a hub.

“What writes demand — is it pull?” Yes. gtmesh pull is the only thing that writes market signal, and it writes it to the bag (data/raw/, immutable and timestamped). Nothing else fetches. plan reads the frozen bag and joins those metrics onto pages — including onto curated seeds/<class>.csv terms, which carry no metrics of their own and pick them up from the bag by keyword at plan time. That’s why a seeds/<class>.csv file holds terms-and-provenance only: the numbers live in one place, the bag, and refresh on their own cadence.

“Is <X> a gtmesh concept, or something the writer/SSG invented?” The dividing line is invariant #4: nothing in the engine names your domain or calls a model. The engine produces graph node props and edges — identity, slug, topical and domain edges, clusters, mesh links, hashes — and nothing else. Anything domain-shaped (a related-products query, a computed label, a rendered widget) comes from your templates, your SSG, or the writer — not the deterministic engine. When in doubt: if it’s a node prop or an edge in the graph, it’s a gtmesh concept; if it only appears in a rendered page, it’s a template/SSG concept.

The flow

Reading top to bottom — committed inputs on the left, the one derived output at the bottom:

YOU / operator / harvest PROVIDER (Ahrefs / GSC) ┌──────────────────────────────────┐ ┌─────────────────────────┐ │ gtmesh.config.yaml (policy+scope)│ │ gtmesh pull demand │ │ reference/signals.csv │ │ gtmesh pull performance│ │ seeds/<class>.csv (terms) │ └───────────┬─────────────┘ │ │ │ writes market signal └───────────────┬──────────────────┘ ▼ │ data/raw/ (the bag — immutable) │ gtmesh graph upsert (entity nodes + facts) │ │ gtmesh page add (source=seed pages) │ │ │ author INTO the committed graph │ └─────────────┴───────────────┬─────────────────┘ gtmesh plan (read-only) classify → score → seedUnion → mesh │ reads config + graph + bag; computes │ DESIRED state, prints the diff gtmesh apply (the writer) graph/{nodes.jsonl,edges.tsv} (authored nodes + derived mesh) │ → site.manifest.json (SSG projection) SSG · skills · gtmesh graph get

The committed graph is both an input (the entity + seed nodes you author into it) and the output (apply re-derives the mesh onto those nodes). The pipeline inside plan is always the same order: classify keywords into typed pages → score them into priority → seedUnion folds demand into your decreed seed pages → mesh derives the link graph. plan writes nothing; apply is what turns the computed desired state into the committed graph. (Full command detail: Plan & apply.)

The two data planes

Demand and performance both come from the outside world, but they enter the model differently — and keeping them on separate planes is a deliberate invariant, not an accident of plumbing:

  • Demand is folded. pull pulls keyword metrics into the bag; plan/apply fold them onto the committed graph as node props (total_vol, opportunity, head_keyword). Demand is the deterministic target — it decides what the mesh should become, so it lives in the committed state and every planning surface reads it from there.
  • Performance is a snapshot. The observation plane has three channels — search performance (pull performance: observed clicks / impressions / CTR / position, into data/raw/performance/<provider>/), AI visibility (pull ai: how often public AI answers cite the site, per platform, into data/raw/ai/<provider>/), and authority (pull links: who links to the site — per-page backlink counts, referring domains, Domain Rating, into data/raw/links/<provider>/) — committed, timestamped, immutable, like every bag pull. But these are volatile observations, so they are never folded: gtmesh topic performance and gtmesh ui join the latest snapshots against the graph at read time, and plan/apply never look at them.

The asymmetry is the point. How your pages perform must never silently change what the engine builds — no perf-driven reconcile actions, statuses, or cooldowns. The performance report informs you (which topics are decaying, in striking distance, under-clicked); only a human, advised by it, changes the graph. That keeps the build loop deterministic end to end while the tune loop stays honest about what it is: a scoreboard. The workflow that uses both planes is the two-sided refresh loop; the adapter detail is in the Ahrefs/GSC integration spec.

Reading the derived state

Don’t parse the graph files by hand — read a node through the typed, read-only accessor:

  • gtmesh graph get <id> — one page node’s labels + props (including its folded entity facts) as JSON.

It’s read-only by design; the graph’s only writers stay apply / seal / lifecycle. The SSG reads the derived site.manifest.json (and the graph-native content-types lib), not the graph files directly.

  • The mental models — the load-bearing ideas (authority, slug freeze, the invariants)
  • Glossary — each noun defined on its own
  • Folding demand into a hub — how a decreed hub absorbs its demand cluster
  • Plan & apply — the compute-diff-then-write loop
  • Tuningthe plan says X is unresolved — which file do I edit?
  • Build a website — how an SSG consumes this derived state (and why the split matters when you read it)
Last updated on