Skip to content

feat(spec-store): bind OpenLore to an external spec store and health-check its targets - #178

Merged
clay-good merged 3 commits into
mainfrom
feat/spec-store-binding
Jun 21, 2026
Merged

clay-good merged 3 commits into
mainfrom
feat/spec-store-binding

Conversation

@clay-good

@clay-good clay-good commented Jun 21, 2026

Copy link
Copy Markdown
Owner

What this is

OpenLore can now bind to an external spec store — a standalone repository that holds specs/changes and declares the code repositories its plans target and reference. The binding is read against the existing multi-repo federation index-of-indexes, so OpenLore can tell you, deterministically, whether that binding is healthy: every declared target resolvable, indexed, and fresh; every reference present.

This is the foundation of the spec-store integration arc (openspec/changes/SPEC-STORE-INTEGRATION.md): once OpenLore knows the plan↔code mapping, it can assemble working-set context and certify change impact across the targets (the two follow-on proposals, shipped here as PROPOSED docs).

What you can do now

# register the code repos a plan is about (reuses federation)
openlore federation add ../api --name api
openlore federation add ../web --name web

# bind via .openlore/config.json:
#   "specStore": { "name": "team-plans", "path": "../team-plans",
#                  "targets": ["api","web"], "references": ["design-system"] }

openlore spec-store status            # human-readable health
openlore spec-store status --json     # stable finding codes for an orchestrator

Design

  • A thin declarative layer, no new machinery. targets/references are names resolved against .openlore/federation.json; resolution and index-state reuse the federation registry verbatim (loadRegistry/listRepos/evaluateRepoState).
  • Read-only, conclusion-shaped, never blocks. One report: per-target resolution + index freshness, per-reference presence, store-path presence, malformed-binding checks. Findings carry stable codes — no-binding, binding-invalid, store-path-missing, target-unresolved, target-missing, index-missing, index-stale, reference-missing — each with a pasteable remediation. The handler never throws; the CLI always exits 0.
  • No LLM (north star c6d1ad07). Deterministic graph/registry computation only.

Surface

  • MCP tool spec_store_status — classified conclusion in tool-contract.ts; registered in tool-dispatch, TOOL_DEFINITIONS, the live tool-driver registry; added to the opt-in federation preset; kept OUT of minimal/navigation/memory.
  • CLI openlore spec-store status [--json].
  • Full surface 60 → 61; tools/list payload ceiling consciously bumped 61k → 62k; count-guarded docs updated.

Decision

c6e36101ADR-0021 (resolve declared targets by federation-registered name).

How it was proven

  • New tests: handler (validation, resolution, every finding state, conclusion-shape — 13) + CLI (--json findings, no-binding — 2).
  • Full src suite: 4298 pass / 2 skip (211 files); eslint src clean; build clean.
  • E2E dogfood (openspec/changes/add-spec-store-binding/DOGFOOD-spec-store-binding.md): registered this repo as an indexed target and observed real indexed / target-unresolved / reference-missing states through the built binary, including --json.

Scope

  • Implements add-spec-store-binding (status: IMPLEMENTED). Spec deltas merged into the canonical mcp-handlers and cli specs.
  • Includes the umbrella overview and the two follow-on proposals (add-working-set-context-briefing, add-change-impact-certificate) as PROPOSED docs — not implemented here.

🤖 Generated with Claude Code


Update — adversarial hardening (follow-up commit d0bd242)

Two independent adversarial code reviews + a hostile-input e2e battery against the built binary found and fixed three real defects, with regression tests:

  • P1 (contract violation, fixed): handleSpecStoreStatus threw on a corrupt/malformed .openlore/federation.json (listReposloadRegistry throws). Only the CLI caught it, so the MCP dispatch path surfaced isError instead of degrading — a direct violation of the "never throws / degrade to a finding" contract. Now caught and reported as a new registry-unreadable finding, with no misleading per-target target-unresolved cascade. Verified on both the CLI and dispatchTool routes.
  • P2a (fixed): a relative store path was canonicalized against process.cwd() in the self-reference check but against the bound repo in the presence check — they disagreed when the MCP directory arg ≠ cwd. Unified to the bound repo.
  • P2b (fixed): a name declared in both targets and references was double-resolved with contradictory severities; now flagged once as binding-invalid.
  • P3 (fixed): the report echoed raw untrimmed store.name/store.path; now echoes the trimmed values it validated.

Documentation brought to parity with federation_status: spec_store_status + the full finding-code table in docs/mcp-tools.md; openlore spec-store status in docs/cli-reference.md; the specStore block in docs/configuration.md; a spec-store binding section in docs/federation.md. Canonical mcp-handlers spec updated (registry-unreadable code + corrupt-registry scenario + MCP-path clause).

Verification: +6 adversarial regression tests (incl. a dispatchTool route test); full src suite 4304 pass / 2 skip; eslint clean; build clean. No new tool — count stays 61. Full e2e matrix in openspec/changes/add-spec-store-binding/DOGFOOD-spec-store-binding.md.

Update — second adversarial pass (commit c22a669)

A deeper sweep found one more throw vector and a spec-categorization defect; both fixed. An independent breadth review then confirmed no other throw/block/miscount path remains (NUL bytes, symlink loops, huge paths, non-string/object inputs all verified safe).

  • Wrong-typed config (fixed): "name": 123, "path": 456, or a non-string entry in targets/references made .trim()/resolution throw (config is consumed as unvalidated JSON.parse) — the same no-throw violation via a different door. Now typeof-guarded: a non-string name/path degrades to binding-invalid ("not a string"); non-string array entries are flagged and dropped from resolution (no numeric cascade). New stringEntries() helper. +3 regression tests.
  • Spec pollution (fixed): the decision-sync had appended this binding's requirement to the unrelated analyzer and drift specs (over-inferred affectedDomains). Removed from both; retained in config/mcp-handlers/cli.

Full src suite 4307 pass / 2 skip; eslint clean; build clean. Tool count unchanged (61). Full matrix in the dogfood note.

sim and others added 3 commits June 21, 2026 14:50
… targets

Adds an optional OpenLoreConfig.specStore binding { name, path, targets[],
references? } whose declared target/reference names resolve by name against the
multi-repo federation registry. A new read-only, conclusion-shaped
spec_store_status MCP tool and `openlore spec-store status [--json]` CLI report
binding health with stable finding codes (target-unresolved, index-stale,
reference-missing, store-path-missing, index-missing, target-missing,
binding-invalid, no-binding) and pasteable remediations. It never throws and
never blocks — a thin declarative layer over the shipped index-of-indexes, no new
index machinery, no LLM (north star c6d1ad07).

First of the spec-store integration arc (add-spec-store-binding); the two
follow-on proposals ship as PROPOSED docs under openspec/changes.

Decision c6e36101 (ADR-0021). Full surface 60->61; tools/list payload ceiling
61k->62k. New tests: handler (13) + CLI (2); full src suite 4298 pass / 2 skip;
eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…istry, fix path base, doc parity

Adversarial review + hostile-input e2e found three real defects in the spec-store
binding (PR #178), all fixed with regression tests:

- P1 (contract violation): handleSpecStoreStatus threw on a corrupt/malformed
  .openlore/federation.json because listRepos -> loadRegistry throws; only the CLI
  caught it, so the MCP dispatch path surfaced isError instead of degrading. Now
  caught and reported as a new `registry-unreadable` finding, with no misleading
  per-target target-unresolved cascade. Verified on both CLI and dispatchTool paths.
- P2a: a relative store path was canonicalized against process.cwd() in the
  self-reference check but against the bound repo in the presence check; they
  disagreed when the MCP `directory` arg != cwd. Unified to the bound repo.
- P2b: a name declared in both targets and references was double-resolved with
  contradictory severities; now flagged once as binding-invalid.
- P3: the report echoed raw untrimmed store name/path; now echoes the trimmed
  values it actually validated.

Docs brought to parity with federation_status: spec_store_status + the full
finding-code table in docs/mcp-tools.md; `openlore spec-store status` in
docs/cli-reference.md; the specStore block in docs/configuration.md; a spec-store
binding section in docs/federation.md. Canonical mcp-handlers spec updated
(registry-unreadable code + corrupt-registry scenario + MCP-path clause).

Tests: +6 adversarial regressions incl. a dispatchTool route test. Full src suite
4304 pass / 2 skip; eslint clean; build clean. No new tool (count stays 61).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n-pollute analyzer/drift specs

Second adversarial pass on PR #178.

- Throw vector (same no-throw contract, different door): a wrong-typed config
  field — "name": 123, "path": 456, or a non-string entry in targets/references —
  made `.trim()`/resolution throw, because .openlore/config.json is consumed as
  unvalidated JSON.parse. Fixed with typeof-guarded coercion: a non-string
  name/path degrades to binding-invalid ("not a string"); non-string array entries
  are flagged binding-invalid and dropped from resolution (no numeric cascade).
  New `stringEntries()` helper centralizes the filtering. +3 regression tests.
- Spec-categorization defect: the decision sync had appended this binding's
  requirement to the unrelated analyzer and drift specs (over-inferred
  affectedDomains). Removed from both; retained in config/mcp-handlers/cli.

An independent breadth review confirmed no other throw/block/miscount path remains
(NUL bytes, symlink loops, huge paths, non-string/object inputs all verified safe).

Full src suite 4307 pass / 2 skip; eslint clean; build clean. No new tool (count 61).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good closed this Jun 21, 2026
@clay-good clay-good reopened this Jun 21, 2026
@clay-good
clay-good merged commit 9d4956e into main Jun 21, 2026
8 checks passed
@clay-good
clay-good deleted the feat/spec-store-binding branch June 22, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant