feat(cache): add rebuild and provenance safeguards
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
bf3f416b82
commit
5dc2f3cdf6
11 changed files with 2809 additions and 4 deletions
|
|
@ -47,7 +47,7 @@
|
|||
| task | RMGR-WP-0005-T04 | progress | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T05 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T06 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T07 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T07 | progress | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T08 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T09 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
| task | RMGR-WP-0005-T10 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||
|
|
|
|||
62
docs/cache-rebuild_v1.md
Normal file
62
docs/cache-rebuild_v1.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
id: RMGR-RUNBOOK-CACHE-0001
|
||||
type: runbook
|
||||
title: "Repository projection cache rebuild"
|
||||
version: "1"
|
||||
status: active
|
||||
created: "2026-08-21"
|
||||
updated: "2026-08-21"
|
||||
workplan_task: RMGR-WP-0005-T07
|
||||
---
|
||||
|
||||
# Repository projection cache rebuild v1
|
||||
|
||||
Repo Manager's local index is advisory. Repository files and Git history are
|
||||
authoritative; deleting `.repo-manager/index.json` cannot delete work.
|
||||
|
||||
## Inspect and rebuild
|
||||
|
||||
```bash
|
||||
rmgr cache status --path /path/to/repository
|
||||
rmgr cache rebuild --path /path/to/repository --slug repository-slug
|
||||
```
|
||||
|
||||
Status always reports `advisory`, `observed_at`, `age_seconds`, the indexed and
|
||||
current source fingerprints, and explicit staleness reasons. Legacy indexes
|
||||
without a fingerprint are stale. A source file changing after observation is
|
||||
stale. Rebuild parses the files afresh and atomically replaces only the local
|
||||
JSON projection.
|
||||
|
||||
The fingerprint covers workplans, intake/decision record locations, registers,
|
||||
classification, and the repository intent/goal file. It hashes relative paths
|
||||
and bytes so uncommitted authoritative changes are visible even when Git HEAD
|
||||
has not moved.
|
||||
|
||||
## State Hub cache replacement gate
|
||||
|
||||
Before replacing a legacy State Hub database, export the closed unbound
|
||||
workplan provenance:
|
||||
|
||||
```bash
|
||||
rmgr cache export-closed \
|
||||
--api-base http://127.0.0.1:8000 \
|
||||
--output closed-workplan-provenance.json
|
||||
```
|
||||
|
||||
The export is minimized to identity, repository, title, lifecycle, owner, and
|
||||
creation/update timestamps. It includes a canonical UTC generation timestamp
|
||||
and SHA-256 over canonical row JSON. The command refuses to overwrite an
|
||||
existing export unless `--force` is supplied.
|
||||
|
||||
A database replacement is not authorized by this command. Before replacement:
|
||||
|
||||
1. verify the export count and `rows_sha256`;
|
||||
2. prove that no live unbound file-derived records remain;
|
||||
3. prove that hub-native records have reached their one authoritative central
|
||||
owner, or retain the old database;
|
||||
4. rebuild file-derived records into an isolated database and compare counts,
|
||||
identifiers, statuses, and source provenance;
|
||||
5. switch the local cache only after the comparison passes.
|
||||
|
||||
The old database remains a recoverable source until those gates pass. Repo
|
||||
Manager does not merge database-local file-derived state back into repositories.
|
||||
2398
docs/evidence/RMGR-WP-0005-closed-provenance-2026-08-21.json
Normal file
2398
docs/evidence/RMGR-WP-0005-closed-provenance-2026-08-21.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -48,6 +48,11 @@ used for the repair because it aborts while refreshing the brief for stale
|
|||
registered path `/home/worsch/inter-hub`; the targeted binding route avoided
|
||||
coupling this data repair to that unrelated registry defect.
|
||||
|
||||
The 217 closed rows were subsequently exported to
|
||||
`docs/evidence/RMGR-WP-0005-closed-provenance-2026-08-21.json`. The minimized
|
||||
export is 90,458 bytes and seals the canonical row payload with SHA-256
|
||||
`40f227ecb138880d41cebf30a1f0559e709ab2d753be892409ea8e00f96d37d7`.
|
||||
|
||||
## Reproduction
|
||||
|
||||
The counts came from `GET /workplans/`, selecting rows whose
|
||||
|
|
|
|||
|
|
@ -62,3 +62,8 @@ records require both repository and domain context and resolve to
|
|||
types resolve to their one declared hub owner. Unknown types and writes whose
|
||||
claimed owner disagrees with the contract are rejected instead of falling back
|
||||
to whichever hub received the request.
|
||||
|
||||
Local repository projections implement the cache side through `rmgr cache
|
||||
status|rebuild`; every read carries observation age and fingerprint-based
|
||||
staleness. The guarded legacy-cache procedure and closed provenance export are
|
||||
defined in `docs/cache-rebuild_v1.md`.
|
||||
|
|
|
|||
156
src/repo_manager/cache.py
Normal file
156
src/repo_manager/cache.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""Rebuildable local-cache status and legacy provenance export."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.request import urlopen
|
||||
|
||||
from repo_manager.gitops import head_sha
|
||||
from repo_manager.index_store import default_index_path, load_index
|
||||
from repo_manager.parse.record import iter_record_files
|
||||
from repo_manager.parse.register import iter_register_files
|
||||
from repo_manager.parse.workplan import iter_workplan_files
|
||||
from repo_manager.time import utc_now, utc_now_text
|
||||
|
||||
CACHE_STATUS_SCHEMA = "repo-manager.cache-status.v1"
|
||||
CLOSED_EXPORT_SCHEMA = "repo-manager.closed-workplan-provenance.v1"
|
||||
|
||||
|
||||
def authoritative_source_files(repo_root: Path) -> list[Path]:
|
||||
repo_root = repo_root.resolve()
|
||||
paths = {
|
||||
*iter_workplan_files(repo_root),
|
||||
*iter_record_files(repo_root),
|
||||
*iter_register_files(repo_root),
|
||||
}
|
||||
for name in (".repo-classification.yaml", "INTENT.md", "GOAL.md"):
|
||||
path = repo_root / name
|
||||
if path.is_file():
|
||||
paths.add(path)
|
||||
return sorted(paths, key=lambda path: str(path.relative_to(repo_root)))
|
||||
|
||||
|
||||
def source_fingerprint(repo_root: Path) -> tuple[str, list[str]]:
|
||||
"""Hash the paths and bytes that feed the local repository projection."""
|
||||
repo_root = repo_root.resolve()
|
||||
digest = hashlib.sha256()
|
||||
relative_paths: list[str] = []
|
||||
for path in authoritative_source_files(repo_root):
|
||||
relative = str(path.relative_to(repo_root))
|
||||
relative_paths.append(relative)
|
||||
digest.update(relative.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(path.read_bytes())
|
||||
digest.update(b"\0")
|
||||
return digest.hexdigest(), relative_paths
|
||||
|
||||
|
||||
def _parse_timestamp(value: str) -> datetime:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
if parsed.tzinfo is None:
|
||||
raise ValueError("cache observed_at must be timezone-aware")
|
||||
return parsed
|
||||
|
||||
|
||||
def cache_status(repo_root: Path) -> dict[str, Any]:
|
||||
repo_root = repo_root.resolve()
|
||||
path = default_index_path(repo_root)
|
||||
checked_at = utc_now()
|
||||
if not path.is_file():
|
||||
return {
|
||||
"schema": CACHE_STATUS_SCHEMA,
|
||||
"ok": True,
|
||||
"advisory": True,
|
||||
"cache_present": False,
|
||||
"stale": True,
|
||||
"stale_reasons": ["cache_missing"],
|
||||
"checked_at": checked_at.isoformat().replace("+00:00", "Z"),
|
||||
"repo_root": str(repo_root),
|
||||
"index_path": str(path),
|
||||
}
|
||||
|
||||
index = load_index(path)
|
||||
fingerprint, source_files = source_fingerprint(repo_root)
|
||||
stale_reasons: list[str] = []
|
||||
if not index.source_fingerprint:
|
||||
stale_reasons.append("legacy_index_missing_source_fingerprint")
|
||||
elif index.source_fingerprint != fingerprint:
|
||||
stale_reasons.append("authoritative_source_changed")
|
||||
try:
|
||||
observed_at = _parse_timestamp(index.observed_at)
|
||||
age_seconds = max(0, int((checked_at - observed_at).total_seconds()))
|
||||
except (TypeError, ValueError):
|
||||
observed_at = None
|
||||
age_seconds = None
|
||||
stale_reasons.append("invalid_observed_at")
|
||||
|
||||
return {
|
||||
"schema": CACHE_STATUS_SCHEMA,
|
||||
"ok": True,
|
||||
"advisory": True,
|
||||
"cache_present": True,
|
||||
"stale": bool(stale_reasons),
|
||||
"stale_reasons": stale_reasons,
|
||||
"checked_at": checked_at.isoformat().replace("+00:00", "Z"),
|
||||
"observed_at": index.observed_at,
|
||||
"age_seconds": age_seconds,
|
||||
"repo_root": str(repo_root),
|
||||
"index_path": str(path),
|
||||
"indexed_head_sha": index.head_sha,
|
||||
"current_head_sha": head_sha(repo_root),
|
||||
"source_fingerprint": index.source_fingerprint,
|
||||
"current_source_fingerprint": fingerprint,
|
||||
"source_file_count": len(source_files),
|
||||
"record_count": len(index.work_records),
|
||||
}
|
||||
|
||||
|
||||
def build_closed_provenance_export(
|
||||
workplans: list[dict[str, Any]],
|
||||
repos: list[dict[str, Any]],
|
||||
*,
|
||||
source: str,
|
||||
) -> dict[str, Any]:
|
||||
repo_slugs = {str(repo["id"]): repo.get("slug") for repo in repos if repo.get("id")}
|
||||
rows = [
|
||||
{
|
||||
"id": row.get("id"),
|
||||
"repo_id": row.get("repo_id"),
|
||||
"repo_slug": repo_slugs.get(str(row.get("repo_id"))),
|
||||
"slug": row.get("slug"),
|
||||
"title": row.get("title"),
|
||||
"status": row.get("status"),
|
||||
"owner": row.get("owner"),
|
||||
"created_at": row.get("created_at"),
|
||||
"updated_at": row.get("updated_at"),
|
||||
}
|
||||
for row in workplans
|
||||
if row.get("status") in {"finished", "archived"} and not row.get("backing_filename")
|
||||
]
|
||||
rows.sort(key=lambda row: (str(row["status"]), str(row["repo_slug"]), str(row["slug"])))
|
||||
canonical_rows = json.dumps(rows, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
return {
|
||||
"schema": CLOSED_EXPORT_SCHEMA,
|
||||
"ok": True,
|
||||
"generated_at": utc_now_text(),
|
||||
"source": source,
|
||||
"filter": "backing_filename is null and status in [finished, archived]",
|
||||
"count": len(rows),
|
||||
"rows_sha256": hashlib.sha256(canonical_rows).hexdigest(),
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
def fetch_closed_provenance(api_base: str) -> dict[str, Any]:
|
||||
base = api_base.rstrip("/")
|
||||
with urlopen(f"{base}/workplans/", timeout=30) as response:
|
||||
workplans = json.load(response)
|
||||
with urlopen(f"{base}/repos/?limit=500", timeout=30) as response:
|
||||
repos = json.load(response)
|
||||
if not isinstance(workplans, list) or not isinstance(repos, list):
|
||||
raise TypeError("State Hub returned a non-list workplan or repository payload")
|
||||
return build_closed_provenance_export(workplans, repos, source=base)
|
||||
|
|
@ -314,6 +314,24 @@ def main(argv: list[str] | None = None) -> int:
|
|||
p_authority_route.add_argument("--domain-slug", default=None)
|
||||
p_authority_route.add_argument("--claimed-owner", default=None)
|
||||
|
||||
p_cache = sub.add_parser("cache", help="Inspect and rebuild advisory repository caches")
|
||||
cache_sub = p_cache.add_subparsers(dest="cache_command")
|
||||
p_cache_status = cache_sub.add_parser("status", help="Report cache age and source drift")
|
||||
p_cache_status.add_argument("--path", default=".")
|
||||
p_cache_rebuild = cache_sub.add_parser("rebuild", help="Rebuild the local index from files")
|
||||
p_cache_rebuild.add_argument("--path", default=".")
|
||||
p_cache_rebuild.add_argument("--slug", default=None)
|
||||
p_cache_export = cache_sub.add_parser(
|
||||
"export-closed",
|
||||
help="Export unbound closed State Hub records before cache replacement",
|
||||
)
|
||||
p_cache_export.add_argument(
|
||||
"--api-base",
|
||||
default=os.environ.get("STATE_HUB_API_BASE", "http://127.0.0.1:8000"),
|
||||
)
|
||||
p_cache_export.add_argument("--output", required=True)
|
||||
p_cache_export.add_argument("--force", action="store_true")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.version or args.command in (None, "version"):
|
||||
|
|
@ -687,6 +705,49 @@ def main(argv: list[str] | None = None) -> int:
|
|||
print(json.dumps({"ok": True, **result}, indent=2))
|
||||
return 0
|
||||
|
||||
if args.command == "cache":
|
||||
if not args.cache_command:
|
||||
p_cache.print_help()
|
||||
return 2
|
||||
from repo_manager.cache import cache_status, fetch_closed_provenance
|
||||
|
||||
if args.cache_command == "status":
|
||||
result = cache_status(Path(args.path))
|
||||
elif args.cache_command == "rebuild":
|
||||
from repo_manager.index_store import save_index
|
||||
from repo_manager.observe import observe_repository
|
||||
|
||||
snapshot, index = observe_repository(Path(args.path), slug=args.slug)
|
||||
index_path = save_index(index)
|
||||
result = {
|
||||
"ok": True,
|
||||
"rebuilt": True,
|
||||
"index_path": str(index_path),
|
||||
"record_count": snapshot["index"]["record_count"],
|
||||
"cache": cache_status(Path(args.path)),
|
||||
}
|
||||
else:
|
||||
output = Path(args.output)
|
||||
if output.exists() and not args.force:
|
||||
print(
|
||||
json.dumps(
|
||||
{"ok": False, "error": f"output exists: {output}; use --force to replace"},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 1
|
||||
try:
|
||||
result = fetch_closed_provenance(args.api_base)
|
||||
except (OSError, TypeError, ValueError) as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
||||
result = {key: value for key, value in result.items() if key != "rows"}
|
||||
result["output"] = str(output)
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result.get("ok") else 1
|
||||
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -32,9 +34,11 @@ class RepoIndex:
|
|||
repo_root: str
|
||||
head_sha: str | None
|
||||
observed_at: str
|
||||
source_fingerprint: str | None = None
|
||||
source_files: list[str] = field(default_factory=list)
|
||||
work_records: list[WorkRecordEntry] = field(default_factory=list)
|
||||
events: list[dict[str, Any]] = field(default_factory=list)
|
||||
schema: str = "repo_manager.index.v0"
|
||||
schema: str = "repo_manager.index.v1"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
|
|
@ -43,6 +47,8 @@ class RepoIndex:
|
|||
"repo_root": self.repo_root,
|
||||
"head_sha": self.head_sha,
|
||||
"observed_at": self.observed_at,
|
||||
"source_fingerprint": self.source_fingerprint,
|
||||
"source_files": self.source_files,
|
||||
"work_records": [asdict(r) for r in self.work_records],
|
||||
"events": self.events,
|
||||
}
|
||||
|
|
@ -58,6 +64,8 @@ class RepoIndex:
|
|||
repo_root=data["repo_root"],
|
||||
head_sha=data.get("head_sha"),
|
||||
observed_at=data.get("observed_at") or _now(),
|
||||
source_fingerprint=data.get("source_fingerprint"),
|
||||
source_files=list(data.get("source_files") or []),
|
||||
work_records=records,
|
||||
events=list(data.get("events") or []),
|
||||
schema=data.get("schema") or "repo_manager.index.v0",
|
||||
|
|
@ -71,7 +79,24 @@ def default_index_path(repo_root: Path) -> Path:
|
|||
def save_index(index: RepoIndex, path: Path | None = None) -> Path:
|
||||
path = path or default_index_path(Path(index.repo_root))
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(index.to_dict(), indent=2) + "\n", encoding="utf-8")
|
||||
temporary: Path | None = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
delete=False,
|
||||
) as handle:
|
||||
temporary = Path(handle.name)
|
||||
handle.write(json.dumps(index.to_dict(), indent=2) + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, path)
|
||||
temporary = None
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||
|
||||
import yaml
|
||||
|
||||
from repo_manager.cache import source_fingerprint
|
||||
from repo_manager.classification import require_valid_classification
|
||||
from repo_manager.gitops import head_sha, is_git_repo
|
||||
from repo_manager.index_store import RepoIndex, WorkRecordEntry, _now
|
||||
|
|
@ -117,11 +118,14 @@ def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dic
|
|||
)
|
||||
|
||||
sha = head_sha(repo_root) if is_git_repo(repo_root) else None
|
||||
fingerprint, source_files = source_fingerprint(repo_root)
|
||||
index = RepoIndex(
|
||||
slug=slug,
|
||||
repo_root=str(repo_root),
|
||||
head_sha=sha,
|
||||
observed_at=_now(),
|
||||
source_fingerprint=fingerprint,
|
||||
source_files=source_files,
|
||||
work_records=records,
|
||||
)
|
||||
|
||||
|
|
|
|||
71
tests/test_cache.py
Normal file
71
tests/test_cache.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from repo_manager.cache import build_closed_provenance_export, cache_status
|
||||
from repo_manager.index_store import save_index
|
||||
from repo_manager.observe import observe_repository
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> None:
|
||||
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
|
||||
|
||||
|
||||
def _repo(tmp_path: Path) -> Path:
|
||||
repo = tmp_path / "example"
|
||||
(repo / "workplans").mkdir(parents=True)
|
||||
_git(repo, "init")
|
||||
_git(repo, "config", "user.email", "test@example.com")
|
||||
_git(repo, "config", "user.name", "Test")
|
||||
(repo / "workplans" / "EX-WP-0001.md").write_text(
|
||||
"---\nid: EX-WP-0001\ntitle: Example\nstatus: active\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
_git(repo, "add", ".")
|
||||
_git(repo, "commit", "-m", "seed")
|
||||
return repo
|
||||
|
||||
|
||||
def test_cache_status_reports_age_and_detects_source_drift(tmp_path: Path) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
missing = cache_status(repo)
|
||||
assert missing["cache_present"] is False
|
||||
assert missing["stale_reasons"] == ["cache_missing"]
|
||||
|
||||
_snapshot, index = observe_repository(repo)
|
||||
save_index(index)
|
||||
fresh = cache_status(repo)
|
||||
assert fresh["advisory"] is True
|
||||
assert fresh["stale"] is False
|
||||
assert fresh["age_seconds"] >= 0
|
||||
|
||||
path = repo / "workplans" / "EX-WP-0001.md"
|
||||
path.write_text(path.read_text(encoding="utf-8") + "\nChanged.\n", encoding="utf-8")
|
||||
stale = cache_status(repo)
|
||||
assert stale["stale"] is True
|
||||
assert stale["stale_reasons"] == ["authoritative_source_changed"]
|
||||
|
||||
|
||||
def test_closed_export_excludes_live_and_bound_rows() -> None:
|
||||
workplans = [
|
||||
{"id": "1", "repo_id": "r1", "slug": "closed", "status": "finished"},
|
||||
{"id": "2", "repo_id": "r1", "slug": "live", "status": "active"},
|
||||
{
|
||||
"id": "3",
|
||||
"repo_id": "r1",
|
||||
"slug": "bound",
|
||||
"status": "archived",
|
||||
"backing_filename": "bound.md",
|
||||
},
|
||||
]
|
||||
result = build_closed_provenance_export(
|
||||
workplans,
|
||||
[{"id": "r1", "slug": "example"}],
|
||||
source="http://state-hub.test",
|
||||
)
|
||||
|
||||
assert result["count"] == 1
|
||||
assert result["rows"][0]["slug"] == "closed"
|
||||
assert result["rows"][0]["repo_slug"] == "example"
|
||||
assert len(result["rows_sha256"]) == 64
|
||||
|
|
@ -378,7 +378,7 @@ byte-identical writeback, and neither creates a duplicate record.
|
|||
|
||||
```task
|
||||
id: RMGR-WP-0005-T07
|
||||
status: wait
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "70f83359-0b61-4dc0-83b0-33f289b64e83"
|
||||
```
|
||||
|
|
@ -396,6 +396,24 @@ Measured 2026-08-17: 955 workplans locally against 649 on the primary, 320
|
|||
local-only, of which **288 are backed by files that all exist on disk**. That
|
||||
portion of the divergence is redundant and needs no merge — only a rebuild.
|
||||
|
||||
**Progress (2026-08-21):** `rmgr cache status|rebuild` now makes the local
|
||||
repository projection explicitly advisory and reports canonical observation
|
||||
time, age, source fingerprint, current/indexed Git revision, and concrete stale
|
||||
reasons. The fingerprint covers the authoritative record/classification/intent
|
||||
bytes, including uncommitted changes. A live drill correctly rejected the
|
||||
legacy index as stale, rebuilt 73 records from repository files, and immediately
|
||||
reported a fresh cache.
|
||||
|
||||
The 217 closed unbound rows are preserved in a minimized, integrity-sealed
|
||||
export at
|
||||
`docs/evidence/RMGR-WP-0005-closed-provenance-2026-08-21.json`; live unbound
|
||||
rows remain zero. `docs/cache-rebuild_v1.md` gates any database replacement on
|
||||
an isolated projection rebuild and proof that hub-native records reached their
|
||||
one central owner. Remaining before `done`: compare against an isolated rebuilt
|
||||
State Hub database and verify hub-native centralization. The documented remote
|
||||
tunnel at `127.0.0.1:18000` was not running during this pass, so no central
|
||||
comparison was inferred.
|
||||
|
||||
## Separate file-derived from hub-native data
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue