fix(consistency): preserve immutable historical binding rows
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
parent
086d79a273
commit
a25b62f75a
10 changed files with 586 additions and 42 deletions
2
Makefile
2
Makefile
|
|
@ -9,7 +9,7 @@ RAILIANCE_STATE_HUB_RELEASE ?= state-hub
|
|||
RAILIANCE_STATE_HUB_NAMESPACE ?= state-hub
|
||||
RAILIANCE_STATE_HUB_CHART ?= deploy/railiance/apps/charts/state-hub
|
||||
RAILIANCE_STATE_HUB_VALUES ?= deploy/railiance/apps/helm/state-hub-values.yaml
|
||||
RAILIANCE_STATE_HUB_IMAGE_TAG ?= main-0747b4d
|
||||
RAILIANCE_STATE_HUB_IMAGE_TAG ?= main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015
|
||||
RAILIANCE_STATE_HUB_PLATFORM_DIR ?= deploy/railiance/platform
|
||||
RAILIANCE_STATE_HUB_APP_MANIFESTS ?= deploy/railiance/apps/manifests
|
||||
# Codex/WSL non-login shells may not source ~/.profile; keep uv discoverable.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ from api.database import get_session
|
|||
from api.events import EventEnvelope, publish_event
|
||||
from api.models.managed_repo import ManagedRepo
|
||||
from api.models.workplan import Workplan
|
||||
from api.models.task import Task
|
||||
from api.services.historical_projection import frozen_cluster_binding, validate_frozen_cluster_rows
|
||||
from api.schemas.workplan import (
|
||||
WorkplanBindingsSync,
|
||||
WorkplanCreate,
|
||||
|
|
@ -435,12 +437,42 @@ async def sync_workplan_bindings(
|
|||
session: AsyncSession = Depends(get_session),
|
||||
) -> dict[str, int]:
|
||||
"""Upsert workstation workplan file bindings for remote API index fallback."""
|
||||
# Preflight the complete batch before assigning even an ordinary row.
|
||||
try:
|
||||
frozen = {entry.workplan_id: bound for entry in body.bindings
|
||||
if (bound := frozen_cluster_binding(entry.model_dump())) is not None}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
requested_ids = {entry.workplan_id for entry in body.bindings}
|
||||
rows = await session.execute(
|
||||
select(Workplan).where(Workplan.id.in_(requested_ids))
|
||||
.order_by(Workplan.id).with_for_update().execution_options(populate_existing=True)
|
||||
)
|
||||
workplans = {workplan.id: workplan for workplan in rows.scalars().all()}
|
||||
if frozen:
|
||||
repo_rows = await session.execute(
|
||||
select(ManagedRepo).where(ManagedRepo.slug == "railiance-cluster")
|
||||
)
|
||||
repo = repo_rows.scalar_one_or_none()
|
||||
task_rows = await session.execute(
|
||||
select(Task).where(Task.workplan_id.in_(frozen))
|
||||
.order_by(Task.id).with_for_update().execution_options(populate_existing=True)
|
||||
)
|
||||
tasks = task_rows.scalars().all()
|
||||
try:
|
||||
for workplan_id, bound in frozen.items():
|
||||
validate_frozen_cluster_rows(
|
||||
bound, workplans.get(workplan_id), tasks, repo.id if repo else None
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
synced_at = datetime.now(timezone.utc)
|
||||
updated = 0
|
||||
requested_ids = {entry.workplan_id for entry in body.bindings}
|
||||
rows = await session.execute(select(Workplan).where(Workplan.id.in_(requested_ids)))
|
||||
workplans = {workplan.id: workplan for workplan in rows.scalars().all()}
|
||||
preserved = 0
|
||||
for entry in body.bindings:
|
||||
if entry.workplan_id in frozen:
|
||||
preserved += 1
|
||||
continue # No setters, including backing_synced_at/updated_at.
|
||||
wp = workplans.get(entry.workplan_id)
|
||||
if wp is None:
|
||||
continue
|
||||
|
|
@ -449,9 +481,10 @@ async def sync_workplan_bindings(
|
|||
wp.backing_archived = entry.archived
|
||||
wp.backing_synced_at = synced_at
|
||||
updated += 1
|
||||
await session.commit()
|
||||
_invalidate_workplan_index_cache()
|
||||
return {"updated": updated, "received": len(body.bindings)}
|
||||
if updated:
|
||||
await session.commit()
|
||||
_invalidate_workplan_index_cache()
|
||||
return {"updated": updated, "received": len(body.bindings), "preserved": preserved}
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_410_GONE)
|
||||
|
|
|
|||
|
|
@ -193,3 +193,46 @@ def bound_sources(repo_root: Path, repo_slug: str):
|
|||
raise ValueError("Historical source binding changed: " + entry["path"])
|
||||
result.append((entry, data.decode("utf-8")))
|
||||
return result
|
||||
|
||||
|
||||
# Only the three existing full-task Cluster bindings have immutable DB rows.
|
||||
# Other historical views retain their existing compatibility behavior.
|
||||
FROZEN_CLUSTER_BINDINGS = tuple(
|
||||
entry for entry in BINDINGS
|
||||
if entry["repo"] == "railiance-cluster" and "tasks" in entry
|
||||
)
|
||||
|
||||
|
||||
def frozen_cluster_binding(binding):
|
||||
"""Validate a claimed frozen binding; unrelated requests remain ordinary."""
|
||||
matches = [entry for entry in FROZEN_CLUSTER_BINDINGS
|
||||
if str(binding["workplan_id"]) == entry["uuid"]
|
||||
or (binding["repo_slug"] == "railiance-cluster"
|
||||
and (binding["relative_path"] == entry["path"]
|
||||
or binding["filename"] == Path(entry["path"]).name))]
|
||||
if not matches:
|
||||
return None
|
||||
if len(matches) != 1:
|
||||
raise ValueError("frozen historical binding is ambiguous")
|
||||
entry = matches[0]
|
||||
expected = {
|
||||
"workplan_id": entry["uuid"], "repo_slug": entry["repo"],
|
||||
"filename": Path(entry["path"]).name, "relative_path": entry["path"],
|
||||
"archived": True, "status": "finished",
|
||||
}
|
||||
if any((str(binding[key]) if key == "workplan_id" else binding[key]) != value
|
||||
for key, value in expected.items()):
|
||||
raise ValueError("frozen historical binding metadata changed")
|
||||
return entry
|
||||
|
||||
|
||||
def validate_frozen_cluster_rows(entry, parent, tasks, repo_id):
|
||||
"""Receiving requires the exact existing terminal closure, never repair."""
|
||||
if (parent is None or str(parent.id) != entry["uuid"]
|
||||
or repo_id is None or parent.repo_id != repo_id
|
||||
or parent.status not in {"done", "finished", "archived", "completed"}):
|
||||
raise ValueError("frozen historical parent missing, foreign or nonterminal")
|
||||
local = {str(task.id): task for task in tasks if task.workplan_id == parent.id}
|
||||
if any(task["uuid"] not in local or local[task["uuid"]].status not in {"done", "cancel"}
|
||||
for task in entry["tasks"]):
|
||||
raise ValueError("frozen historical task missing, foreign or nonterminal")
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ name: state-hub
|
|||
description: State Hub API service for private Railiance operation
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "main-5dd3df9"
|
||||
appVersion: "main-51bc70d"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
# to match the standard version. CUST-WP-0050 T01.
|
||||
|
||||
standard: "Repo Classification Standard"
|
||||
version: "1.0"
|
||||
version: "1.1"
|
||||
canon_id: "canon-repo-classification"
|
||||
|
||||
# category — exactly 1 required (§5)
|
||||
|
|
@ -24,6 +24,7 @@ categories:
|
|||
- tooling
|
||||
- product
|
||||
- business
|
||||
- publication
|
||||
|
||||
# domain / secondary_domains — primary exactly 1; secondaries 0..n (§6)
|
||||
domains:
|
||||
|
|
@ -85,6 +86,8 @@ capability_families:
|
|||
- traceability
|
||||
- documentation
|
||||
- decision-support
|
||||
- history
|
||||
- participation
|
||||
platform_and_operations:
|
||||
- platform
|
||||
- deployment
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace:
|
|||
|
||||
image:
|
||||
repository: forgejo.coulomb.social/coulomb/state-hub
|
||||
tag: "main-0747b4d"
|
||||
tag: "main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"schema": "statehub.cluster-historical-receiving-delivered.v1",
|
||||
"status": "verified",
|
||||
"proposal_id": "STATE-WP-0095-T06-cluster-frozen-receiving-r1",
|
||||
"approved_revision": 4,
|
||||
"approved_digest_sha256": "sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32",
|
||||
"approval_decision_id": "e3798419-8aa9-5425-8c08-20df6bd82a19",
|
||||
"source_revision": "51bc70d1ff8f25450a8650d8e50caf053093a02e",
|
||||
"readiness_source_revision": "086d79a2736e34b916bd7de3c8a65e2ea38a3d64",
|
||||
"helm_revision": 76,
|
||||
"image_reference": "forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015",
|
||||
"expected_schema": "c4d5e6f7a8b9",
|
||||
"health": {
|
||||
"status": "ok",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"schema": {
|
||||
"status": "ok",
|
||||
"applied": "c4d5e6f7a8b9",
|
||||
"expected": "c4d5e6f7a8b9"
|
||||
}
|
||||
},
|
||||
"pods_ready": true,
|
||||
"historical_module_sha256": "sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2",
|
||||
"historical_preservation_verified": true,
|
||||
"parents": 3,
|
||||
"tasks": 15,
|
||||
"full_historical_rows_equal": true,
|
||||
"native_rollback_probes": {
|
||||
"foreign-holder": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"missing-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-parent": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"nonterminal-task": {
|
||||
"full_historical_rows_restored": true,
|
||||
"historical_binding_refused": true,
|
||||
"native_outcome_status": "refused",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
},
|
||||
"preserve": {
|
||||
"full_historical_rows_equal": true,
|
||||
"full_historical_rows_restored": true,
|
||||
"native_outcome_status": "applied",
|
||||
"rolled_back": true,
|
||||
"status": "passed"
|
||||
}
|
||||
},
|
||||
"native_probe_commits": 0,
|
||||
"fresh_transaction_full_rows_equal": true,
|
||||
"source_receiving_gate_delivered": true,
|
||||
"normal_cluster_publication_verified": false,
|
||||
"registrar_admission_or_cluster_publication_not_inferred": true,
|
||||
"authority_limits": [
|
||||
"Rollback probe SQL effects rolled back; foreign-holder case is historical-binding refusal, not separate global holder branch.",
|
||||
"Actual historical native receiving is verified; owner-native TLS/private admission remains unresolved.",
|
||||
"Migration schema remains additive; Helm rollback does not downgrade schema or stop an active hook Job."
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/approval-decision.json",
|
||||
"sha256": "sha256:b51554c004daa882f3709f67bdae7f2e939d611120383e61be04ec6969878134"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/helm-return.json",
|
||||
"sha256": "sha256:61727deacf22d37da2d28cbaf04f5a3ddee0f9ff7571c77f1e85dba97963b572"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/delivered-runtime.json",
|
||||
"sha256": "sha256:89681066e8fe139456163ebfafa3efe77ffae8534ea167219230b0a74367f129"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/native-refusal-probes.json",
|
||||
"sha256": "sha256:673ccef965c820704dc310a1bf7832b583c30c3edfeb814528cf892555199172"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/before-full-row-hashes.json",
|
||||
"sha256": "sha256:75cdd87eef79059ff3f484ed24ca69e285899cdf8d851cc4e472653c417eb0ee"
|
||||
},
|
||||
{
|
||||
"path": "docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/after-full-row-hashes.json",
|
||||
"sha256": "sha256:2156aad18f190384838c27e792706a156a7423391408afbc6107e6c28c5872d8"
|
||||
}
|
||||
],
|
||||
"later_ordinary_cluster_publication": {
|
||||
"status": "failed_full_parent_row_preservation",
|
||||
"changed_parent_hashes": 3,
|
||||
"unchanged_task_hashes": 15,
|
||||
"exact_changed_columns": "unknown_hash_only_baseline",
|
||||
"original_parent_preimages": "unavailable_no_guessed_restoration",
|
||||
"receipt": "the-custodian/docs/changes/netkingdom-target-2026-10-17/receiver-rollout-r4/post-publication-full-row-hashes-FAILED.json",
|
||||
"T07": "wait",
|
||||
"RMGR_WP_0019_T06": "wait",
|
||||
"followup": [
|
||||
"STATE-WP-0095-T09",
|
||||
"STATE-WP-0095-T10"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +120,9 @@ from api.classification import ( # noqa: E402
|
|||
resolve_topic_domain_slug,
|
||||
)
|
||||
from api.services.lifecycle import should_activate_parent_for_active_tasks # noqa: E402
|
||||
from api.services.historical_projection import BINDINGS as _HISTORICAL_BINDINGS # noqa: E402
|
||||
from api.services.historical_projection import ( # noqa: E402
|
||||
BINDINGS as _HISTORICAL_BINDINGS, FROZEN_CLUSTER_BINDINGS, frozen_cluster_binding,
|
||||
)
|
||||
from api.task_status import ( # noqa: E402
|
||||
CANONICAL_TASK_STATUSES,
|
||||
OPEN_TASK_STATUSES,
|
||||
|
|
@ -318,7 +320,7 @@ def iter_workplan_files(workplans_dir: Path, include_archived: bool = True) -> l
|
|||
|
||||
# C-00 marks infrastructure/automation faults (API down, repo missing in DB).
|
||||
# All other FAIL severities are assessment findings for follow-up.
|
||||
_AUTOMATION_ERROR_CHECKS: frozenset[str] = frozenset({"C-00"})
|
||||
_AUTOMATION_ERROR_CHECKS: frozenset[str] = frozenset({"C-00", "C-44"})
|
||||
_NON_WORKPLAN_WORKPLAN_FILES: frozenset[str] = frozenset({"README.md"})
|
||||
|
||||
|
||||
|
|
@ -2484,6 +2486,8 @@ def check_repo(
|
|||
_check_workplan_id_collisions(api_base, repo_slug, repo_dir, workplan_infos, report)
|
||||
_check_scope_freshness(repo_dir, workplan_infos, report)
|
||||
|
||||
if _refuse_frozen_fix_batch(report, repo_slug):
|
||||
return report
|
||||
_sync_workplan_bindings(api_base, repo_slug, workplan_infos, repo_dir, report)
|
||||
|
||||
return report
|
||||
|
|
@ -2496,23 +2500,44 @@ def _sync_workplan_bindings(
|
|||
repo_dir: Path,
|
||||
report: ConsistencyReport,
|
||||
) -> None:
|
||||
if repo_slug == "railiance-cluster":
|
||||
scanned_paths = {workplan_display_path(repo_dir, path) for path, _, _ in workplan_infos}
|
||||
missing = [entry["path"] for entry in FROZEN_CLUSTER_BINDINGS
|
||||
if entry["path"] not in scanned_paths]
|
||||
if missing:
|
||||
report.add(severity="FAIL", check_id="C-44",
|
||||
message="Frozen binding batch refused: bound source missing from scan",
|
||||
fixable=False)
|
||||
return
|
||||
bindings: list[dict[str, Any]] = []
|
||||
for wp_file, meta, _ in workplan_infos:
|
||||
ws_id = str(meta.get("state_hub_workstream_id", "")).strip().strip('"')
|
||||
if not ws_id:
|
||||
continue
|
||||
archived = wp_file.parent.name == "archived"
|
||||
file_status = normalise_workstream_status(str(meta.get("status", "")).strip())
|
||||
bindings.append(
|
||||
{
|
||||
"workplan_id": ws_id,
|
||||
"filename": wp_file.name,
|
||||
"relative_path": workplan_display_path(repo_dir, wp_file),
|
||||
"repo_slug": repo_slug,
|
||||
"archived": archived,
|
||||
"status": file_status or None,
|
||||
}
|
||||
)
|
||||
binding = {
|
||||
"workplan_id": ws_id, "filename": wp_file.name,
|
||||
"relative_path": workplan_display_path(repo_dir, wp_file),
|
||||
"repo_slug": repo_slug, "archived": archived,
|
||||
"status": file_status or None,
|
||||
}
|
||||
try:
|
||||
frozen = frozen_cluster_binding(binding)
|
||||
if frozen is not None:
|
||||
# Verify exact source before omitting the immutable binding.
|
||||
# This is safe even against a receiver predating the API guard.
|
||||
from hashlib import sha256
|
||||
if (wp_file.is_symlink() or not wp_file.is_file()
|
||||
or sha256(wp_file.read_bytes()).hexdigest() != frozen["sha256"]):
|
||||
raise ValueError("frozen historical source missing or changed")
|
||||
continue
|
||||
except ValueError as exc:
|
||||
report.add(severity="FAIL", check_id="C-44",
|
||||
message=f"Frozen binding batch refused: {exc}",
|
||||
file_path=str(wp_file), fixable=False)
|
||||
return # Never send a partial batch after a frozen-source refusal.
|
||||
if not ws_id:
|
||||
continue
|
||||
bindings.append(binding)
|
||||
if not bindings:
|
||||
return
|
||||
result = _api_put(api_base, "/workplans/index/bindings", {"bindings": bindings})
|
||||
|
|
@ -3610,6 +3635,45 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
|
|||
# Fix engine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _refuse_frozen_fix_batch(report: ConsistencyReport, repo_slug: str) -> bool:
|
||||
"""Refuse a complete repair batch touching the exact bound histories."""
|
||||
if any(issue.check_id == "C-44" and issue.severity == "FAIL" for issue in report.issues):
|
||||
return True
|
||||
protected_ids = {entry["uuid"] for entry in FROZEN_CLUSTER_BINDINGS}
|
||||
protected_ids.update(task["uuid"] for entry in FROZEN_CLUSTER_BINDINGS
|
||||
for task in entry["tasks"])
|
||||
paths = {entry["path"] for entry in FROZEN_CLUSTER_BINDINGS}
|
||||
root = Path(report.repo_path).absolute() if report.repo_path else None
|
||||
|
||||
def protected(value, key=""):
|
||||
if isinstance(value, dict):
|
||||
return any(protected(item, name) for name, item in value.items())
|
||||
if isinstance(value, (list, tuple)):
|
||||
return any(protected(item, key) for item in value)
|
||||
text = str(value)
|
||||
if key in {"db_id", "ws_id", "task_id", "from_workstream_id",
|
||||
"from_workplan_id", "from_task_id", "desired_ws_id",
|
||||
"desired_task_id", "desired_record_id", "state_hub_workstream_id", "state_hub_task_id"}:
|
||||
if text in protected_ids:
|
||||
return True
|
||||
if key in {"file_path", "wp_file", "md_path", "path"} and text:
|
||||
candidate = Path(text)
|
||||
if candidate.is_absolute():
|
||||
return root is not None and any(candidate.absolute() == root / path for path in paths)
|
||||
return repo_slug == "railiance-cluster" and candidate.as_posix() in paths
|
||||
return False
|
||||
|
||||
for issue in report.issues:
|
||||
if issue.fixable and (protected(issue.db_id, "db_id")
|
||||
or protected(issue.file_path, "file_path")
|
||||
or protected(issue._fix_context)):
|
||||
report.add(severity="FAIL", check_id="C-44",
|
||||
message="Frozen historical repair batch refused; owner review required",
|
||||
fixable=False)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fix_repo(
|
||||
api_base: str,
|
||||
repo_slug: str,
|
||||
|
|
@ -3624,7 +3688,9 @@ def fix_repo(
|
|||
repo_path_override,
|
||||
bootstrap_empty_projection=bootstrap_empty_projection,
|
||||
)
|
||||
if any(i.check_id in {"C-00", "C-36"} for i in report.failures):
|
||||
if _refuse_frozen_fix_batch(report, repo_slug):
|
||||
return report
|
||||
if any(i.check_id in {"C-00", "C-36", "C-44"} for i in report.failures):
|
||||
return report
|
||||
|
||||
# RMGR-WP-0002: optional repo-manager reconcile proxy (index rebuild) for pilots
|
||||
|
|
|
|||
200
tests/test_frozen_binding_guard_isolated.py
Normal file
200
tests/test_frozen_binding_guard_isolated.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
from pathlib import Path
|
||||
import ast, asyncio, importlib.util, types, uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec=importlib.util.spec_from_file_location('historical',ROOT/'api/services/historical_projection.py')
|
||||
h=importlib.util.module_from_spec(spec);spec.loader.exec_module(h)
|
||||
B=h.FROZEN_CLUSTER_BINDINGS[0]
|
||||
def binding(_bound=B,**changes):
|
||||
B=_bound
|
||||
d=dict(workplan_id=uuid.UUID(B['uuid']),repo_slug=B['repo'],filename=Path(B['path']).name,relative_path=B['path'],archived=True,status='finished');d.update(changes);return d
|
||||
class Obj:
|
||||
def __init__(self,**kw):self.__dict__.update(kw);self.writes=[]
|
||||
def __setattr__(self,k,v):
|
||||
if 'writes' in self.__dict__:self.writes.append(k)
|
||||
object.__setattr__(self,k,v)
|
||||
class Entry(Obj):
|
||||
def model_dump(self):return {k:v for k,v in self.__dict__.items() if k!='writes'}
|
||||
class Col:
|
||||
def in_(self,x):return self
|
||||
def __eq__(self,x):return self
|
||||
class Query:
|
||||
def __init__(self):self.locked=False;self.refreshed=False;self.ordered=False
|
||||
def where(self,*a):return self
|
||||
def order_by(self,*a):self.ordered=True;return self
|
||||
def with_for_update(self):self.locked=True;return self
|
||||
def execution_options(self,**kw):self.refreshed=kw.get("populate_existing") is True;return self
|
||||
class Model: id=Col();slug=Col();workplan_id=Col()
|
||||
class Result:
|
||||
def __init__(self,values):self.values=values
|
||||
def scalars(self):return self
|
||||
def all(self):return self.values
|
||||
def scalar_one_or_none(self):return self.values[0] if self.values else None
|
||||
class Session:
|
||||
def __init__(self,rows):self.results=iter(rows);self.commits=0;self.queries=[]
|
||||
async def execute(self,q):self.queries.append(q);return Result(next(self.results))
|
||||
async def commit(self):self.commits+=1
|
||||
class HTTPException(Exception):
|
||||
def __init__(self,status_code,detail):self.status_code=status_code;super().__init__(detail)
|
||||
def extracted(path,name,extra):
|
||||
tree=ast.parse((ROOT/path).read_text());node=next(n for n in tree.body if isinstance(n,(ast.FunctionDef,ast.AsyncFunctionDef)) and n.name==name)
|
||||
node.decorator_list=[];node.returns=None
|
||||
for a in node.args.args + node.args.kwonlyargs:a.annotation=None
|
||||
node.args.defaults=[]
|
||||
ns=dict(Path=Path,datetime=datetime,timezone=timezone,**extra)
|
||||
exec(compile(ast.Module(body=[node],type_ignores=[]),str(path),'exec'),ns);return ns[name]
|
||||
def actual_report():
|
||||
tree=ast.parse((ROOT/'scripts/consistency_check.py').read_text())
|
||||
nodes=[n for n in tree.body if (isinstance(n,ast.ClassDef) and n.name in {'Issue','ConsistencyReport'}) or (isinstance(n,ast.AnnAssign) and isinstance(n.target,ast.Name) and n.target.id=='_AUTOMATION_ERROR_CHECKS')]
|
||||
ns=dict(dataclass=dataclass,field=field,_AUTOMATION_ERROR_CHECKS=frozenset({'C-00','C-44'}))
|
||||
exec(compile(ast.Module(body=nodes,type_ignores=[]),'actual_report','exec'),ns)
|
||||
return ns['ConsistencyReport']('railiance-cluster','/unused')
|
||||
|
||||
def closure(B=B):
|
||||
repo=Obj(id=uuid.uuid4());parent=Obj(id=uuid.UUID(B['uuid']),repo_id=repo.id,status='finished')
|
||||
tasks=[Obj(id=uuid.UUID(t['uuid']),workplan_id=parent.id,status='done') for t in B['tasks']]
|
||||
return repo,parent,tasks
|
||||
async def run_api(entries,repo,parent,tasks,ordinary=None):
|
||||
rows=([parent] if parent else [])+([ordinary] if ordinary else [])
|
||||
session=Session([rows,[repo] if repo else [],tasks])
|
||||
fn=extracted('api/routers/workstreams.py','sync_workplan_bindings',dict(frozen_cluster_binding=h.frozen_cluster_binding,validate_frozen_cluster_rows=h.validate_frozen_cluster_rows,HTTPException=HTTPException,select=lambda *_:Query(),Workplan=Model,Task=Model,ManagedRepo=Model,_invalidate_workplan_index_cache=lambda:None))
|
||||
result=await fn(Obj(bindings=[Entry(**e) for e in entries]),session)
|
||||
return result,session
|
||||
@pytest.mark.parametrize('change',[dict(repo_slug='foreign'),dict(workplan_id=uuid.uuid4()),dict(relative_path='workplans/other.md'),dict(filename='other.md'),dict(archived=False),dict(status='active')])
|
||||
def test_metadata_refuses(change):
|
||||
with pytest.raises(ValueError):h.frozen_cluster_binding(binding(**change))
|
||||
def test_unrelated_binding_unchanged():assert h.frozen_cluster_binding(binding(workplan_id=uuid.uuid4(),filename='ordinary.md',relative_path='workplans/ordinary.md',repo_slug='other')) is None
|
||||
@pytest.mark.parametrize('fault',['parent_missing','foreign_repo','parent_active','task_missing','task_active','task_foreign','repo_missing'])
|
||||
@pytest.mark.parametrize("bound",h.FROZEN_CLUSTER_BINDINGS)
|
||||
def test_mixed_batch_refusal_precedes_all_writes(fault,bound):
|
||||
repo,parent,tasks=closure(bound);ordinary=Obj(id=uuid.uuid4())
|
||||
if fault=='repo_missing':repo=None
|
||||
if fault=='parent_missing':parent=None
|
||||
if fault=='foreign_repo':parent.repo_id=uuid.uuid4()
|
||||
if fault=='parent_active':parent.status='active'
|
||||
if fault=='task_missing':tasks.pop()
|
||||
if fault=='task_active':tasks[0].status='progress'
|
||||
if fault=='task_foreign':tasks[0].workplan_id=uuid.uuid4()
|
||||
if parent:parent.writes.clear()
|
||||
for t in tasks:t.writes.clear()
|
||||
normal=binding(workplan_id=ordinary.id,repo_slug='ordinary',filename='ordinary.md',relative_path='workplans/ordinary.md')
|
||||
with pytest.raises(HTTPException):asyncio.run(run_api([normal,binding(_bound=bound)],repo,parent,tasks,ordinary))
|
||||
assert ordinary.writes==[]
|
||||
if parent:assert parent.writes==[]
|
||||
assert all(t.writes==[] for t in tasks)
|
||||
@pytest.mark.parametrize("bound",h.FROZEN_CLUSTER_BINDINGS)
|
||||
def test_exact_frozen_no_setters_or_commit(bound):
|
||||
repo,parent,tasks=closure(bound);result,session=asyncio.run(run_api([binding(_bound=bound)],repo,parent,tasks))
|
||||
assert result==dict(updated=0,received=1,preserved=1);assert session.commits==0;assert parent.writes==[]
|
||||
assert all(q.locked and q.refreshed and q.ordered for q in (session.queries[0],session.queries[2]))
|
||||
@pytest.mark.parametrize("bound",h.FROZEN_CLUSTER_BINDINGS)
|
||||
def test_mixed_valid_preserves_frozen_and_updates_ordinary(bound):
|
||||
repo,parent,tasks=closure(bound);ordinary=Obj(id=uuid.uuid4())
|
||||
normal=binding(workplan_id=ordinary.id,repo_slug='ordinary',filename='ordinary.md',relative_path='workplans/ordinary.md')
|
||||
result,session=asyncio.run(run_api([normal,binding(_bound=bound)],repo,parent,tasks,ordinary))
|
||||
assert result==dict(updated=1,received=2,preserved=1);assert session.commits==1;assert parent.writes==[]
|
||||
assert ordinary.writes==['backing_filename','backing_relative_path','backing_archived','backing_synced_at']
|
||||
@pytest.mark.parametrize('tamper',['metadata','bytes','symlink','missing_scan','missing_uuid','valid'])
|
||||
@pytest.mark.parametrize("bound_index", range(3))
|
||||
def test_cli_exact_source_omission_or_whole_batch_refusal(tmp_path,tamper,bound_index):
|
||||
bound=h.FROZEN_CLUSTER_BINDINGS[bound_index]
|
||||
import shutil
|
||||
infos=[]
|
||||
for entry in h.FROZEN_CLUSTER_BINDINGS:
|
||||
target=tmp_path/entry['path'];target.parent.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copyfile(Path('/home/worsch/railiance-cluster')/entry['path'],target)
|
||||
infos.append((target,dict(state_hub_workstream_id=entry['uuid'],status='finished'),''))
|
||||
path=tmp_path/bound['path']
|
||||
meta=dict(state_hub_workstream_id=bound['uuid'],status='finished')
|
||||
if tamper=='metadata':meta['status']='active'
|
||||
if tamper=='missing_uuid':meta.pop('state_hub_workstream_id')
|
||||
if tamper=='bytes':path.write_text(path.read_text()+'\n')
|
||||
if tamper=='symlink':path.unlink();path.symlink_to(Path('/home/worsch/railiance-cluster')/bound['path'])
|
||||
ordinary=tmp_path/'workplans/ordinary.md';ordinary.write_text('ordinary')
|
||||
if tamper=='missing_scan':infos.pop(bound_index)
|
||||
else:infos[bound_index]=(path,meta,'')
|
||||
calls=[];report=actual_report()
|
||||
fn=extracted('scripts/consistency_check.py','_sync_workplan_bindings',dict(FROZEN_CLUSTER_BINDINGS=h.FROZEN_CLUSTER_BINDINGS,frozen_cluster_binding=h.frozen_cluster_binding,normalise_workstream_status=lambda s:s,workplan_display_path=lambda root,p:p.relative_to(root).as_posix(),_api_put=lambda *a:calls.append(a) or dict(updated=1,received=1)))
|
||||
fn('http://unused','railiance-cluster',[(ordinary,dict(state_hub_workstream_id=str(uuid.uuid4()),status='active'),''),*infos],tmp_path,report)
|
||||
if tamper=='valid':assert len(calls)==1 and len(calls[0][2]['bindings'])==1
|
||||
else:assert calls==[] and report.failures[0].check_id=='C-44' and report.automation_errors
|
||||
|
||||
|
||||
def test_api_tampered_mixed_batch_refuses_before_queries_or_assignments():
|
||||
repo,parent,tasks=closure();ordinary=Obj(id=uuid.uuid4())
|
||||
with pytest.raises(HTTPException):
|
||||
asyncio.run(run_api([binding(workplan_id=ordinary.id,repo_slug='ordinary',filename='ordinary.md',relative_path='workplans/ordinary.md'),binding(archived=False)],repo,parent,tasks,ordinary))
|
||||
assert ordinary.writes==[] and parent.writes==[]
|
||||
|
||||
|
||||
def test_foreign_repo_same_filename_path_is_ordinary():
|
||||
assert h.frozen_cluster_binding(binding(workplan_id=uuid.uuid4(),repo_slug='foreign')) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('entry',h.FROZEN_CLUSTER_BINDINGS)
|
||||
def test_all_three_registry_bindings_are_guarded(entry):
|
||||
good=dict(workplan_id=entry['uuid'],repo_slug=entry['repo'],filename=Path(entry['path']).name,relative_path=entry['path'],archived=True,status='finished')
|
||||
assert h.frozen_cluster_binding(good) is entry
|
||||
with pytest.raises(ValueError):h.frozen_cluster_binding(dict(good,status='active'))
|
||||
|
||||
|
||||
def test_real_cli_exit_criterion_is_nonzero_including_remote_all():
|
||||
report=actual_report();report.add(severity='FAIL',check_id='C-44',message='refused')
|
||||
fn=extracted('scripts/consistency_check.py','consistency_exit_code',{})
|
||||
assert fn([report])==1 and fn([report],remote_all=True)==1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('check,key,protected_kind',[
|
||||
('C-04','ws_id','parent'),('C-10','task_id','task'),('C-22','task_id','task'),
|
||||
('C-09','db_id','parent'),('C-23','wp_file','path'),
|
||||
])
|
||||
@pytest.mark.parametrize('bound',h.FROZEN_CLUSTER_BINDINGS)
|
||||
def test_complete_fix_batch_preflight_before_ordinary_writes(check,key,protected_kind,bound):
|
||||
report=actual_report();report.repo_path='/tmp/fixture-cluster'
|
||||
report.add(severity='WARN',check_id='C-05',message='ordinary first',fixable=True,_fix_context={'ws_id':str(uuid.uuid4()),'field':'title','value':'ordinary'})
|
||||
value=bound['uuid'] if protected_kind=='parent' else bound['tasks'][0]['uuid'] if protected_kind=='task' else str(Path(report.repo_path)/bound['path'])
|
||||
report.add(severity='WARN',check_id=check,message='protected later',fixable=True,db_id=value if key=='db_id' else '',_fix_context={key:value})
|
||||
guard=extracted('scripts/consistency_check.py','_refuse_frozen_fix_batch',dict(FROZEN_CLUSTER_BINDINGS=h.FROZEN_CLUSTER_BINDINGS))
|
||||
assert guard(report,'railiance-cluster') and report.automation_errors
|
||||
calls=[]
|
||||
fn=extracted('scripts/consistency_check.py','fix_repo',dict(check_repo=lambda *a,**kw:report,_refuse_frozen_fix_batch=guard,_api_post=lambda *a:calls.append(a),_api_patch=lambda *a:calls.append(a)))
|
||||
assert fn('http://unused','railiance-cluster',None,False,False) is report and calls==[]
|
||||
|
||||
def test_unrelated_fix_context_not_refused():
|
||||
report=actual_report();report.add(severity='WARN',check_id='C-05',message='ordinary',fixable=True,_fix_context={'ws_id':str(uuid.uuid4())})
|
||||
guard=extracted('scripts/consistency_check.py','_refuse_frozen_fix_batch',dict(FROZEN_CLUSTER_BINDINGS=h.FROZEN_CLUSTER_BINDINGS))
|
||||
assert not guard(report,'other')
|
||||
|
||||
|
||||
def test_check_repo_guard_precedes_binding_sync_and_does_not_duplicate():
|
||||
tree=ast.parse((ROOT/'scripts/consistency_check.py').read_text())
|
||||
node=next(n for n in tree.body if isinstance(n,ast.FunctionDef) and n.name=='check_repo')
|
||||
guard_lines=[n.lineno for n in ast.walk(node) if isinstance(n,ast.Call) and isinstance(n.func,ast.Name) and n.func.id=='_refuse_frozen_fix_batch']
|
||||
sync_lines=[n.lineno for n in ast.walk(node) if isinstance(n,ast.Call) and isinstance(n.func,ast.Name) and n.func.id=='_sync_workplan_bindings']
|
||||
assert len(guard_lines)==len(sync_lines)==1 and guard_lines[0]<sync_lines[0]
|
||||
report=actual_report();report.add(severity='FAIL',check_id='C-44',message='already refused')
|
||||
guard=extracted('scripts/consistency_check.py','_refuse_frozen_fix_batch',dict(FROZEN_CLUSTER_BINDINGS=h.FROZEN_CLUSTER_BINDINGS))
|
||||
assert guard(report,'railiance-cluster') and len(report.issues)==1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bound',h.FROZEN_CLUSTER_BINDINGS)
|
||||
@pytest.mark.parametrize('key',['to_workstream_id','to_workplan_id','to_task_id'])
|
||||
def test_ordinary_dependency_to_history_remains_allowed(bound,key):
|
||||
report=actual_report();ordinary=str(uuid.uuid4())
|
||||
target=bound['tasks'][0]['uuid'] if key=='to_task_id' else bound['uuid']
|
||||
report.add(severity='WARN',check_id='C-20',message='ordinary inbound edge',fixable=True,db_id=ordinary,_fix_context={'from_workstream_id':ordinary,key:target,'relationship_type':'blocks'})
|
||||
guard=extracted('scripts/consistency_check.py','_refuse_frozen_fix_batch',dict(FROZEN_CLUSTER_BINDINGS=h.FROZEN_CLUSTER_BINDINGS))
|
||||
assert not guard(report,'railiance-cluster') and not report.automation_errors
|
||||
|
||||
@pytest.mark.parametrize('bound',h.FROZEN_CLUSTER_BINDINGS)
|
||||
@pytest.mark.parametrize('key',['from_workstream_id','from_workplan_id','from_task_id','state_hub_workstream_id','state_hub_task_id','desired_record_id'])
|
||||
def test_protected_dependency_origin_and_nested_binding_refused(bound,key):
|
||||
report=actual_report();origin=bound['tasks'][0]['uuid'] if 'task' in key else bound['uuid']
|
||||
context={key:origin,'to_workstream_id':str(uuid.uuid4())}
|
||||
if key.startswith('state_hub'):context={'meta':context}
|
||||
report.add(severity='WARN',check_id='C-20',message='protected origin',fixable=True,_fix_context=context)
|
||||
guard=extracted('scripts/consistency_check.py','_refuse_frozen_fix_batch',dict(FROZEN_CLUSTER_BINDINGS=h.FROZEN_CLUSTER_BINDINGS))
|
||||
assert guard(report,'railiance-cluster') and report.automation_errors
|
||||
|
|
@ -199,27 +199,37 @@ not delivered receiving or a committed native trial.
|
|||
|
||||
```task
|
||||
id: STATE-WP-0095-T06
|
||||
status: wait
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T05]
|
||||
needs_human: true
|
||||
intervention_note: "Review exact committed source, CI image and owner deployment proposal before rollout."
|
||||
needs_human: false
|
||||
intervention_note: "Exact revision 4 approved and verified; no pending rollout intervention."
|
||||
state_hub_task_id: "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4"
|
||||
```
|
||||
|
||||
Use the normal CI image and Helm owner path after exact revision/image pin,
|
||||
fresh headroom and reviewed dry-run/diff. API/MCP currently run main-7d99282;
|
||||
Helm revision75 is the observed rollback baseline, requiring freshness before
|
||||
execution. No deployment assent exists for this successor. Verify delivered
|
||||
module hash, health, both readiness checks and expected migrated schema
|
||||
c4d5e6f7a8b9; verify exact
|
||||
three parents/15 tasks and rejection behavior without committed probe changes.
|
||||
Only the verified return unblocks RMGR-WP-0019-T05. The full current image also carries inbox-feed migration c4d5e6f7a8b9,
|
||||
although this receiver patch adds none. Helm rollback leaves that migration in
|
||||
place; unrelated migration acceptance and old-image compatibility need normal
|
||||
owner evidence before rollout approval. Do not run destructive default local
|
||||
DB tests; use only exact disposable DB containment. Costs and monetary risk are
|
||||
unknown pending exact rollout proposal; no zero-cost or autonomy grant is inferred.
|
||||
Completed 2026-10-17 on the founder's approval of exact revision 4 of stable
|
||||
proposal STATE-WP-0095-T06-cluster-frozen-receiving-r1 (decision
|
||||
e3798419-8aa9-5425-8c08-20df6bd82a19; approved digest
|
||||
sha256:63a02e8296d126c0158b7eb2c4ce79c7eeb6f8d6f61298d473c1065c7557ba32).
|
||||
The ordinary owner Helm path delivered revision 76, runtime source 51bc70d and
|
||||
exact image manifest sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015.
|
||||
API/MCP Ready and primary/railiance01 health report expected schema
|
||||
c4d5e6f7a8b9; delivered historical module sha256:e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2 matches.
|
||||
All three parent and 15 task full DB row hashes/columns/UUIDs remained unchanged.
|
||||
Five actual native probes passed: preserve, actual foreign-parent holder move,
|
||||
nonterminal task, nonterminal parent and missing task. Every probe rolled back;
|
||||
a fresh transaction confirmed complete row restoration. The foreign-holder
|
||||
case precisely verified historical binding refusal, not the separate global
|
||||
UUID-holder creation branch. No committed probe writes were made.
|
||||
|
||||
Evidence: [verified receiving rollout](../docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json).
|
||||
This actual receiving return satisfies the prerequisite of RMGR-WP-0019-T05.
|
||||
Normal cluster publication and its immutable-source/row return remain T07;
|
||||
source readiness and this deployment do not count that later acceptance twice.
|
||||
The additive inbox migration is retained; Helm rollback does not downgrade
|
||||
schema or stop an active migration Job. Original failures and exact-UID recovery
|
||||
limits remain in the referenced approved packet. No TLS recovery/native private
|
||||
admission or new autonomy/cost grant is inferred.
|
||||
|
||||
## Verify ordinary cluster receiving and immutable historical rows
|
||||
|
||||
|
|
@ -256,6 +266,79 @@ Secret value change, policy relaxation or actual Secret write is introduced.
|
|||
Command-fixture tests verify both requests and preserve other dry-run phases.
|
||||
Evidence and original refusal remain in the Custodian cluster historical
|
||||
publication packet, revisions/r2 and r3-readiness. This is completed source
|
||||
preparation; T06 remains the explicit native rollout gate. The runtime image
|
||||
preparation; T06 separately records the verified actual rollout. The runtime image
|
||||
continues to be main-51bc70d; Makefile/workplan changes do not create a new
|
||||
application image or native acceptance.
|
||||
|
||||
|
||||
T08 source repair refinement, 2026-10-17: the required normal Make render
|
||||
correctly refused a pre-existing generated chart classification copy at v1.0
|
||||
after authoritative canon reached v1.1. Regenerated only through the existing
|
||||
scripts/sync_classification_allowed.py, preserving canon and strict check.
|
||||
The exact source diff is version 1.0→1.1 plus publication/history/participation.
|
||||
Normal Make rendering and the focused classification check passed in an
|
||||
isolated owner-layout copy. The original refusal is retained in the Custodian
|
||||
owner-followthrough-r1 validation receipt. This is source repair only: no live
|
||||
ConfigMap apply, new runtime image, policy relaxation or T07 completion.
|
||||
|
||||
|
||||
T07 actual ordinary-publication return, 2026-10-17: cluster owner deep consistency
|
||||
and normal fast projection completed, exact archived source bytes and all 15
|
||||
historical task full-row hashes remained unchanged. The later full-row check
|
||||
FAILED for exactly the three parent rows. The earlier approved T06 rollout had
|
||||
preserved all 18 rows at its verification stage; that success is not the later
|
||||
whole-pipeline acceptance. The original snapshots retain hashes only, so exact
|
||||
changed columns and original field preimages are not established. No guessed
|
||||
DB repair or claim that all 18 original row hashes are restored is authorized.
|
||||
T07 and RMGR-WP-0019-T06 remain waiting pending T09/T10 and a reviewed return.
|
||||
|
||||
## Guard immutable historical index bindings in CLI and receiving API
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T09
|
||||
status: done
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T06]
|
||||
state_hub_task_id: "8be8f13b-6352-5295-9755-de6fbfb13593"
|
||||
```
|
||||
|
||||
Prepare and publish the narrowly scoped ordinary-binding repair for the three
|
||||
existing task-bound Cluster registry entries. The CLI verifies exact frozen
|
||||
source metadata/bytes and omits those bindings, including on old receivers.
|
||||
The API independently validates the complete batch before any mutation, checks
|
||||
actual repo ownership and the existing terminal parent/task closure, and
|
||||
preserves valid frozen rows without field or timestamp assignments. Invalid
|
||||
metadata, missing/foreign/nonterminal rows refuse the batch. Unknown ordinary
|
||||
bindings retain existing behavior; historical IDs, UUIDs and source bytes stay
|
||||
unchanged. Isolated regression tests and normal owner rendering are required.
|
||||
Reviewed source preparation and canonical regression proof completed on
|
||||
2026-10-17: 102 isolated tests pass and normal Make render/classification
|
||||
check passes (eight objects, three unchanged delivered runtime images).
|
||||
This completion record accompanies the same source publication/projection;
|
||||
source preparation does not establish delivered runtime or T07 acceptance.
|
||||
Central check/fix engines also refuse the complete repair batch if any fixable
|
||||
context touches an exact registered parent/task UUID or frozen source path.
|
||||
This runs before index bindings and before reconcile/push/individual fixes;
|
||||
ordinary earlier fixes cannot partially execute before a later frozen refusal.
|
||||
|
||||
## Deliver and verify the ordinary-binding guard successor
|
||||
|
||||
```task
|
||||
id: STATE-WP-0095-T10
|
||||
status: wait
|
||||
priority: high
|
||||
depends_on: [STATE-WP-0095-T09]
|
||||
needs_human: true
|
||||
intervention_note: "New API runtime requires its own exact image, reviewed preflight and scoped rollout approval; prior revision-4 approval does not cover it."
|
||||
state_hub_task_id: "88585c69-d9b9-5e1f-a256-6416ff71e824"
|
||||
```
|
||||
|
||||
After source publication, resolve normal CI status and immutable image identity,
|
||||
verify delivered guard source closure, prepare exact chart/reuse-values dry-run,
|
||||
full closure/headroom/schema baseline and conditional recovery. Retain additive
|
||||
migration and active-Job cancellation limitations; no Helm schema downgrade or
|
||||
bounded-outage claim. Obtain scoped approval before live rollout. Then verify
|
||||
new full-row baseline preservation and actual rollback-only refusal probes,
|
||||
and only then return to T07 ordinary-pipeline acceptance. This prevents future
|
||||
binding mutation; it does not reconstruct or restore original parent hashes.
|
||||
No production changes, new autonomy or credential authority are granted here.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue