dotgraph integration: contract Surfaces + awareness block + doctor/sync wiring (0.5.3)#49
Merged
Merged
Conversation
Every fresh contract.md scaffolded by `init_contract` now carries a
canonical `## Surfaces` section under the `<!-- anchor: surfaces -->`
HTML anchor. The section is a fenced YAML block whose key names match
dotgraph's `reconcile` flags verbatim so a consumer can pipe straight
through without remapping:
dotgraph reconcile \
--paths $(git diff --name-only) \
--claimed-tables $(yq '.surfaces.data.tables[]' contract.md) \
--claimed-keys $(yq '.surfaces.data.redis_keys[]' contract.md) \
--claimed-topics $(yq '.surfaces.data.kafka_topics[]' contract.md)
The locked schema:
surfaces:
code: # list of {id, why}
data:
tables: [] # mirrors --claimed-tables
columns: []
redis_keys: [] # mirrors --claimed-keys
kafka_topics: [] # mirrors --claimed-topics
collections: []
callers_to_update: # list of {id, reason}
tests_to_update: # list of paths / node ids
The anchor sits BEFORE `<!-- anchor: negotiation-log -->` so Surfaces
is part of the content hash and round-trips through `contract round`
unchanged. Convergence semantics work correctly: same Surfaces between
rounds → same hash; different Surfaces → different hash.
### Public surfaces
- `extract_surfaces_block(text) -> str` — raw YAML body or "".
- `parse_surfaces(text) -> dict` — normalized parse against
SURFACES_SCHEMA. Tolerant: missing top-level keys default to empty
arrays; malformed YAML returns empty schema; missing `surfaces:`
root returns empty schema.
- `count_surfaces(text) -> int` — total enumerated items, placeholders
excluded (scaffold's `<dotgraph_node_id>`, `<short reason>`, etc.
drop from the count so a fresh init reports 0).
- `SURFACES_SCHEMA` — the canonical shape consumers can rely on.
### Score JSON additions
`dotagent project contract score --json` now emits two additional
top-level fields:
"surfaces_enumerated": <int> # count of real (non-placeholder) items
"surfaces_present": <bool> # true iff enumerated > 0
The rubric (S1..S11, max 33) is unchanged — Surfaces is observability
only. Downstream gates (e.g. dotgraph reconcile, Coda's doc-maintenance
stage) decide enforcement. This matches the spec: "the goal is
observability, not enforcement."
### Backward compatibility
The `surfaces` anchor is NOT added to `SECTION_ANCHORS` (the validator's
required-anchor tuple). It lives in a new `OPTIONAL_SECTION_ANCHORS`
tuple documenting intent. Old contracts written before this PR continue
to validate, score, round-trip, and freeze — they just report
`surfaces_enumerated: 0`.
### Deviations from spec to flag
The spec mentioned a `callers` field that matches a dotgraph CLI flag.
Inspection of dotgraph/cli.py shows `dotgraph reconcile` accepts
`--paths`, `--symbols`, `--claimed-tables`, `--claimed-keys`,
`--claimed-topics`, `--base`, `--include-intra-file` — NO
`--claimed-callers`. The YAML keeps the spec's `callers_to_update:`
shape verbatim because it's consumed via the MCP `reconcile` tool's
dict input, not the CLI. Documented in the schema comments.
### Tests
- tests/project/test_contract_surfaces.py (new) — 13 tests:
· scaffold emits anchor + fenced YAML with all top-level keys
· parse_full_yaml round-trips real values
· parse returns empty schema when absent
· parse tolerates missing top-level keys (only `code` declared)
· parse tolerates malformed YAML
· parse tolerates missing `surfaces:` root
· count excludes placeholder entries (fresh scaffold == 0)
· count tallies real entries (full YAML == 10)
· count handles mixed placeholder + real
· score --json includes surfaces fields (placeholder case)
· score --json reflects real surfaces when populated
· old contract without Surfaces still validates
· content-hash changes with Surfaces edits, ignores neg-log
- tests/project/test_contract_score_json.py — updated to accept the new
surfaces_* keys in the JSON receipt schema. The pre-existing key-set
assertion now includes them.
Full suite: 761 passed, 2 skipped.
…ph.db` exists
When dotagent renders adapter files (CLAUDE.md, .cursorrules,
copilot-instructions.md, AGENTS.md) and detects `.dotgraph/graph.db`
at the project root, a deterministic "Code-graph awareness (dotgraph)"
section is injected into the rules-of-engagement segment.
### Behaviour
- `.dotgraph/graph.db` present → block emitted into every adapter.
- Absent → block omitted entirely. No empty header, no "not installed"
stub. Section simply doesn't exist.
- Filesystem check ONLY — does not shell out to `dotgraph` during
render. Renders must stay deterministic and side-effect-free.
- Check is on the database FILE, not the `.dotgraph/` directory — a
bare directory without the db is treated as absent.
### Block content
Lists the 5 dotgraph MCP tools (unprefixed; runtime adds namespace):
· context_pack(task_description) — call FIRST before grep/read
· impact(symbol, depth=2) — blast radius for symbol
· reconcile(changed_paths, claimed) — diff stated vs structural
· find_refs(needle) — content-FTS safety net
· search(query, kind) — name/signature lookup
Plus pointers to the static graph snapshots dotgraph emit-docs
maintains:
· docs/dependency-map.md
· docs/db-impact-map.md
· docs/redis-key-registry.md
· docs/kafka-topics.md
· docs/endpoints.md
Plus an instruction to populate the contract's Surfaces section honestly
using `impact()` output (closes the loop with deliverable 1).
### NOT included (spec)
- MCP server config snippet (the `command: dotgraph`, `args: [...]`
block). That's a consumer concern. Coda generates per-spawn; Cursor
users run `dotgraph mcp-config --tool cursor`.
- Tool-runtime prefixes (e.g. `mcp__dotgraph__context_pack`). Tool
names are unprefixed; runtime adds its own namespace.
### Placement
Both render paths emit the block in the rules-of-engagement segment:
- v3 manifest (default in v0.5.0+): after HARD_POLICY, before the
MUST READ + navigation manifest sections.
- v1 compendium: after `_render_rules`, before bug-registry.
Sister adapters (.cursorrules, copilot, AGENTS.md) inherit the same
body — the block lands in every one of them.
### Public surface
- `render/workflow.py::CODE_GRAPH_AWARENESS_BLOCK` — the template
constant. Locked text; modify only with intention.
- `render/workflow.py::code_graph_awareness_block(repo_root) -> str`
— helper that returns the block string when the db file exists,
empty string otherwise.
### Tests
tests/render/test_code_graph_awareness.py (13 tests):
helper directly:
- returns empty when db absent
- returns block when db present
- returns empty when dir exists but db file missing
v3 manifest renderer:
- includes block when db present (verifies all 5 tool names + 5
static doc paths appear)
- omits block when db absent
- block appears in rules-of-engagement segment (after HARD POLICY,
before "Where to find what" navigation)
- byte-diff is exactly the block (toggling db adds only the block,
removes nothing)
v1 compendium renderer:
- includes block when db present
- omits block when db absent
sister-adapter parity:
- all 4 adapters (claude/cursor/copilot/opencode) carry the block
content invariants:
- block does NOT embed an MCP server config snippet (spec)
- block references all 5 MCP tool names
- block references all 5 static doc snapshots
Full suite: 774 passed, 2 skipped.
…(0.5.3)
`dotagent doctor` reports dotgraph's freshness; `dotagent sync` runs
`dotgraph emit-docs --target all` as a pre-step when `.dotgraph/graph.db`
is present so the adapter render sees fresh graph-derived docs.
### `dotagent doctor --format json`
JSON output gained a top-level `dotgraph` envelope; the existing
`diagnoses` list moved into `payload.diagnoses` (the previous JSON
shape was a bare list — that envelope is the documented change).
{
"diagnoses": [...],
"dotgraph": {
"installed": true,
"version": "0.1.8",
"db_present": true,
"db_path": ".dotgraph/graph.db",
"dirty_files": 0,
"last_indexed": null,
"stale": false,
"error": null,
"files": 42,
"nodes": 7361,
"edges": 12000,
"unresolved": 3
}
}
Failure modes:
- dotgraph not on PATH → installed=false, every other field null.
- dotgraph crash on `status` → installed=true, db_present=true,
error=<stderr tail>, stale=true (conservative default).
- bad JSON from `status` → same as crash (error captured).
Text mode gains a one-line summary:
dotgraph: 0.1.8 · 7361 nodes · clean
dotgraph: 0.1.8 · 9 dirty file(s)
dotgraph: 0.1.8 · no graph at .dotgraph/graph.db (run: dotgraph index .)
dotgraph: not installed
Staleness heuristic (documented in `dotgraph_probe.py`):
· `dirty_files > 0` → stale
· `last_indexed` older than 24h AND any
source file mtime > last_indexed → stale
· no `last_indexed` from status JSON → stale iff dirty_files>0
· everything else → fresh
### `dotagent sync` pre-step
When `.dotgraph/graph.db` exists at the project root AND dotgraph is on
PATH, sync runs `dotgraph emit-docs --target all` BEFORE the source
reindex + adapter render. Best-effort:
- non-zero exit → warning logged, sync proceeds.
- missing binary → warning logged, sync proceeds.
- timeout (>120s) → warning logged, sync proceeds.
The adapter render then reads whatever `docs/*.md` is on disk — fresh
if emit-docs succeeded, stale otherwise.
New flag `--skip-dotgraph` opts out of the pre-step (CI envs without
dotgraph available, or runs that want to be hermetic).
### New module
`src/dotagent/dotgraph_probe.py` owns all IPC with the dotgraph CLI:
- `probe(repo_root) -> DotgraphInfo` — read-only state snapshot.
- `emit_docs(repo_root) -> (ok, msg)` — runs `dotgraph emit-docs`.
Both functions are best-effort and NEVER raise. All failure modes
land as structured fields on the returned dataclass / tuple.
Subprocess concerns isolated here for trivial PATH-shim mocking in
tests.
### Deviations from spec (resolved without help)
- `dotgraph status --json` does NOT include `last_indexed`. Real
fields: `files`, `dirty_files`, `nodes`, `edges`, `unresolved`, `db`.
We emit `last_indexed: null` and derive `stale` from `dirty_files`
when the timestamp is unavailable. Documented in the heuristic
comment.
- `dotgraph reconcile` CLI has no `--claimed-callers` flag. The
`callers_to_update:` YAML key in the Surfaces section is consumed
by the MCP `reconcile` tool's dict input, not the CLI flag set.
### Tests (+18)
tests/test_dotgraph_integration.py uses PATH-shimming to install a
fake `dotgraph` binary that emulates real surfaces. Coverage:
probe:
- dotgraph not on PATH → installed=False, fields null
- installed but no db → installed=True, db_present=False
- installed + db + clean → all fields populated, stale=False
- dirty_files > 0 → stale=True
- status crash → error captured, stale=True
- bad JSON from status → error captured
emit_docs:
- not installed → ok=False, "not on PATH"
- success → writes the requested docs/* files
- non-zero exit → ok=False, error message
doctor --format json:
- envelope has both `diagnoses` and `dotgraph`
- dotgraph fields present + correct values
- dotgraph.installed=False when binary missing
doctor text mode:
- emits one-line dotgraph summary when clean
- says "not installed" when binary missing
- shows dirty file count when stale
sync:
- runs emit-docs when db present (writes verified on disk)
- skips emit-docs when db absent
- --skip-dotgraph suppresses emit-docs even with db present
- proceeds when emit-docs fails (logs but doesn't break)
### README + CHANGELOG
Single-paragraph integration note per spec, plus a 0.5.3 CHANGELOG entry
covering all three deliverables.
Full suite: 792 passed, 2 skipped.
Version: 0.5.2 → 0.5.3.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
One-way integration with dotgraph. dotagent stays the document/process layer; dotgraph stays the code-graph tool. Three additive surfaces, all gated on the presence of
.dotgraph/graph.db. The boundary stays clean: dotagent never indexes code, never bundles dotgraph, never gates convergence on surfaces (the orchestrator does that).Three commits
d81b4e3af3880b.dotgraph/graph.dbexists67d6266What's new
1. Contract Surfaces schema
Every contract.md scaffold now carries a
## Surfacessection under a new<!-- anchor: surfaces -->HTML anchor. The fenced YAML block uses key names that match dotgraph'sreconcileflags verbatim:Backward-compatible — old contracts without the section still validate.
contract score --jsonemits newsurfaces_enumerated+surfaces_presentobservability fields (no rubric weight; downstream gates decide).2. Adapter "Code-graph awareness" block
When
.dotgraph/graph.dbexists, every rendered adapter (CLAUDE.md, .cursorrules, copilot-instructions.md, AGENTS.md) gains a rules-of-engagement section listing the 5 dotgraph MCP tools (context_pack,impact,reconcile,find_refs,search) and the 5 emitted doc snapshots. Filesystem check only — render stays deterministic. NO MCP config snippet (that's the consumer's job).3. Doctor + sync wiring
dotagent doctor --format jsongains adotgraphenvelope withinstalled,version,db_present,dirty_files,stale, etc.dotagent syncrunsdotgraph emit-docs --target allas a pre-step when conditions are met. Opt out with--skip-dotgraph. All failures fail-soft.Spec verification matrix
All 8 spec verification items + 4 bonus checks passed end-to-end:
score --jsonemits surfaces fields in populated/placeholder/stripped cases (9/0/0)--skip-dotgraphsuppresses pre-step.dotgraph/dir without graph.db produces NO blockTwo deviations resolved without help
dotgraph status --jsonreturnslast_indexedlast_indexed: null; stale derived fromdirty_files > 0alone. Documented in_compute_stale.--claimed-callersflag ondotgraph reconcilecallers_to_update:preserved; consumed via MCP tool dict input, not CLI. Documented in template.Test plan
tests/project/test_contract_surfaces.py(new) — 13 teststests/render/test_code_graph_awareness.py(new) — 13 teststests/test_dotgraph_integration.py(new) — 18 tests (PATH-shim mocked dotgraph for full lifecycle)tests/project/test_contract_score_json.py— extended to accept new key-setDocs
CHANGELOG.md— v0.5.3 entryREADME.md— single-paragraph integration note (per spec)Version
0.5.2 → 0.5.3Generated by Claude Code