feat(spec-store): bind OpenLore to an external spec store and health-check its targets - #178
Merged
Merged
Conversation
… 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>
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.
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 asPROPOSEDdocs).What you can do now
Design
targets/referencesare names resolved against.openlore/federation.json; resolution and index-state reuse the federation registry verbatim (loadRegistry/listRepos/evaluateRepoState).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.c6d1ad07). Deterministic graph/registry computation only.Surface
spec_store_status— classifiedconclusionintool-contract.ts; registered intool-dispatch,TOOL_DEFINITIONS, the livetool-driverregistry; added to the opt-infederationpreset; kept OUT ofminimal/navigation/memory.openlore spec-store status [--json].tools/listpayload ceiling consciously bumped 61k → 62k; count-guarded docs updated.Decision
c6e36101→ ADR-0021 (resolve declared targets by federation-registered name).How it was proven
--jsonfindings, no-binding — 2).srcsuite: 4298 pass / 2 skip (211 files);eslint srcclean; build clean.openspec/changes/add-spec-store-binding/DOGFOOD-spec-store-binding.md): registered this repo as an indexed target and observed realindexed/target-unresolved/reference-missingstates through the built binary, including--json.Scope
add-spec-store-binding(status: IMPLEMENTED). Spec deltas merged into the canonicalmcp-handlersandclispecs.add-working-set-context-briefing,add-change-impact-certificate) asPROPOSEDdocs — 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:
handleSpecStoreStatusthrew on a corrupt/malformed.openlore/federation.json(listRepos→loadRegistrythrows). Only the CLI caught it, so the MCP dispatch path surfacedisErrorinstead of degrading — a direct violation of the "never throws / degrade to a finding" contract. Now caught and reported as a newregistry-unreadablefinding, with no misleading per-targettarget-unresolvedcascade. Verified on both the CLI anddispatchToolroutes.process.cwd()in the self-reference check but against the bound repo in the presence check — they disagreed when the MCPdirectoryarg ≠ cwd. Unified to the bound repo.targetsandreferenceswas double-resolved with contradictory severities; now flagged once asbinding-invalid.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 indocs/mcp-tools.md;openlore spec-store statusindocs/cli-reference.md; thespecStoreblock indocs/configuration.md; a spec-store binding section indocs/federation.md. Canonicalmcp-handlersspec updated (registry-unreadablecode + corrupt-registry scenario + MCP-path clause).Verification: +6 adversarial regression tests (incl. a
dispatchToolroute test); fullsrcsuite 4304 pass / 2 skip; eslint clean; build clean. No new tool — count stays 61. Full e2e matrix inopenspec/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).
"name": 123,"path": 456, or a non-string entry intargets/referencesmade.trim()/resolution throw (config is consumed as unvalidatedJSON.parse) — the same no-throw violation via a different door. Nowtypeof-guarded: a non-stringname/pathdegrades tobinding-invalid("not a string"); non-string array entries are flagged and dropped from resolution (no numeric cascade). NewstringEntries()helper. +3 regression tests.analyzeranddriftspecs (over-inferredaffectedDomains). Removed from both; retained inconfig/mcp-handlers/cli.Full
srcsuite 4307 pass / 2 skip; eslint clean; build clean. Tool count unchanged (61). Full matrix in the dogfood note.