---
title: "What Is a Skills Graph — and How Does It Grow?"
description: "Written 2026-09-16, the day the skills graph UI moved from Tapestry to the capcom Repos page. Sources: tapestry-node/docs/repo-skill-graph-v1.md, skill-graph-architecture-v1.md, src/lib/skillValidation.ts,…"
created: 2026-09-16
updated: 2026-09-21
authors: ThinkingCap R&D
topics: [Tapestry]
status: published
canonical: https://console.thinkingcap.com/rd/Tapestry/what-is-a-skills-graph-and-how-it-grows
date: 2026-09-16
---

# What Is a Skills Graph — and How Does It Grow?

*Written 2026-09-16, the day the skills graph UI moved from Tapestry to the
capcom Repos page. Sources: `tapestry-node/docs/repo-skill-graph-v1.md`,
`skill-graph-architecture-v1.md`, `src/lib/skillValidation.ts`,
`src/lib/codeSurface.ts`, `src/lib/repoSkillGraph.ts`, `src/routes/gleaner.ts`,
and the ported capcom implementation (`capcom apps/api/server/routes/repos.ts`).*

---

## 1. The short answer

A **skills graph** is the estate's written record of *what each repo can do,
for whom, and why* — one structured document per capability, keyed to the repo
that owns it, gated by one strict validator, and queryable as a set rather than
as tribal knowledge.

It exists because of a simple gap: the fleet has ~350 repos and nobody —
human or agent — could answer "what does this repo do, and how much of it do
we actually understand?" The skills graph is the machine-readable answer to
both halves of that question:

- **the skills** (`repo_skills`) — what we *say* the repo does;
- **the surface** (`repo_surface`) — the triggerable behaviours the scanner
  *found in the code*, which is how we know what we haven't described yet.

Douglas's framing, 2026-09-05: *"like registering your thumb print by touching
the sensor in many angles till all the lines are captured… I want to know how
close I am at all times and what is missing."* The skills are the lines
captured so far; the surface is the whole thumb.

---

## 2. The atom: one skill = one RSD

Every skill is a **Repo Skill Definition (RSD)** — a single JSON document.
Nine fields are required by the validator, in this order:

| Field | What it answers |
|---|---|
| `name` | Short kebab-case identifier (`flag-expiring-certification`) |
| `description` | One sentence — the three-second elevator pitch |
| `skillStatement` | **The WHY**, in the fixed template *"\<verb\> \<what\> for \<who\> so that \<why\>"*. The only required field that carries intent, which is why the Designer asks it first. No "so that" clause → not a skill statement. |
| `audience` | Who invokes it: a person in a role, the system, a scheduled job, another skill |
| `category` | Dotted taxonomy key in the skill map (`compliance.certification`) |
| `input_contract` | Inputs needed to act on ONE case, field → type |
| `output_contract` | The outcome produced, field → type. The skill's binding is compiled from this; change it and the binding goes STALE |
| `requires` | External capabilities needed, as `{role, capability}`. **An empty array is a real answer** ("self-contained") — not the same as the field being absent, and the difference matters (§5) |
| `resources` | Loom-provided stores/queues touched, as `{id, role, description}`. `id` must be `{type}.{name}[.{qualifier}]`; `role` one of consume / read / write / read_write / queue_producer / queue_consumer |

Beyond the required nine, an RSD can carry:

- **`kind`** — how it deploys: `page`, `service`, `scaled_service`,
  `non_scaled_service`. Required once a skill declares side effects.
- **`trigger`** — what sets it off, constrained by kind: `service` → http
  (the default), `scaled_service` → queue (must also declare a matching
  `queue_consumer` resource), `non_scaled_service` → cron.
- **`endpoints`** — a standalone service should declare at least one
  `{name, shape: "sync"}`.
- **`operationalContext`** — authored notes that are *not* contracts:
  derivations (how each output is produced — this is what makes outputs
  "reachable"), edge cases, the reasoning behind a rule.

### The one rule everyone breaks

The RSD is the **intent layer**: it says *what* and *why*, never *which
vendor*. The STRICT validator **hard-blocks** infrastructure names anywhere in
the document — azure, postgres, mongodb, kafka, redis, s3, smtp, … — with the
message "Move to system-definition (W&W) layer". It even soft-warns on
`http`, `rest`, `json`, `api`. Consequence: a skill cannot record which
endpoint, queue, or table realises it. That mapping lives elsewhere
(`repo_surface.skill_id`), deliberately.

### A complete example

The downloadable sample (capcom Repos page → **Sample RSD**) is one
fully-defined skill that passes the STRICT validator with zero gaps —
`flag-expiring-certification`: finds a learner's certifications lapsing inside
a window, *for a compliance manager, so that a lapse can be corrected before
the learner loses their standing*. Every output traces to an input or a
documented derivation; its one resource is `store.certification-records` with
role `read`. It is deliberately unexciting — it is the target to read, not a
showpiece.

---

## 3. The gate: the STRICT validator

Nothing enters the graph ungraded. `classifySkillState` (pure, deterministic —
no DB, no AI, no env) classifies every skill as **COMPLETE**, **READY**,
**DRAFT**, or **BLOCKED**, and it is the *same* gate everywhere:

- the **executor** applies it when a proposal is approved (BLOCKED refuses
  the write; missing fields land as DRAFT — only *incoherence* blocks);
- the **Dresser worker** hard-gates on BLOCKED and asks questions on
  RESOLVABLE items;
- the **Repos page** grades with it at read time, so the number you see
  cannot drift from the gate that decides whether a skill is buildable.

Blocking invariants (each one discovered by running real documents through
the gate):

- **REACHABILITY** — every output must be explainable: present in the inputs,
  provided by a dependency, a declared pivot, or a key in
  `operationalContext.derivations`. "An output nobody can explain the origin
  of is not a contract, it is a wish."
- **RESOURCE_ROLE_ENUM / RESOURCE_ID_FORMAT** — legal roles and dotted ids.
- **KIND_REQUIRED / TRIGGER_SHAPE / QUEUE_CONSUMER_REQUIRED** — deployment
  coherence for non-page skills.
- **SINGLE_SOURCE** — contracts live in `input_contract`/`output_contract`
  only; copying them into operationalContext is flagged.
- **Tech leakage** — the vendor-name block above.

The validator's guardrail, in its own words: *blocking prefers false negatives
(never accuse wrongly); warnings prefer false positives (never miss a smell).*

---

## 4. The graph: keyed by repo, edged by declaration

Skills belong to a **repo**, not to the conversation that happened to describe
them (that was the v0 bug: 2,656 RSD rows lived inside 3 teaching
conversations; zero where real work happened). The canonical key is
`repo_catalog.full_name` — `thinking-cap/tc-scorm-node` — with local-only
repos normalised to their git string so they still get a graph.

**Edges** come from four RSD fields: `requires`, `dependsOn`, `composedOf`,
`resources`. The modal resolves them by both currencies a skill can be
addressed in (gleaner skills carry a slug `skill_id`; loom skills a `pairId`)
and lists references to skills outside the repo as "living in other graphs".

### The honest finding: the graph is currently flat

Measured across every skill in the fleet (202 on 2026-09-06; 247 today):
`dependsOn` absent everywhere, `composedOf` absent everywhere, and
`requires`/`resources` present but **empty** on every single skill. There is
nothing to draw — which is why the old Mermaid chart was deleted. It rendered
disconnected boxes and said "no structure" less clearly than one sentence.

The subtle trap the UI now separates: `requires: []` is a *real answer* —
"self-contained" — that the validator accepts; a *missing* `requires` means
nobody was ever asked. Conflating the two is exactly how a graph ends up flat
without anyone noticing, so the modal renders them differently ("answered
empty" vs "not answered"). Skills also never reference each other by name —
cross-skill coupling is meant to happen through a loom-level shared resource,
producer never naming consumer. Declaring those edges is the next frontier;
the graph today is a well-formed *inventory* waiting for its first edge.

---

## 5. The two numbers: completeness vs coverage

Conflating these is what made the old page read ~100% for a repo nobody had
described.

**Completeness** — *how well-formed are the skills we have?* Share of a repo's
skills the STRICT validator rates READY or COMPLETE. Reads high by
construction: the Gleaner pre-validates before proposing, so mined skills
arrive complete; loom skeletons start as DRAFT and pull it down. It says
nothing about how much of the repo is described — 8 thin skills against 382
entry points still score 100%.

**Coverage** — *how much of the code has a recorded why?* This is the
thumbprint. The denominator comes from the **code surface scan**: the scanner
counts **triggerable behaviours** ("ridges"), in the RSD's own vocabulary:

- **http** — one per *resource*, not per endpoint (`GET/PATCH/POST /api/looms`
  is one ridge, "looms", because they share one why; 286 endpoints is a
  number nobody can drive to 100%, ~40 resources is finishable);
- **queue** — one per consumer;
- **cron** — one per scheduled job;
- **event** — one per other long-running worker.

The headline score is **weighted why-coverage**: a *confirmed* why counts 1.0,
an *inferred* one 0.6 / 0.4 / 0.2 by confidence. An inferred why is "canon but
unverified" (Douglas, 2026-09-07) — read off the code, so it can restate what
the code does but cannot know why it exists; pure inference therefore tops out
near 60% and the number only reaches 100% when a human confirms.

Both metrics can be **null**, and null is load-bearing: a repo the scanner
can't parse (most of the C# estate) reports *not measured* — never 0% (which
accuses) and never 100% (which is the bug coverage exists to kill). The
thumbprint has to be able to say "I don't know".

---

## 6. How the graph grows

Every write — no matter its origin — travels the **same path**:
*propose → approve → STRICT-validate → write*. There is no side door. The
differences are only in who proposes and who clicks approve.

### a) The catalog sync (the graph's address space)

`syncRepoCatalogFromLocalGit()` runs at server start and on demand: every repo
managed by local git gets a `repo_catalog` row and is **always included** —
there is no selection step (Douglas, 2026-08-22: *"repos have moved to local
git… ALWAYS INCLUDE ALL repos managed by local git"*). The fleet's bare
mirrors are the authority; a repo GitHub stops listing still appears.

### b) Skeleton seeding at clone (bootstrapping a zero-skill repo)

When a repo first gets a clone and has no skills on record,
`seedRepoSkillSkeleton` opens one pending `skill_definition` decision with a
skeleton RSD (name from the catalog's friendly name, description from the
README or package.json, `buildState=draft`). It lands in the Weaver's normal
approval queue; approving it passes the STRICT gate as DRAFT (missing fields
are DRAFT — only incoherence BLOCKs), and the directive loop fills the rest in
via `skill_update`.

### c) The direct-loom directive (work drives the graph)

A direct loom with a repo gets a compact **REPO SKILL GRAPH** block injected
into its system prompt each turn: skills on record, a staleness signal
(approved decisions + build artifacts since the newest RSD update), the open
questions where a skill's intent is still unknown, and the directive:

> *When your work changes behavior, contracts, or dependencies, keep the graph
> true: propose new skills with `propose_skill`, corrections with
> `skill_update`. Proposals enter the normal decision queue for approval.*

Work still drives the graph — but when the WHY is missing, the Designer now
asks instead of guessing. Douglas, 2026-09-06: *"I want the Designer to be a
partner and she can't be if she doesn't know why."*

### d) The Gleaner (mining at fleet scale)

The volume producer. A transcript-mining service reads Claude/Kimi session
transcripts and files skill proposals into the **`gleaner-system` loom** as
ordinary pending decisions. Per Douglas's standing call (2026-08-24), Gleaner
proposals are **auto-approved** through the standard path (integrity check +
executor STRICT validation, one transaction each); the quality gate is
after-the-fact RSD inspection plus the validator itself, not per-decision
clicks. Provenance is derived, never declared: decisions from the gleaner loom
stamp `source='gleaner'` (a hardcoded `'loom'` once mislabelled the entire
2026-09-05 batch — a coverage metric built on a provenance column that lies is
worse than none).

### e) The surface scanner (the denominator)

Independently of skills, the scanner walks each parseable repo and persists
its ridges to `repo_surface` (retiring ones that vanished), feeding the
`repo_surface_coverage` view the Repos page reads. This is what turns "how
close am I and what is missing" from a feeling into a number — and the
`weakestRidges` query answers "what is weakest?" (no-why ridges first, then
lowest confidence) so attention goes where the graph is thinnest.

### Provenance

Every skill carries `source`: `loom` (a human in a real loom), `gleaner`
(mined), or `restore` (the seed script). The modal shows it as a chip.

---

## 7. Where it lives

**Data** (Azure Postgres `tapestry` database — unchanged by the UI move):

| Table | Role |
|---|---|
| `repo_catalog` | The address space: every local-git repo, friendly name, tier, icon |
| `repo_skills` | The graph itself: one row per skill revision (`rsd` jsonb, `binding`, `build_state`, `source`, `updated_at`) |
| `repo_surface` / `repo_surface_scans` | The ridges per repo + scan provenance |
| `repo_surface_coverage` (view) | The weighted why-coverage the UI displays (weights live only here, so UI and view never disagree) |
| `org_repo_cache` | Legacy GitHub-org listing cache — now unwritten, harmless |

**UI — capcom (since 2026-09-16):** the Repos page shows the per-repo
**Skills Graph (N · X% why)** button with the coverage bar and the full
tooltip, the whole-graph modal (every RSD as a document: answered fields,
"not answered" gaps vs "answered empty" answers, per-skill edges), and the
**"What a skill looks like"** header card with two downloads:

- **Schema** — the JSON Schema for an RSD, *generated from
  `REQUIRED_RSD_FIELDS` on every request* so it cannot drift from the
  validator;
- **Sample RSD** — the `flag-expiring-certification` worked example, asserted
  in tests to classify COMPLETE under STRICT.

Capcom reads the graph **read-only** straight from the tapestry database.
**Removed from Tapestry:** the page and its three page-only endpoints. **Kept
in Tapestry** (the platform consumes them): the loom focus API
(`/api/repos/available`), catalog curation, the start-of-day sync, and the
canonical validator — capcom carries a dated, verbatim copy of it (re-copy
both files together if the original ever changes).

---

## 8. Where it's going

Three honest gaps define the roadmap:

1. **Edges.** 247 skills, zero declared relationships. The graph becomes a
   *graph* the day skills start declaring `requires`/`dependsOn` — or when
   shared resources start coupling producer to consumer through the loom
   layer instead.
2. **Weak whys.** Inferred whys are canon-but-unverified by design and cap
   coverage near 60%. Moving a ridge from weak to strong is a one-line human
   confirmation — and is exactly the work the directive loop and the
   weakest-first queue put in front of people.
3. **The unscannable estate.** Most C# repos report *not measured*. That is
   honesty, not absence — but it means the thumbprint is only as complete as
   the scanners written so far.

The machinery for all three already exists: one write path, one gate, one
denominator, and now one page — on capcom — that shows all of it.
