---
name: query-pkg
description: Answer a "what does the repo say about X, where was Y decided, what is the status or provenance of Z, which sources are still unexplored, what depends on bead W" question by running a SPARQL query over the ingested Project-Knowledge-Graph (PKG) instead of reading whole documents. Use when an agent working on sparq needs a sourced, answer-sized fact from AGENTS.md, the skills, or the bd backlog and would otherwise Read or Grep a large doc. The mechanism is introspect then ground then ask via the pkg-query helper in crates/sparq-kb. DEFAULT path is to DELEGATE the round-trip to a model:haiku sub-agent as a natural-language tool call (measured cheaper at equal quality, sq-zbyo7; see bench/pkg-dogfood/RESULTS.md); plain in-context pkg-query is the fallback.
---

# Query the PKG — introspect → ground → ask

[OPUS-4.8] Bead **sq-2m6zm.3** (epic sq-2m6zm). Design record:
`research/dogfooding-sparq-knowledge-graph.md` (§4.1 frontier queries, §6 worked
questions). 🤖 SPARQ agent — dogfooding sparq as a project knowledge graph.

The PKG is sparq's own knowledge stored as RDF triples: `pkg:Finding`s (sourced,
confidence-tagged discoveries from AGENTS.md), `pkg:Source`s (skills / docs with an
explored-status), `pkg:Technique`s (the surfaces), and `pkg:Task`s (the bd backlog
projected to RDF with `pkg:dependsOn` edges). Instead of `Read`-ing a whole document to
find one fact, run a SPARQL query and read the **answer-sized** result. The answer is
computed by sparq's own engine over the ingested graph — within the store the agent
cannot fabricate a fact the data does not contain (the engine bounds the result set), and
an empty result is the honest "I don't know / none outstanding" answer.

> **Honesty.** This skill is the *mechanism*. The cost saving versus reading the source
> document is now **MEASURED** (bead **sq-zbyo7**, the agent-flavour cheap-model NL-tool:
> ≈ 30× cheaper in $ than read-docs at equal quality on PKG-answerable tasks); the full
> table, method, and honest caveats live in `bench/pkg-dogfood/RESULTS.md` (the sanctioned
> measurement record) — this file only points at the headline. The win is scoped to
> **PKG-answerable** questions by construction. The PKG is a Phase-1 *head slice* (the
> AGENTS.md finding set + a mechanical bd→Task projection + the heaviest skills'
> front-matter + the trust-graph admission-program provenance tier of `sq-7utko` —
> sourced, caveat-carrying findings from `research/trust-graph-authorisation-2026-07.md`,
> e.g. "unlinkability is NOT a v1 guarantee, gated on `sq-qhy4`"), not the whole corpus —
> a miss means "not in the head slice yet", so fall back to Read/Grep.

## The helper

One command, in `crates/sparq-kb`, behind the default-OFF `query` feature (keeps the lean
default build a pure data crate):

```bash
cargo run -p sparq-kb --features query --bin pkg-query -- <args>
```

It loads `pkg.ttl` (the ontology) + `pkg-instances.ttl` (the ingested data) +
`trust-graph-findings.ttl` (the `sq-7utko` provenance tier) into a sparq
store, runs a **named canned query** or a **raw SPARQL string**, prints the executed
SPARQL (always — for verification) and the result rows. Add `--features close` and
`--close owl-rl` to materialise the RDFS/OWL-RL closure first (entails the `pkg:dependsOn
owl:inverseOf pkg:blockedBy` pair and `pkg:couldBeMergedWith` symmetry — the design's
"always close first" step).

`--extra-graph <path>` (repeatable) loads extra Turtle file(s) alongside the PKG before
querying — their triples become visible to the query and participate in `--close owl-rl`.
This is the seam the FO-KM benchmark (epic `sq-mztg8`, Metric 1) uses to load a
foundational-ontology overlay so its `rdfs:subClassOf` axioms entail FO-typed facts; the
arms, overlays, and tasks live in `bench/fo-km/`.

## Cheap-model NL-tool — the DEFAULT path (delegate to Haiku)

[OPUS-4.8] **For a PKG-answerable question, do NOT run the round-trip yourself in the
expensive orchestrator. DELEGATE it as a natural-language tool call to a `model:haiku`
sub-agent.** The cheap model does the whole `introspect → ground → SPARQL → run → NL`
loop; the orchestrator (Opus) only emits the question and reads the answer back. It
never sees the schema card, the SPARQL, or the raw rows.

**Why this is the default.** The verbose NL→SPARQL→run→NL middle is exactly the part a
~15×-cheaper model handles fine, and pushing it off the expensive orchestrator is a
large **model-price-weighted** saving. The agent-flavour cheap-model NL-tool was
measured (bead **sq-zbyo7**, N=30 frozen PKG-answerable tasks) at **≈ 30× cheaper in $
than Opus reading the docs, and ≈ 16× cheaper than Opus running `pkg-query` itself, at
equal answer quality.** Full table + method + caveats: `bench/pkg-dogfood/RESULTS.md`.
(The decision metric is $, not raw tokens — that is why the cheap-model arm wins even
though its raw-token count is similar.)

**The brief to give the Haiku sub-agent** (it must return a self-checkable answer):

> Use the `query-pkg` skill. Answer this question over the PKG by `introspect → ground →
> ask` via `pkg-query`. Question: «…». Return (1) a concise NL answer, (2) the **exact
> executed SPARQL**, and (3) the **provenance + confidence** of each fact (the
> `dcterms:source` section anchor and `pkg:confidence` each row carries). If the query
> returns 0 rows, say so — an empty result is the honest "not in the head slice / none
> outstanding" answer; do not invent rows.

**The verification echo (the guardrail).** Because the sub-agent returns the executed
SPARQL + resolved IRIs + the per-row provenance/confidence, the caller can **verify**
the answer was computed from a real query over the data, not guessed — the soundness
echo. If the returned SPARQL does not match the question, re-ask or fall back; never
accept a bare NL answer with no query behind it.

**Abstain → fall back (the safe-default rule).** [OPUS-4.8] The PKG is a Phase-1 head
slice, so it cannot answer every question — and the safe behaviour is to **abstain, not
guess**. An empty / 0-row result (or an explicit `NOT_IN_PKG` from the sub-agent) is the
honest "not in the head slice / none outstanding" answer; the sub-agent brief above
already forbids inventing rows. **On an abstain, fall back to `Read`/`Grep` on the actual
docs/code for that question** — the measured win in `bench/pkg-dogfood/RESULTS.md` is
scoped to PKG-answerable questions *by construction*, so a miss is expected and is not a
failure of the flow. Never paper over an abstain with a fabricated answer.

**Tier escalation — Haiku by default, escalate only when accuracy is critical.**
[OPUS-4.8] The cheap-model NL-tool (Haiku, arm **C**) is the default precisely because
it is the cheapest arm at equal quality on PKG-answerable tasks. For an
**accuracy-critical** lookup — one where a cheap-model misread would be costly — escalate
a model tier: run the round-trip on a Sonnet/Opus `pkg-query` (this is arm **B**, which
is still cheaper than Opus reading the docs, just not as cheap as the Haiku arm). Use the
verification echo as the trigger: if the returned SPARQL does not match the question, or
the answer looks off, re-ask, escalate a tier, or fall back to reading — do not lower the
bar to make the cheap arm "work".

**Fallback — plain Opus `pkg-query`.** When sub-agent delegation is not available (e.g.
you are already inside a leaf sub-agent, or the dispatch path is unavailable), run
`pkg-query` yourself in-context as described below. That is arm **B** of the measurement
— still cheaper than reading the docs, just not as cheap as the Haiku NL-tool.

## (a) INTROSPECT — what can I ask about?

Before grounding a question, learn the dataset's effective schema so you know which
classes and predicates exist (cheaper than scanning sample data). The `schema-classes`
query lists every `pkg:` class actually instantiated, with a count:

```bash
cargo run -p sparq-kb --features query --bin pkg-query -- --query schema-classes
```

```text
class  |  instances
Task  |  1255
Finding  |  11
Source  |  6
Document  |  6
Technique  |  5
```

`--query schema-properties` lists the predicates in use (label, type, title, identifier,
priority, status, issueType, isPartOf, **dependsOn**, subject, source, …). List every
canned query with:

```bash
cargo run -p sparq-kb --features query --bin pkg-query -- --list
```

## (b) GROUND — translate the question to SPARQL over those terms

The canned queries are the §4.1-class, verified-expressible SPARQL frontier queries
(plain SELECT / `FILTER NOT EXISTS` / `GROUP BY`; the §4.2/§4.3 N3 rules are explicitly
Phase-2/3 and NOT used here). Pick the template that matches the question shape; some take
an argument via `--arg`.

| Question shape | canned `--query` | `--arg` |
|---|---|---|
| "What does the repo say about TOPIC?" | `findings-about` | a topic IRI |
| "Where was this decided / what is its provenance + confidence?" | `finding-provenance` | — |
| "What are the DQV quality measurements (subject + metric + value)?" | `finding-quality-dqv` | — |
| "Which sources are still UNEXPLORED (target follow-up)?" | `unexplored-sources` | — |
| "What is the explored-status of the source catalog?" | `source-status` | — |
| "What does bead W depend on, and is each dependency done?" | `task-depends-on` | a bd id |
| "What is blocked by bead W (downstream impact)?" | `task-blocks` | a bd id |
| "Which sources should I read next (priority order)?" | `high-followup-priority` | — |
| "How many beads are ready (§4.1 dependency frontier)?" | `ready-frontier` | — |

The topic IRIs in the current head slice are
`https://sparq.dev/ns/pkg/kb#topic-merge-discipline`,
`…#topic-subagent-rules`, and `…#topic-zk-discipline`.

To go beyond the templates, write a raw SPARQL SELECT/ASK against the `pkg:` terms and
pass it with `--sparql '<query>'`. Use `--sparql-only` to print the SPARQL without running
it (verify before executing).

## (c) ASK — run the helper and read the answer

Run the chosen query; read the small result table instead of the source doc. Each finding
row carries its `dcterms:source` section anchor and `pkg:confidence`, so the answer is
**sourced and confidence-tagged**, not bare prose.

---

## End-to-end examples (real questions, real returned answers)

### Example A — "What is the merge discipline?" (a findings-about GROUND query)

Instead of reading the whole `AGENTS.md` contribution-workflow + gate-matrix sections:

```bash
cargo run -p sparq-kb --features query --bin pkg-query -- \
  --query findings-about --arg https://sparq.dev/ns/pkg/kb#topic-merge-discipline
```

Returned answer (sourced + confidence-tagged):

```text
label  |  section  |  conf
A PR merges only when ci-summary is green and every review thread is resolved
    |  AGENTS.md#contribution-workflow--prs-reviews-resolved-the-ci-summary-gate  |  0.98
A verified-clean non-perf PR auto-arms; never arm auto-merge on a stacked PR whose base is not main
    |  AGENTS.md#contribution-workflow--prs-reviews-resolved-the-ci-summary-gate  |  0.93
2 row(s).
```

(Swap the topic to `…#topic-zk-discipline` for "what's the merge discipline for a ZK PR?"
— it adds the forge_gates + gate-count snapshot row and the HARD privacy-claims gate row.)

### Example B — "Which sources are still unexplored?" (an honest empty answer)

```bash
cargo run -p sparq-kb --features query --bin pkg-query -- --query unexplored-sources
```

Returned answer:

```text
source  |  title  |  status  |  prio
0 row(s).
```

Over the Phase-1 ingest every source is `pkg:Explored`, so the targeted-follow-up list is
**empty** — the honest "none outstanding" answer, computed by the engine rather than
guessed. (As `Unexplored`/`Exploring` sources are ingested in later phases they appear
here automatically.)

### Example C — "What is blocked by bead sq-8thu?" (a task-blocks GROUND query)

Instead of running `bd dep tree` or grepping `.beads/issues.jsonl`, ask the read-model:

```bash
cargo run -p sparq-kb --features query --bin pkg-query -- --query task-blocks --arg sq-8thu
```

Returned answer (downstream dependents + each one's status; truncated):

```text
downstreamId  |  downstreamStatus  |  downstreamTitle
sq-0po6  |  Closed  |  page: /surface/inference (tier-b live)
sq-11zy  |  Closed  |  page: /surface/streaming-rsp (tier-b live)
sq-13rg  |  Closed  |  ZK: bb.js in-browser UltraHonk proving wiring (WZK)
…
22 row(s).
```

The inverse direction (`?b pkg:blockedBy sq-8thu`) is never asserted in the data; under
OWL-RL closure it is entailed, so this is equivalent — verify with:

```bash
cargo run -p sparq-kb --features close --bin pkg-query -- --close owl-rl \
  --sparql 'PREFIX pkg: <https://sparq.dev/ns/pkg#> PREFIX dcterms: <http://purl.org/dc/terms/>
            SELECT (COUNT(*) AS ?n) WHERE { ?b dcterms:identifier "sq-8thu" . ?b pkg:blockedBy ?d }'
```

## The natural-language tool (agent flavor) — `sq-ve5dy`

Instead of writing the SPARQL yourself, you can delegate the whole round-trip to a
**cheap-model (Haiku) sub-agent** so the expensive orchestrator only emits a plain-English
question and reads a short answer. The sub-agent (`.claude/agents/sparq-pkg-nl.md`,
`model: haiku`) does NL → introspect → ground → SPARQL → run → NL answer itself.

**Soundness guardrail (load-bearing):** the tool ALWAYS returns the **executed SPARQL +
resolved IRIs + grounding confidence** so the caller can verify the answer was *computed*,
not guessed — it never silently answers from a guessed query. `pkg-query --json` emits this
**NL-tool envelope** (`crates/sparq-kb/src/query/nl_tool.rs`):

```bash
cargo run -q -p sparq-kb --features query --bin pkg-query -- --query schema-classes --json
```

```json
{
  "answer": "5 row(s). Columns: class, instances.\n- Task | 1255\n- Finding | 11 …",
  "executed_sparql": "PREFIX pkg: <https://sparq.dev/ns/pkg#> SELECT ?class …",
  "resolved_iris": ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
  "ungrounded_iris": [],
  "hints": [],
  "row_count": 5,
  "confidence": "canned"
}
```

`confidence` is a **query-grounding** signal (NOT an answer-correctness claim): `canned` =
a curated template; `grounded` = a raw query whose every term exists in the data;
`ungrounded` = a raw query that used a term the data lacks (so it matched nothing — the
`hints` give same-namespace repair candidates; re-ground and re-run before trusting it). The
deterministic `answer` is the engine's computed rows; the cheap model rephrases it. The
**PRODUCT flavor** (sparq calling a configurable cheap-model endpoint as a real GenAI
integration) needs sparq's own API key (an external-credential dependency) and is a
follow-up — only the **agent flavor** (no API key) ships here.

> **Honesty.** Whether routing PKG access through a cheap-model sub-agent actually cuts the
> orchestrator's *model-price-weighted* cost is now **MEASURED** (beads sq-zbyo7 / sq-jgi97,
> the real-transcript N=30 3-arm A/B that superseded the sq-2m6zm.4 #1078 proxy): the Haiku
> NL-tool was ≈ 30× cheaper in $ than read-docs at equal quality. The full table, method,
> and caveats live in `bench/pkg-dogfood/RESULTS.md` (the sanctioned record); this file
> only points at it. The win is scoped to **PKG-answerable** questions by construction —
> a miss is an honest abstain, not a fabricated answer.

## Authoring new Findings — the write-path (`sq-mztg8.2`)

[OPUS-4.8] The `pkg:Finding` tier this skill queries is **authored**, not hand-written as
raw Turtle. To add or edit a Finding, edit the compact, IRI-free YAML-LD source
`crates/sparq-kb/ingest/agents-findings.yaml.ld` (generalising the shipped
`sec-prop.yaml.ld` pattern), then recompile:

```bash
python3 crates/sparq-kb/ingest/ingest_pkg.py \
  --beads .beads/issues.jsonl --skills-dir skills \
  --findings-yamlld crates/sparq-kb/ingest/agents-findings.yaml.ld \
  --out crates/sparq-kb/ingest/pkg-instances.ttl
```

A **deterministic** compiler (`ingest/yamlld_compile.py`) — not an LLM — expands the typed
`@graph` to schema.org-typed PKG Turtle. You write fluent author keys + concept TOKENS
(e.g. `about: merge-discipline`), never IRIs or `xsd:` datatypes. The **guarded `V()`**
resolver grounds each TOKEN against the file's `concepts:` catalog (lexical: local-name or
normalised `prefLabel`); an **ambiguous TOKEN is a HARD compile error** — the write-path
analogue of the read-path abstain, so a mis-resolution fails loud rather than emitting a
silent wrong IRI. The compiled tier is gated for **0 SHACL violations + triple-for-triple
round-trip** against the prior hand-authored tier
(`cargo test -p sparq-kb --features validate --test yamlld_write_path`; the compiler's
parser + resolver are self-tested by `scripts/tests/test_yamlld_compile.py`). See
`research/fo-llm-bridge.md` §3.4 / §6 Phase 4.

## Literature-ingestion scaffolding — fixtures only (`literature` feature, `sq-2489d.5`)

[OPUS-4.8] The **scaffolding** for the scaled, provenance-stamped literature-trawling tier
(GenAI-KB Phase 5, design `research/provenance-driven-genai-kb.md` §4/§5) lives behind the
default-OFF `literature` feature in `crates/sparq-kb/src/literature/`. It exercises the
`[connector] → [normalise] → [extract (record/replay)] → [ground] → [emit TTL] →
[SHACL gate] → [sidecar]` pipeline over **committed fixtures** and makes **ZERO network and
ZERO live-model calls** — the only LLM-in-the-loop step (extraction) is isolated behind the
`Extractor` **record/replay trait**, and the shipped adapter (`RecordedExtractor`) replays
a frozen tape. The live pilot (a real cheap-model batch over OpenAlex/S2) is the separate,
credential-gated **Phase-6** bead (`needs-access`).

Load-bearing honesty invariants the scaffolding enforces: the **deterministic
grounding-resolver** (`literature::ground`) requires every committed Finding's
justification to be an entailed span of the source abstract AND every cited DOI to resolve
to an in-batch `pkg:Source` — failures are **quarantined to the sidecar, never silently
dropped** (`literature::pipeline::Sidecar`). Every emitted Finding sits on the low-trust
machine tier (`prov:wasAttributedTo` a `pkg:MachineAgent`, capped at `secx:Conjectured`
with a bounded confidence) enforced declaratively by `shapes/literature.shapes.ttl`. The
Phase-5 metric (the **citation-grounding rate**) is *computed at run time* from the fixture
batch — no hard-coded number. NOT appended to `pkg-instances.ttl`: bulk ingestion is gated
on the Phase-6 per-topic recommend-adopt verdict. Run:
`cargo test -p sparq-kb --features literature,validate --test literature_pipeline`.

**CORE API v3 live connector** (`sq-tzars.1`, `connector_core`): a second connector
(`parse_core_batch` → the same DOI-keyed `SourceStub`s as `parse_openalex_batch`) plus a
live HTTP client behind the default-OFF **`literature-live`** feature (implies `literature`,
pulls only `ureq`). The pure parse + paging/retry discipline (`fetch_paginated`,
`backoff_delay`, the `Transport` trait) is under `literature` and runs over a **fake
transport** + the committed `fixtures/literature/core-batch.json` (a REAL, SANITIZED CORE
response — no key, no full-text) so CI stays **network-free**; only `CoreClient` (from the
`CORE_API_KEY` env var, **never** committed/logged) touches sockets. `SourceStub` now carries
`license: Option<String>`, captured **fail-closed** by both connectors — `None` (unknown) is
treated as NON-REDISTRIBUTABLE by the downstream dump tiering (`sq-tzars.7`). Rate-limit
discipline honours `429`/`Retry-After` with bounded backoff + a HARD per-run request cap.
Run: `cargo test -p sparq-kb --features literature,literature-live --test core_connector`.

## When NOT to use this

- The fact is **not in the head slice** (PKG is a Phase-1 subset) → an empty/partial
  result means fall back to `Read`/`Grep`. An empty result is honest, not a failure.
- The document is **small** and you would read it once → loading it directly is fewer
  tokens and fewer round-trips (the design's §1.1 honest boundary).
- For the **live** ready-frontier (git/gh/nproc-aware) use `scripts/push-frontier.sh`;
  `ready-frontier` here is only the §4.1 dependency half over the projected backlog.
- bd remains the **source-of-record** for tasks; the PKG `pkg:Task`s are a read-model
  mirror, not a place to write task state.

## Where this lives

- Helper binary: `crates/sparq-kb/src/bin/pkg_query.rs` (`--bin pkg-query`; `--json`
  emits the NL-tool envelope).
- Canned queries (the library the binary + the test share):
  `crates/sparq-kb/src/query/canned.rs`.
- NL-tool envelope (`sq-ve5dy`): `crates/sparq-kb/src/query/nl_tool.rs`
  (`run_canned` / `run_raw` → `NlToolResult`).
- Agent-flavor sub-agent (`model: haiku`): `.claude/agents/sparq-pkg-nl.md`.
- Loader + optional closure: `crates/sparq-kb/src/lib.rs` (`query` / `query::close` modules).
- Rot-guard tests: `crates/sparq-kb/tests/query_canned.rs` and
  `crates/sparq-kb/tests/nl_tool.rs`
  (`cargo test -p sparq-kb --features query`; add `--features close` for the closure test).
- Ontology + data: `crates/sparq-kb/ontology/pkg/pkg.ttl`,
  `crates/sparq-kb/ingest/pkg-instances.ttl`.
- Write-path (`sq-mztg8.2`): authoring source
  `crates/sparq-kb/ingest/agents-findings.yaml.ld`, deterministic compiler
  `crates/sparq-kb/ingest/yamlld_compile.py`, gates
  `crates/sparq-kb/tests/yamlld_write_path.rs` + `scripts/tests/test_yamlld_compile.py`.
- Literature-ingestion scaffolding (`literature` feature, `sq-2489d.5`):
  `crates/sparq-kb/src/literature/` (`connector` / `extract` / `ground` / `pipeline`),
  shapes `crates/sparq-kb/shapes/literature.shapes.ttl`, fixtures
  `crates/sparq-kb/fixtures/literature/`, gate
  `crates/sparq-kb/tests/literature_pipeline.rs`.
- CORE v3 live connector (`literature`/`literature-live`, `sq-tzars.1`):
  `crates/sparq-kb/src/literature/connector_core.rs`, fixture
  `crates/sparq-kb/fixtures/literature/core-batch.json`, gate
  `crates/sparq-kb/tests/core_connector.rs`.
- **Citation renderer** (`query` feature, `sq-2489d.11`):
  `crates/sparq-kb/src/query/citations.rs` — `render_citations(&graph, &rows)` renders
  `prov:wasDerivedFrom` sources from `FINDING_PROVENANCE` rows as `[source: label]`
  citations; returns `CitationReport` with `CitationMetrics` (resolution-rate + fabricated-count).
  Gate: `crates/sparq-kb/tests/citations.rs`.
