feat(registry): mirror spec documents to disk via opt-in spec_mirror config - #1383
Open
ren-jentic wants to merge 2 commits into
Open
ren-jentic wants to merge 2 commits into
ren-jentic wants to merge 2 commits into
Conversation
…config When spec_mirror.enabled is set, import/promote/archive/delete rewrite the affected API's directory under spec_mirror.path so the filesystem mirrors the registry DB (one JSON doc per revision, grouped by lifecycle state, plus a meta sidecar), and a startup lifespan reconciles the whole mirror. Mirroring is post-commit, best-effort, and idempotent; the DB stays the source of truth. Defaults to off — omitting the block wires nothing. Co-authored-by: Cursor <cursoragent@cursor.com>
…verage Post-review hardening of the unshipped spec_mirror feature: - Standalone registry processes now carry the composition container, so the spec-mirror lifespan (directory validation + startup reconcile) reaches parts-mode deploys, not just the combined app - OverlayService.rollback gains the post-commit sync hook — the mirror no longer keeps serving a rolled-back overlay spec as live - Syncs verify mirrored bytes (not just file presence), so corrupted or truncated spec files heal on the next sync/reconcile; the sidecar carries mirror_digest (sha256 of the mirrored bytes) for consumers - Atomic writes use unique temp names and syncs serialize on a per-API lock, so racing syncs can't publish interleaved bytes or stale state - Startup reconcile runs as a lifespan-supervised background task and re-reads the identifier set before pruning, so a large registry never delays readiness and a mid-reconcile import is never pruned - API delete syncs from DB state (safe against delete/re-import races); failures log at error level and count on spec_mirror.failures - Segment encoding covers case-insensitive filesystems and Windows reserved names; pruning covers out-of-enum state dirs and orphaned temp files - spec_mirror.path must be absolute; single-writer constraint documented Co-authored-by: Cursor <cursoragent@cursor.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.
Summary
Spec documents live only in the registry DB, so other services that want direct access to specs (e.g. to bundle for Arazzo runs) must go through the HTTP API. This adds an opt-in
spec_mirrorconfig section: when enabled, the registry mirrors every spec document to a local directory that can be mounted rw into jentic-one and ro into consumer services. The DB stays the source of truth — mirroring is post-commit, best-effort, idempotent, and self-healing (syncs verify mirrored bytes, not just file presence). Plan:jentic-one-plans/research/spec-mirror-to-disk.md.Changes
shared/config.py: newSpecMirrorConfig(enableddefault off,path— must be absolute,reconcile_on_startup) onAppConfig; regeneratedconfig/config-schema.json,docs/reference/config.md, and the CLI vendored schema + generated structregistry/services/spec_mirror_service.py(new):sync_apiidempotently rewrites one API's directory from DB state — one JSON doc per revision grouped by lifecycle state (published/,imported/,draft/,archived/) plus a<revision_id>.meta.jsonsidecar carryingmirror_digest(sha256 of the mirrored bytes, verifiable by consumers);reconcile_all(backfill + prune, prune set re-read after syncs so a mid-reconcile import is never pruned); atomic writes via unique temp names, per-API async locks so racing syncs can't publish interleaved bytes or stale state, desired-before-prune ordering, percent-encoded path segments (incl. case-insensitive-FS and Windows reserved-name hardening), file I/O viaasyncio.to_threadImportHandler.executeafter each successful ingest (covers manual, catalog, and overlay-materialize imports),RevisionService.promote/archive/delete,OverlayService.rollback(A5b flips revision states outside the promote/archive paths — without the hook the mirror would keep serving a rolled-back overlay spec as live), andApiService.delete(a DB-driven sync, so it stays correct when a delete races a re-import) — all best-effort, never fail the registry operation; failures log at error level and count on thespec_mirror.failuresmetricwiring.py+__main__.py+registry/web/app.py:spec_mirror_lifespan(fail-fast directory validation + startup reconcile as a lifespan-supervised background task, so a large registry never delays readiness) wired for registry-surface shapes — combined and standalone/parts-mode (standalone registry now carries the composition container, same as control/auth)ApiRepository.list_identifiers,ApiRevisionRepository.list_for_api_with_spec_filesspec_mirrorsection indocs/development/context-and-config.md, incl. the single-writer constraint (write serialization is per process — exactly one registry-surface writer per mirror directory; replicas sharing a rw mount are unsupported)Out of scope: no delete-side event seam (hooks call the service directly, matching the existing best-effort post-commit posture), no periodic re-sync job (drift heals on the next mutation or startup reconcile), and no lazy content loading in the reconcile (it runs in the background off the readiness path; per-API reads stay bounded).
Risk & rollback
spec_mirror.pathis missing, relative, or unwritable (deliberate, mirrors the config validator posture)spec_mirror.failuresTest plan
make lint(ruff + mypy) andmake test-fast(unit + arch, 4062 tests) greenmake test-integration-sqlitegreen (850 passed), includingtests/integration/registry/services/test_spec_mirror.py: import lands the spec in the state dir, promote movesdraft/->published/, API delete removes the directory, overlay rollback re-syncs the live/archived split, a corrupted mirrored file heals on the next sync, reconcile backfills and prunes, disabled config is a no-optests/unit/registry/services/test_spec_mirror_helpers.py: segment encoding (incl../..traversal guard, uppercase/case-insensitive-FS collisions, Windows reserved names, round-trips), state-dir placement/moves/prune, unchanged-content skip, corrupted-file heal,mirror_digestverification, out-of-enum state-dir and orphaned-temp pruning, unknown-dir pruningtests/unit/test_app_factory.py: spec-mirror lifespan wired for registry shapes (combined + standalone regression guard), not for broker-only or disabled configstests/unit/test_config.py: defaults, YAML, env override, enabled-requires-path and absolute-path validationcd cli && GOWORK=off go build ./...green after config-struct regenMade with Cursor