Admit reviewed receiver evidence batch 2
This commit is contained in:
parent
4b647dc32f
commit
5994d4208b
24 changed files with 28536 additions and 0 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"schema": "statehub.source-owner-followthrough.v1",
|
||||
"revision": 5,
|
||||
"status": "isolated_source_candidate_not_published",
|
||||
"base_revision": "086d79a2736e34b916bd7de3c8a65e2ea38a3d64",
|
||||
"files": [
|
||||
{
|
||||
"path": "workplans/STATE-WP-0095-historical-source-receiving.md",
|
||||
"action": "modify",
|
||||
"base_sha256": "ef10359cc5974283b5bdbfc602adf14e3c24362adf00640698d2937c6538483a",
|
||||
"candidate_sha256": "5671189ee87f2d4dff08979f6243e5eb7888b140955899642a74d569496b71c7"
|
||||
},
|
||||
{
|
||||
"path": "Makefile",
|
||||
"action": "modify",
|
||||
"base_sha256": "5584ef94071044cca8215141ecd8aee8d6ae5cfe73e8f5719358c680eb8af0d4",
|
||||
"candidate_sha256": "c5b70e8d4b8ae70380e0ade1c35f9e7fe80038de10080a4723b21ef07a98001e"
|
||||
},
|
||||
{
|
||||
"path": "deploy/railiance/apps/charts/state-hub/Chart.yaml",
|
||||
"action": "modify",
|
||||
"base_sha256": "7fe02b390ffd2ba4ce6f82bd62df900826826401e31bdc2138dd5bf36b51a6ae",
|
||||
"candidate_sha256": "61a989fcf277d05ffbececb7332ee3ca1d8f369c72f960ab1bc4dbc6815536b8"
|
||||
},
|
||||
{
|
||||
"path": "deploy/railiance/apps/helm/state-hub-values.yaml",
|
||||
"action": "modify",
|
||||
"base_sha256": "f7cc0e9968598f6ce0faa0d56f62d6817845c3570c18b6ac39ed6561b952b197",
|
||||
"candidate_sha256": "b51296fc937450f4ecf0ae43243262a631b551389c0d09c8bfe58fa10e2f3ee9"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json",
|
||||
"action": "add",
|
||||
"base_sha256": null,
|
||||
"candidate_sha256": "fb0b59ba3558e084fb4ae2c36c556f3cfbb2a889f6fd86d4e803c52f483b26fd"
|
||||
},
|
||||
{
|
||||
"path": "deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml",
|
||||
"action": "modify",
|
||||
"base_sha256": "9c36fbd5308b19695f9f05d86f8e315bbeccdd267575ce4e2fccf6fb2ea82e32",
|
||||
"candidate_sha256": "f53c651bb4329c09c04c66f906e2462fe4339ab51ef0ec3da9f942c3016f5bed"
|
||||
},
|
||||
{
|
||||
"path": "api/services/historical_projection.py",
|
||||
"action": "modify",
|
||||
"base_sha256": "e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2",
|
||||
"candidate_sha256": "e24e57c58f4aaa4ec9d9bf088009ac69006e53f75b9477ee53dc26c4e850b975"
|
||||
},
|
||||
{
|
||||
"path": "api/routers/workstreams.py",
|
||||
"action": "modify",
|
||||
"base_sha256": "61e7a0ab638b155e9e0b2a72fe8e0ddd2edf5bc6c2d3ca7f6c3ff51e8868910b",
|
||||
"candidate_sha256": "9c9bf4376dd323b52109912bb3f615949e89ac02c8b88b4720f569f8565ad1d6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/consistency_check.py",
|
||||
"action": "modify",
|
||||
"base_sha256": "1827921c79b476bee823c2ddf9896aeb83f3efca6548427e17b3244216bbc6b5",
|
||||
"candidate_sha256": "eaee916f46a11f49d2621fc9386717d73b7fd517acbe4b695e7b551d67bfa8f7"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_frozen_binding_guard_isolated.py",
|
||||
"action": "add",
|
||||
"base_sha256": null,
|
||||
"candidate_sha256": "4dc64df78a02dc633e5744d0277afe5d350fbb0637196e78dec992f91135bded"
|
||||
}
|
||||
],
|
||||
"patch_sha256": "sha256:ad3d49b83435dd026118ee1e4a25779b5a7739473619bbda256f160c4eb0e56a",
|
||||
"T06": "done_verified_approved_rollout_at_stage",
|
||||
"T07": "wait_actual_later_parent_row_preservation_failure",
|
||||
"T08": "done_source_repair_pending_publication",
|
||||
"T09": "progress_until_source_published",
|
||||
"T10": "wait_needs_human_new_runtime",
|
||||
"live_effects": false,
|
||||
"original_parent_preimages": "unknown_do_not_restore_guesses",
|
||||
"managed_UUIDs": "T09_T10_not_allocated_pending_review"
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"schema": "statehub.source-owner-followthrough.v1",
|
||||
"revision": 5,
|
||||
"status": "isolated_source_candidate_not_published",
|
||||
"base_revision": "086d79a2736e34b916bd7de3c8a65e2ea38a3d64",
|
||||
"files": [
|
||||
{
|
||||
"path": "workplans/STATE-WP-0095-historical-source-receiving.md",
|
||||
"action": "modify",
|
||||
"base_sha256": "ef10359cc5974283b5bdbfc602adf14e3c24362adf00640698d2937c6538483a",
|
||||
"candidate_sha256": "a9aa50f5772ac985dcee9b1b65228e5f909ab25209f80dea9d6f0248029855ca"
|
||||
},
|
||||
{
|
||||
"path": "Makefile",
|
||||
"action": "modify",
|
||||
"base_sha256": "5584ef94071044cca8215141ecd8aee8d6ae5cfe73e8f5719358c680eb8af0d4",
|
||||
"candidate_sha256": "c5b70e8d4b8ae70380e0ade1c35f9e7fe80038de10080a4723b21ef07a98001e"
|
||||
},
|
||||
{
|
||||
"path": "deploy/railiance/apps/charts/state-hub/Chart.yaml",
|
||||
"action": "modify",
|
||||
"base_sha256": "7fe02b390ffd2ba4ce6f82bd62df900826826401e31bdc2138dd5bf36b51a6ae",
|
||||
"candidate_sha256": "61a989fcf277d05ffbececb7332ee3ca1d8f369c72f960ab1bc4dbc6815536b8"
|
||||
},
|
||||
{
|
||||
"path": "deploy/railiance/apps/helm/state-hub-values.yaml",
|
||||
"action": "modify",
|
||||
"base_sha256": "f7cc0e9968598f6ce0faa0d56f62d6817845c3570c18b6ac39ed6561b952b197",
|
||||
"candidate_sha256": "b51296fc937450f4ecf0ae43243262a631b551389c0d09c8bfe58fa10e2f3ee9"
|
||||
},
|
||||
{
|
||||
"path": "docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json",
|
||||
"action": "add",
|
||||
"base_sha256": null,
|
||||
"candidate_sha256": "fb0b59ba3558e084fb4ae2c36c556f3cfbb2a889f6fd86d4e803c52f483b26fd"
|
||||
},
|
||||
{
|
||||
"path": "deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml",
|
||||
"action": "modify",
|
||||
"base_sha256": "9c36fbd5308b19695f9f05d86f8e315bbeccdd267575ce4e2fccf6fb2ea82e32",
|
||||
"candidate_sha256": "f53c651bb4329c09c04c66f906e2462fe4339ab51ef0ec3da9f942c3016f5bed"
|
||||
},
|
||||
{
|
||||
"path": "api/services/historical_projection.py",
|
||||
"action": "modify",
|
||||
"base_sha256": "e3c4bf965764b32a6df5d61c71272ee22569df9a9ddae2a3fc3df97ec554e0b2",
|
||||
"candidate_sha256": "e24e57c58f4aaa4ec9d9bf088009ac69006e53f75b9477ee53dc26c4e850b975"
|
||||
},
|
||||
{
|
||||
"path": "api/routers/workstreams.py",
|
||||
"action": "modify",
|
||||
"base_sha256": "61e7a0ab638b155e9e0b2a72fe8e0ddd2edf5bc6c2d3ca7f6c3ff51e8868910b",
|
||||
"candidate_sha256": "9c9bf4376dd323b52109912bb3f615949e89ac02c8b88b4720f569f8565ad1d6"
|
||||
},
|
||||
{
|
||||
"path": "scripts/consistency_check.py",
|
||||
"action": "modify",
|
||||
"base_sha256": "1827921c79b476bee823c2ddf9896aeb83f3efca6548427e17b3244216bbc6b5",
|
||||
"candidate_sha256": "eaee916f46a11f49d2621fc9386717d73b7fd517acbe4b695e7b551d67bfa8f7"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_frozen_binding_guard_isolated.py",
|
||||
"action": "add",
|
||||
"base_sha256": null,
|
||||
"candidate_sha256": "4dc64df78a02dc633e5744d0277afe5d350fbb0637196e78dec992f91135bded"
|
||||
}
|
||||
],
|
||||
"patch_sha256": "sha256:ad3d49b83435dd026118ee1e4a25779b5a7739473619bbda256f160c4eb0e56a",
|
||||
"T06": "done_verified_approved_rollout_at_stage",
|
||||
"T07": "wait_actual_later_parent_row_preservation_failure",
|
||||
"T08": "done_source_repair_pending_publication",
|
||||
"T09": "progress_until_source_published",
|
||||
"T10": "wait_needs_human_new_runtime",
|
||||
"live_effects": false,
|
||||
"original_parent_preimages": "unknown_do_not_restore_guesses",
|
||||
"managed_UUIDs": "T09_T10_not_allocated_pending_review",
|
||||
"publication_stage": "root_reviewed_UUIDs_and_T09_completion",
|
||||
"managed_UUID_assignments": [
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "STATE-WP-0095-T09",
|
||||
"uuid": "8be8f13b-6352-5295-9755-de6fbfb13593",
|
||||
"path": "workplans/STATE-WP-0095-historical-source-receiving.md"
|
||||
},
|
||||
{
|
||||
"kind": "task",
|
||||
"record_id": "STATE-WP-0095-T10",
|
||||
"uuid": "88585c69-d9b9-5e1f-a256-6416ff71e824",
|
||||
"path": "workplans/STATE-WP-0095-historical-source-receiving.md"
|
||||
}
|
||||
],
|
||||
"final_workplan_sha256": "sha256:a9aa50f5772ac985dcee9b1b65228e5f909ab25209f80dea9d6f0248029855ca"
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Prepared exact-ten-path source publisher; requires final reviewed overlay. No deploy/sync."""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
BASE = Path(__file__).resolve().parent
|
||||
OWNER = Path('/home/worsch/state-hub')
|
||||
SCANNER = ['/usr/bin/python3', '/home/worsch/repo-manager/src/repo_manager/secret_scan.py',
|
||||
'--path', str(OWNER), '--binary', '/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks']
|
||||
|
||||
class Refusal(RuntimeError):
|
||||
pass
|
||||
|
||||
def sha(raw):
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
def git(owner, *args, check=True):
|
||||
p = subprocess.run(['git', '-C', str(owner), *args], capture_output=True, timeout=150)
|
||||
if check and p.returncode:
|
||||
raise Refusal('git_' + args[0] + '_refused_' + str(p.returncode))
|
||||
return p
|
||||
|
||||
def index_entries(owner):
|
||||
# Stage bytes remain private to this process. Receipts expose hashes only.
|
||||
return dict((entry.split(b'\t', 1)[1].decode(), entry.split(b'\t', 1)[0])
|
||||
for entry in git(owner, 'ls-files', '--stage', '-z').stdout.split(b'\0') if entry)
|
||||
|
||||
def dirty_files(owner):
|
||||
tracked = git(owner, 'diff', '--name-only', 'HEAD', '-z').stdout.split(b'\0')
|
||||
untracked = git(owner, 'ls-files', '--others', '--exclude-standard', '-z').stdout.split(b'\0')
|
||||
return sorted({p.decode() for p in tracked + untracked if p})
|
||||
|
||||
def preserved_state(owner, owned):
|
||||
indices = {p: sha(v) for p, v in index_entries(owner).items() if p not in owned}
|
||||
worktree = {}
|
||||
for p in dirty_files(owner):
|
||||
if p in owned:
|
||||
continue
|
||||
file = owner / p
|
||||
if file.is_symlink():
|
||||
raise Refusal('unrelated_symlink_requires_review')
|
||||
worktree[p] = sha(file.read_bytes()) if file.exists() else None
|
||||
return {'index_entries': indices, 'dirty_worktree': worktree}
|
||||
|
||||
def validate(owner, expected):
|
||||
if git(owner, 'rev-parse', 'HEAD').stdout.decode().strip() != expected['base_revision']:
|
||||
raise Refusal('source_base_drift')
|
||||
files = expected['files']
|
||||
names = {f['path'] for f in files}
|
||||
if len(names) != len(files):
|
||||
raise Refusal('duplicate_expected_path')
|
||||
for f in files:
|
||||
p = owner / f['path']
|
||||
if p.is_symlink() or not p.is_file() or sha(p.read_bytes()) != f['candidate_sha256']:
|
||||
raise Refusal('reviewed_source_drift')
|
||||
# Every unrelated worktree/index entry is retained. No stash/reset/clean.
|
||||
return names, preserved_state(owner, names)
|
||||
|
||||
def commit_source(owner, expected, scanner, execute=False, push=False):
|
||||
names, before = validate(owner, expected)
|
||||
result = {'schema': 'receiver-source-publication.v1', 'scope': 'source_only',
|
||||
'deployed': False,
|
||||
'base_revision': expected['base_revision'], 'files': sorted(names),
|
||||
'preserved_before': before, 'status': 'validated_not_executed'}
|
||||
if not execute:
|
||||
return result
|
||||
# Stage exact reviewed paths first so the normal scanner inspects them.
|
||||
# A refusal retains reviewed source/staging for inspection; unrelated state stays intact.
|
||||
git(owner, 'add', '--', *sorted(names))
|
||||
scan = subprocess.run(scanner, cwd=owner, capture_output=True, timeout=150)
|
||||
result['scanner_exit'] = scan.returncode
|
||||
if scan.returncode:
|
||||
raise Refusal('normal_source_scanner_refused')
|
||||
# --only preserves unrelated staged content while committing exact paths.
|
||||
git(owner, 'commit', '--only', '-m',
|
||||
'fix(consistency): preserve immutable historical binding rows', '--', *sorted(names))
|
||||
commit = git(owner, 'rev-parse', 'HEAD').stdout.decode().strip()
|
||||
changed = set(git(owner, 'diff-tree', '--no-commit-id', '--name-only', '-r', commit).stdout.decode().splitlines())
|
||||
if changed != names:
|
||||
raise Refusal('committed_path_set_mismatch_stop')
|
||||
if preserved_state(owner, names) != before:
|
||||
raise Refusal('unrelated_state_changed_stop')
|
||||
result.update(status='committed_not_pushed', source_revision=commit,
|
||||
preserved_after=before)
|
||||
if push:
|
||||
# Explicit ref and bounded push. A nonzero or timeout never triggers retry.
|
||||
try:
|
||||
p = git(owner, 'push', 'origin', 'HEAD:refs/heads/main', check=False)
|
||||
result['push_exit'] = p.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
result.update(status='push_timeout_remote_unverified')
|
||||
return result
|
||||
remote = git(owner, 'ls-remote', '--exit-code', 'origin', 'refs/heads/main', check=False)
|
||||
actual = remote.stdout.split()[0].decode() if remote.returncode == 0 and remote.stdout else None
|
||||
result['observed_remote_revision'] = actual
|
||||
result['status'] = 'source_published_verified' if actual == commit else 'push_remote_unverified'
|
||||
return result
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--execute-source-commit', action='store_true')
|
||||
ap.add_argument('--push-source', action='store_true')
|
||||
args = ap.parse_args()
|
||||
if args.push_source:
|
||||
ap.error('direct push disabled; use normal repo-local make sync after exact source commit')
|
||||
publication_manifest = BASE / 'publication-manifest.json'
|
||||
if not publication_manifest.is_file():
|
||||
print(json.dumps({'status':'refused','reason':'reviewed_managed_UUID_T09_completion_manifest_not_ready','scope':'source_only'}))
|
||||
return 1
|
||||
expected = json.loads(publication_manifest.read_text())
|
||||
if expected.get('publication_stage') != 'root_reviewed_UUIDs_and_T09_completion':
|
||||
raise Refusal('publication_review_overlay_missing')
|
||||
if len(expected['files']) != 10:
|
||||
raise Refusal('expected_exact_ten_paths')
|
||||
try:
|
||||
result = commit_source(OWNER, expected, SCANNER, args.execute_source_commit, args.push_source)
|
||||
except (Refusal, subprocess.TimeoutExpired) as exc:
|
||||
result = {'schema': 'receiver-source-publication.v1', 'status': 'refused_or_unverified',
|
||||
'reason': str(exc) if isinstance(exc, Refusal) else 'bounded_command_timeout',
|
||||
'deployed': False}
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result['status'] in {'validated_not_executed','committed_not_pushed','source_published_verified'} else 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from pathlib import Path
|
||||
import subprocess,json
|
||||
p=Path(__file__).resolve().parent
|
||||
r=subprocess.run(['/home/worsch/repo-manager/.venv/bin/python',str(p/'publish_followthrough_source.py'),'--execute-source-commit'],capture_output=True,text=True,timeout=240)
|
||||
try:d=json.loads(r.stdout)
|
||||
except Exception:d={'status':'refused_or_unverified','reason':'publisher_output_unparseable','exit':r.returncode}
|
||||
(p/'source-commit-result.json').write_text(json.dumps(d,indent=2)+'\n')
|
||||
print(json.dumps({k:v for k,v in d.items() if k not in {'preserved_before','preserved_after'}}))
|
||||
raise SystemExit(r.returncode)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"schema": "state95.source-publication-readback.v1",
|
||||
"status": "applied",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "a25b62f75aab6165ee9872ee53ac90c679ec746a",
|
||||
"derived_commit": "a25b62f75aab6165ee9872ee53ac90c679ec746a",
|
||||
"sync_receipt_evidence": "actual make sync tool return session68995 exit0; normalized selected metadata retained here",
|
||||
"identifier_assignments_during_sync": [],
|
||||
"remote_commit": "a25b62f75aab6165ee9872ee53ac90c679ec746a",
|
||||
"task_count": 10,
|
||||
"tasks": [
|
||||
{
|
||||
"uuid": "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7",
|
||||
"record_id": "STATE-WP-0095-T01",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "ebb24194-e4cc-5130-aaa6-663f2ea2c822",
|
||||
"record_id": "STATE-WP-0095-T02",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "e8566341-cf6e-5b9c-ad80-8564d14cd52a",
|
||||
"record_id": "STATE-WP-0095-T03",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "3bbb311f-194e-5862-8aed-358a580f22b4",
|
||||
"record_id": "STATE-WP-0095-T04",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "339b9a37-181c-5599-9a89-0393e199bd4b",
|
||||
"record_id": "STATE-WP-0095-T05",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4",
|
||||
"record_id": "STATE-WP-0095-T06",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
|
||||
"record_id": "STATE-WP-0095-T07",
|
||||
"status": "wait",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "2dd9b04f-2250-5a8a-b5bc-c8a009872858",
|
||||
"record_id": "STATE-WP-0095-T08",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "8be8f13b-6352-5295-9755-de6fbfb13593",
|
||||
"record_id": "STATE-WP-0095-T09",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "88585c69-d9b9-5e1f-a256-6416ff71e824",
|
||||
"record_id": "STATE-WP-0095-T10",
|
||||
"status": "wait",
|
||||
"needs_human": true
|
||||
}
|
||||
],
|
||||
"unrelated_whole_index_and_dirty_preserved": true,
|
||||
"runtime_changed": false,
|
||||
"T07": "wait_later_pipeline_failure_retained",
|
||||
"T10": "wait_needs_human"
|
||||
}
|
||||
|
|
@ -0,0 +1,777 @@
|
|||
--- a/workplans/STATE-WP-0095-historical-source-receiving.md
|
||||
+++ b/workplans/STATE-WP-0095-historical-source-receiving.md
|
||||
@@ -199,27 +199,37 @@
|
||||
|
||||
```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,74 @@
|
||||
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: progress
|
||||
+priority: high
|
||||
+depends_on: [STATE-WP-0095-T06]
|
||||
+```
|
||||
+
|
||||
+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.
|
||||
+Mark done only after reviewed source publication and normal owner 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."
|
||||
+```
|
||||
+
|
||||
+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.
|
||||
--- a/Makefile
|
||||
+++ b/Makefile
|
||||
@@ -9,7 +9,7 @@
|
||||
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.
|
||||
--- a/deploy/railiance/apps/charts/state-hub/Chart.yaml
|
||||
+++ b/deploy/railiance/apps/charts/state-hub/Chart.yaml
|
||||
@@ -3,4 +3,4 @@
|
||||
description: State Hub API service for private Railiance operation
|
||||
type: application
|
||||
version: 0.1.0
|
||||
-appVersion: "main-5dd3df9"
|
||||
+appVersion: "main-51bc70d"
|
||||
--- a/deploy/railiance/apps/helm/state-hub-values.yaml
|
||||
+++ b/deploy/railiance/apps/helm/state-hub-values.yaml
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
image:
|
||||
repository: forgejo.coulomb.social/coulomb/state-hub
|
||||
- tag: "main-0747b4d"
|
||||
+ tag: "main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
--- /dev/null
|
||||
+++ b/docs/evidence/2026-10-17-cluster-historical-receiving-rollout.json
|
||||
@@ -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"
|
||||
+ ]
|
||||
+ }
|
||||
+}
|
||||
--- a/deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml
|
||||
+++ b/deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml
|
||||
@@ -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 @@
|
||||
- tooling
|
||||
- product
|
||||
- business
|
||||
+ - publication
|
||||
|
||||
# domain / secondary_domains — primary exactly 1; secondaries 0..n (§6)
|
||||
domains:
|
||||
@@ -85,6 +86,8 @@
|
||||
- traceability
|
||||
- documentation
|
||||
- decision-support
|
||||
+ - history
|
||||
+ - participation
|
||||
platform_and_operations:
|
||||
- platform
|
||||
- deployment
|
||||
--- a/api/services/historical_projection.py
|
||||
+++ b/api/services/historical_projection.py
|
||||
@@ -193,3 +193,46 @@
|
||||
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")
|
||||
--- a/api/routers/workstreams.py
|
||||
+++ b/api/routers/workstreams.py
|
||||
@@ -16,6 +16,8 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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)
|
||||
--- a/scripts/consistency_check.py
|
||||
+++ b/scripts/consistency_check.py
|
||||
@@ -120,7 +120,9 @@
|
||||
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 @@
|
||||
|
||||
# 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 @@
|
||||
_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 @@
|
||||
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('"')
|
||||
+ archived = wp_file.parent.name == "archived"
|
||||
+ file_status = normalise_workstream_status(str(meta.get("status", "")).strip())
|
||||
+ 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
|
||||
- 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,
|
||||
- }
|
||||
- )
|
||||
+ bindings.append(binding)
|
||||
if not bindings:
|
||||
return
|
||||
result = _api_put(api_base, "/workplans/index/bindings", {"bindings": bindings})
|
||||
@@ -3610,6 +3635,45 @@
|
||||
# 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 @@
|
||||
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
|
||||
--- /dev/null
|
||||
+++ b/tests/test_frozen_binding_guard_isolated.py
|
||||
@@ -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
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
def load(name):
|
||||
path=Path(__file__).parent/(name+'.py')
|
||||
spec=importlib.util.spec_from_file_location(name,path)
|
||||
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
pub=load('publish_followthrough_source')
|
||||
|
||||
def git(root,*args):
|
||||
return subprocess.check_output(['git','-C',str(root),*args]).decode().strip()
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path):
|
||||
root=tmp_path/'owner';root.mkdir()
|
||||
git(root,'init','-b','main');git(root,'config','user.name','Synthetic Test');git(root,'config','user.email','synthetic@example.invalid')
|
||||
for name in ['source.py','.custodian-brief.md','WORK-RECORDS.md','evidence.json']:
|
||||
(root/name).write_text('old\n')
|
||||
git(root,'add','.');git(root,'commit','-m','baseline')
|
||||
base=git(root,'rev-parse','HEAD')
|
||||
(root/'.custodian-brief.md').write_text('staged brief\n');git(root,'add','.custodian-brief.md')
|
||||
(root/'.custodian-brief.md').write_text('working brief\n')
|
||||
(root/'WORK-RECORDS.md').write_text('working index\n');(root/'evidence.json').write_text('working evidence\n')
|
||||
(root/'new-evidence.json').write_text('new evidence\n')
|
||||
(root/'source.py').write_text('reviewed source\n');(root/'new-test.py').write_text('reviewed new test\n')
|
||||
expected={'base_revision':base,'files':[{'path':p,'candidate_sha256':hashlib.sha256((root/p).read_bytes()).hexdigest()} for p in ['source.py','new-test.py']]}
|
||||
return root,expected
|
||||
|
||||
def test_exact_commit_preserves_staged_brief_and_worktree_evidence(repo,tmp_path):
|
||||
root,expected=repo
|
||||
remote=tmp_path/'remote.git';subprocess.run(['git','init','--bare',str(remote)],check=True,capture_output=True)
|
||||
git(root,'remote','add','origin',str(remote))
|
||||
owned={f['path'] for f in expected['files']};before=pub.preserved_state(root,owned)
|
||||
result=pub.commit_source(root,expected,[sys.executable,'-c','raise SystemExit(0)'],execute=True,push=True)
|
||||
assert result['status']=='source_published_verified'
|
||||
assert pub.preserved_state(root,owned)==before
|
||||
assert git(root,'show',':.custodian-brief.md')=='staged brief'
|
||||
assert git(root,'show','HEAD:.custodian-brief.md')=='old'
|
||||
assert set(git(root,'diff-tree','--no-commit-id','--name-only','-r','HEAD').splitlines())==owned
|
||||
|
||||
def test_default_is_read_only_and_drift_refuses_before_commit(repo):
|
||||
root,expected=repo;head=git(root,'rev-parse','HEAD');stage=git(root,'ls-files','--stage')
|
||||
assert pub.commit_source(root,expected,[])['status']=='validated_not_executed'
|
||||
assert git(root,'rev-parse','HEAD')==head and git(root,'ls-files','--stage')==stage
|
||||
(root/'source.py').write_text('unreviewed change')
|
||||
with pytest.raises(pub.Refusal,match='reviewed_source_drift'):
|
||||
pub.commit_source(root,expected,[],execute=True)
|
||||
assert git(root,'rev-parse','HEAD')==head and git(root,'ls-files','--stage')==stage
|
||||
|
||||
def test_normal_scanner_refusal_does_not_commit_or_change_unrelated_state(repo):
|
||||
root,expected=repo;head=git(root,'rev-parse','HEAD')
|
||||
owned={f['path'] for f in expected['files']};before=pub.preserved_state(root,owned)
|
||||
with pytest.raises(pub.Refusal,match='normal_source_scanner_refused'):
|
||||
pub.commit_source(root,expected,[sys.executable,'-c','raise SystemExit(1)'],execute=True)
|
||||
assert git(root,'rev-parse','HEAD')==head and pub.preserved_state(root,owned)==before
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"isolated_regression_tests": 102,
|
||||
"pytest_exit": 0,
|
||||
"pytest_command": "python3 -m pytest --noconftest -q state-hub/tests/test_frozen_binding_guard_isolated.py",
|
||||
"normal_make_render_exit": 0,
|
||||
"render_complete_tracked_chart": true,
|
||||
"rendered_objects": 8,
|
||||
"runtime_images": [
|
||||
"forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015",
|
||||
"forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015",
|
||||
"forgejo.coulomb.social/coulomb/state-hub:main-51bc70d@sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015"
|
||||
],
|
||||
"classification_check_exit": 0,
|
||||
"no_DB_or_cluster_access": true,
|
||||
"no_canonical_mutations": true,
|
||||
"original_r1_r2_receipts_preserved": true
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
from pathlib import Path
|
||||
import json,hashlib,importlib.util
|
||||
p=Path(__file__).resolve().parent;owner=Path('/home/worsch/state-hub')
|
||||
spec=importlib.util.spec_from_file_location('pub',p.parent/'owner-followthrough-r5/publish_followthrough_source.py');pub=importlib.util.module_from_spec(spec);spec.loader.exec_module(pub)
|
||||
d=json.loads((p/'manifest.json').read_text());sha=lambda b:hashlib.sha256(b).hexdigest()
|
||||
assert pub.git(owner,'rev-parse','HEAD').stdout.decode().strip()==d['base_revision']
|
||||
assert sha((p/'state-hub-writeback.patch').read_bytes())=='a99b02b2e2f745c23e7fb33821fdd17656ad22b0e040abe4ff7124f25487b825'
|
||||
owned={e['path'] for e in d['files']};assert len(owned)==3
|
||||
before=pub.preserved_state(owner,owned)
|
||||
for e in d['files']:
|
||||
f=owner/e['path'];s=p/'state-hub'/e['path'];assert not f.is_symlink() and not s.is_symlink()
|
||||
assert (sha(f.read_bytes()) if f.exists() else None)==e['base_sha256']
|
||||
assert sha(s.read_bytes())==e['candidate_sha256']
|
||||
assert not pub.git(owner,'diff','--cached','--',e['path']).stdout
|
||||
for e in d['files']:
|
||||
f=owner/e['path'];f.parent.mkdir(parents=True,exist_ok=True);f.write_bytes((p/'state-hub'/e['path']).read_bytes())
|
||||
assert pub.preserved_state(owner,owned)==before
|
||||
(p/'local-integration-result.json').write_text(json.dumps({'status':'integrated_not_committed','preserved_before':before,'preserved_after':before,'new_UUIDs':False,'runtime_changed':False},indent=2)+'\n')
|
||||
print('Exact3path integration passed; unrelated index and dirty state preserved')
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"schema": "statehub.writeback-isolation.v1",
|
||||
"base_revision": "a25b62f75aab6165ee9872ee53ac90c679ec746a",
|
||||
"files": [
|
||||
{
|
||||
"path": "scripts/consistency_check.py",
|
||||
"base_sha256": "eaee916f46a11f49d2621fc9386717d73b7fd517acbe4b695e7b551d67bfa8f7",
|
||||
"candidate_sha256": "1bfbc89d3ff5c621ec541fd32384083cc7dcf96a639b217d62b6fc34de238efd"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_writeback_git_isolated.py",
|
||||
"base_sha256": null,
|
||||
"candidate_sha256": "b5c00d3228edc8430be0a0c4e7277895bf6cf87782f14eda3a865262730b640e"
|
||||
},
|
||||
{
|
||||
"path": "workplans/STATE-WP-0095-historical-source-receiving.md",
|
||||
"base_sha256": "a9aa50f5772ac985dcee9b1b65228e5f909ab25209f80dea9d6f0248029855ca",
|
||||
"candidate_sha256": "e386157304621ca2c174d6220fcd156e510592d43bb904c2bdbc5f73fa6a0722"
|
||||
}
|
||||
],
|
||||
"patch_sha256": "sha256:a99b02b2e2f745c23e7fb33821fdd17656ad22b0e040abe4ff7124f25487b825",
|
||||
"tests_passed": 5,
|
||||
"canonical_mutation": false,
|
||||
"runtime_changed": false,
|
||||
"T07": "wait",
|
||||
"T09": "done_source_refinement_candidate",
|
||||
"T10": "wait_needs_human"
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"ok": true,
|
||||
"status": "applied",
|
||||
"repo_slug": "state-hub",
|
||||
"commit": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"requests": 2,
|
||||
"elapsed_ms": 13852.7,
|
||||
"git": {
|
||||
"upstream": "origin/main",
|
||||
"behind": 0,
|
||||
"ahead": 0
|
||||
},
|
||||
"identifiers": {
|
||||
"schema": "repo-manager.identifier-assignment.v1",
|
||||
"ok": true,
|
||||
"executed": true,
|
||||
"repo": "state-hub",
|
||||
"namespace": "helixforge",
|
||||
"assignments": [],
|
||||
"historical_invalid_identifiers": [],
|
||||
"historical_identity_collisions": [],
|
||||
"files_changed": []
|
||||
},
|
||||
"receipt": {
|
||||
"schema": "state-hub.repository-projection-reconcile.v1",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"derived_commit": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"outcome": {
|
||||
"schema": "state-hub.projection-reset.v1",
|
||||
"repo_slug": "state-hub",
|
||||
"commit": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"status": "applied",
|
||||
"counts": {
|
||||
"created": 0,
|
||||
"updated": 70,
|
||||
"retired": 0,
|
||||
"refused": 0,
|
||||
"released": 0,
|
||||
"created_tasks": 0,
|
||||
"updated_tasks": 1,
|
||||
"cancelled_tasks": 0,
|
||||
"created_edges": 0,
|
||||
"removed_edges": 0,
|
||||
"unresolved_edges": 0,
|
||||
"external_edges": 6
|
||||
},
|
||||
"created": [],
|
||||
"updated": [
|
||||
"CUST-WP-0003",
|
||||
"CUST-WP-0011",
|
||||
"CUST-WP-0012",
|
||||
"CUST-WP-0038",
|
||||
"CUST-WP-0042",
|
||||
"STATE-WP-0043",
|
||||
"STATE-WP-0044",
|
||||
"STATE-WP-0045",
|
||||
"STATE-WP-0046",
|
||||
"STATE-WP-0047",
|
||||
"STATE-WP-0048",
|
||||
"STATE-WP-0049",
|
||||
"STATE-WP-0050",
|
||||
"STATE-WP-0051",
|
||||
"STATE-WP-0052",
|
||||
"STATE-WP-0053",
|
||||
"STATE-WP-0054",
|
||||
"STATE-WP-0055",
|
||||
"STATE-WP-0056",
|
||||
"STATE-WP-0057",
|
||||
"STATE-WP-0058",
|
||||
"STATE-WP-0059",
|
||||
"STATE-WP-0060",
|
||||
"STATE-WP-0061",
|
||||
"STATE-WP-0062",
|
||||
"STATE-WP-0063",
|
||||
"STATE-WP-0064",
|
||||
"STATE-WP-0065",
|
||||
"STATE-WP-0066",
|
||||
"STATE-WP-0067",
|
||||
"STATE-WP-0068",
|
||||
"STATE-WP-0069",
|
||||
"STATE-WP-0070",
|
||||
"STATE-WP-0071",
|
||||
"STATE-WP-0072",
|
||||
"STATE-WP-0073",
|
||||
"STATE-WP-0074",
|
||||
"STATE-WP-0075",
|
||||
"STATE-WP-0076",
|
||||
"STATE-WP-0077",
|
||||
"STATE-WP-0078",
|
||||
"STATE-WP-0079",
|
||||
"STATE-WP-0080",
|
||||
"STATE-WP-0081",
|
||||
"STATE-WP-0082",
|
||||
"STATE-WP-0083",
|
||||
"STATE-WP-0084",
|
||||
"STATE-WP-0085",
|
||||
"STATE-WP-0086",
|
||||
"STATE-WP-0087",
|
||||
"STATE-WP-0088",
|
||||
"STATE-WP-0089",
|
||||
"STATE-WP-0090",
|
||||
"STATE-WP-0091",
|
||||
"STATE-WP-0092",
|
||||
"STATE-WP-0093",
|
||||
"STATE-WP-0094",
|
||||
"STATE-WP-0095",
|
||||
"STATE-WP-0096",
|
||||
"STATE-WP-0097",
|
||||
"STATE-WP-0098",
|
||||
"STATE-WP-ADHOC-2026-06-04",
|
||||
"STATE-WP-ADHOC-2026-07-01",
|
||||
"STATE-WP-ADHOC-2026-08-08",
|
||||
"STATE-WP-ADHOC-2026-08-23",
|
||||
"STATE-WP-ADHOC-2026-08-30",
|
||||
"STATE-WP-ADHOC-2026-09-04",
|
||||
"STATE-WP-ADHOC-2026-09-22",
|
||||
"STATE-WP-ADHOC-2026-10-06",
|
||||
"STATE-WP-ADHOC-2026-10-13"
|
||||
],
|
||||
"retired": [],
|
||||
"released": [],
|
||||
"created_tasks": [],
|
||||
"updated_tasks": [
|
||||
"STATE-WP-0095-T10"
|
||||
],
|
||||
"cancelled_tasks": [],
|
||||
"created_edges": [],
|
||||
"removed_edges": [],
|
||||
"unresolved_edges": [],
|
||||
"external_edges": [
|
||||
{
|
||||
"workplan": "STATE-WP-0098",
|
||||
"from": "STATE-WP-0098-T04",
|
||||
"target": "HUB-WP-0011-T01",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "0c68970e-295b-57fc-ac70-e472ee3a7bc4",
|
||||
"from_task_id": "077797c8-289b-55ec-9597-28f223891b02",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "588e4b58-4694-513c-8e7a-1fab38fb6ce3"
|
||||
},
|
||||
{
|
||||
"workplan": "CUST-WP-0038",
|
||||
"from": "CUST-WP-0038",
|
||||
"target": "CUST-WP-0011",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "ccb87fa4-6381-5f22-b097-6e026f56a9c1",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "967baafb-d92d-405a-ba0b-0d00d37c4940",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T04",
|
||||
"target": "HUB-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "3a454438-1955-547f-a491-8eab7cd25710",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "61c727a5-f4b1-54c0-b634-7d71a5416496"
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T04",
|
||||
"target": "HUB-WP-0006-T06",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "3a454438-1955-547f-a491-8eab7cd25710",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "7c8225f7-03dc-57b0-a668-c3d781bd828a"
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T06",
|
||||
"target": "STATE-WP-0079-T04",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "9230c99b-af58-5794-9291-452e619a30fa",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "3a454438-1955-547f-a491-8eab7cd25710"
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T06",
|
||||
"target": "STATE-WP-0079-T05",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "9230c99b-af58-5794-9291-452e619a30fa",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "f6cf8c01-eb59-5ca8-a541-23928d4a000c"
|
||||
}
|
||||
],
|
||||
"refused": [],
|
||||
"notes": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from pathlib import Path
|
||||
import subprocess,json
|
||||
p=Path(__file__).resolve().parent
|
||||
r=subprocess.run(['make','sync','SYNC_PATH=.','SYNC_PUSH=1','STATE_HUB_API_BASE=http://127.0.0.1:8000'],cwd='/home/worsch/state-hub',capture_output=True,text=True,timeout=180)
|
||||
try:d=json.loads(r.stdout[r.stdout.index('{'):])
|
||||
except Exception:d={'status':'unverified','reason':'normal_sync_receipt_unparseable','exit':r.returncode}
|
||||
(p/'normal-sync-result.json').write_text(json.dumps(d,indent=2)+'\n')
|
||||
receipt=d.get('receipt',{})
|
||||
print(json.dumps({'exit':r.returncode,'status':d.get('status'),'commit':d.get('commit'),'instance_role':receipt.get('instance_role'),'instance_label':receipt.get('instance_label'),'expected_commit':receipt.get('expected_commit'),'derived_commit':receipt.get('derived_commit'),'identifier_assignments':d.get('identifiers',{}).get('assignments')}))
|
||||
raise SystemExit(r.returncode)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from pathlib import Path
|
||||
import importlib.util,json
|
||||
p=Path(__file__).resolve().parent
|
||||
spec=importlib.util.spec_from_file_location('pub',p.parent/'owner-followthrough-r5/publish_followthrough_source.py');pub=importlib.util.module_from_spec(spec);spec.loader.exec_module(pub)
|
||||
expected=json.loads((p/'manifest.json').read_text())
|
||||
r=pub.commit_source(Path('/home/worsch/state-hub'),expected,pub.SCANNER,execute=True,push=False)
|
||||
(p/'source-commit-result.json').write_text(json.dumps(r,indent=2)+'\n')
|
||||
print(json.dumps({k:v for k,v in r.items() if k not in {'preserved_before','preserved_after'}}))
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"schema": "state95.writeback-publication-readback.v1",
|
||||
"status": "applied",
|
||||
"source_revision": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"remote_revision": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"derived_commit": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"checker_sha256": "sha256:1bfbc89d3ff5c621ec541fd32384083cc7dcf96a639b217d62b6fc34de238efd",
|
||||
"canonical_tests_passed": 5,
|
||||
"tasks": [
|
||||
{
|
||||
"uuid": "6a5f87bb-e63d-5e20-b9fc-c1b5cd403ff7",
|
||||
"record_id": "STATE-WP-0095-T01",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "ebb24194-e4cc-5130-aaa6-663f2ea2c822",
|
||||
"record_id": "STATE-WP-0095-T02",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "e8566341-cf6e-5b9c-ad80-8564d14cd52a",
|
||||
"record_id": "STATE-WP-0095-T03",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "3bbb311f-194e-5862-8aed-358a580f22b4",
|
||||
"record_id": "STATE-WP-0095-T04",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "339b9a37-181c-5599-9a89-0393e199bd4b",
|
||||
"record_id": "STATE-WP-0095-T05",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "6af973b4-1964-5d4d-a2f8-8dbd4c01fda4",
|
||||
"record_id": "STATE-WP-0095-T06",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
|
||||
"record_id": "STATE-WP-0095-T07",
|
||||
"status": "wait",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "2dd9b04f-2250-5a8a-b5bc-c8a009872858",
|
||||
"record_id": "STATE-WP-0095-T08",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "8be8f13b-6352-5295-9755-de6fbfb13593",
|
||||
"record_id": "STATE-WP-0095-T09",
|
||||
"status": "done",
|
||||
"needs_human": false
|
||||
},
|
||||
{
|
||||
"uuid": "88585c69-d9b9-5e1f-a256-6416ff71e824",
|
||||
"record_id": "STATE-WP-0095-T10",
|
||||
"status": "wait",
|
||||
"needs_human": true
|
||||
}
|
||||
],
|
||||
"unrelated_whole_index_dirty_preserved": true,
|
||||
"new_UUIDs": false,
|
||||
"runtime_changed": false,
|
||||
"T07": "wait",
|
||||
"T10": "wait_needs_human"
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
--- a/scripts/consistency_check.py
|
||||
+++ b/scripts/consistency_check.py
|
||||
@@ -2767,11 +2767,12 @@
|
||||
sync_env = {**_os.environ, "GIT_CUSTODIAN_SYNC": "1"}
|
||||
try:
|
||||
subprocess.run(
|
||||
- ["git", "-C", repo_path, "add", str(file_path)],
|
||||
+ ["git", "-C", repo_path, "add", "--", str(file_path)],
|
||||
check=True, capture_output=True, env=sync_env,
|
||||
)
|
||||
subprocess.run(
|
||||
- ["git", "-C", repo_path, *_git_identity_config(), "commit", "-m", msg],
|
||||
+ ["git", "-C", repo_path, *_git_identity_config(),
|
||||
+ "commit", "--only", "-m", msg, "--", str(file_path)],
|
||||
check=True, capture_output=True, env=sync_env,
|
||||
)
|
||||
return True
|
||||
--- /dev/null
|
||||
+++ b/tests/test_writeback_git_isolated.py
|
||||
@@ -0,0 +1,60 @@
|
||||
+"""Real disposable Git proof; does not import owner app or test conftest."""
|
||||
+import ast
|
||||
+from pathlib import Path
|
||||
+import subprocess
|
||||
+import sys
|
||||
+import pytest
|
||||
+
|
||||
+SOURCE=Path(__file__).resolve().parents[1]/'scripts/consistency_check.py'
|
||||
+def actual_writeback():
|
||||
+ tree=ast.parse(SOURCE.read_text());nodes=[]
|
||||
+ for node in tree.body:
|
||||
+ if isinstance(node,ast.FunctionDef) and node.name in {'_git_identity_config','_git_commit_writeback'}:
|
||||
+ node.returns=None
|
||||
+ for arg in node.args.args+node.args.kwonlyargs:arg.annotation=None
|
||||
+ nodes.append(node)
|
||||
+ env=dict(subprocess=subprocess,sys=sys,Path=Path)
|
||||
+ exec(compile(ast.Module(body=nodes,type_ignores=[]),str(SOURCE),'exec'),env)
|
||||
+ return env['_git_commit_writeback']
|
||||
+def git(root,*args):return subprocess.check_output(['git','-C',str(root),*args]).decode().strip()
|
||||
+@pytest.fixture
|
||||
+def repo(tmp_path):
|
||||
+ root=tmp_path/'owner';root.mkdir();git(root,'init','-b','main');git(root,'config','user.name','Synthetic Test');git(root,'config','user.email','synthetic@example.invalid');git(root,'config','core.hooksPath','.git/hooks')
|
||||
+ for name in ['writeback.md','.custodian-brief.md','evidence.json']:(root/name).write_text('original\n')
|
||||
+ git(root,'add','.');git(root,'commit','-m','baseline')
|
||||
+ (root/'.custodian-brief.md').write_text('staged unrelated\n');git(root,'add','.custodian-brief.md');(root/'.custodian-brief.md').write_text('working unrelated\n')
|
||||
+ (root/'evidence.json').write_text('dirty evidence\n')
|
||||
+ return root
|
||||
+
|
||||
+def test_writeback_commits_only_target_preserves_unrelated_stage_and_worktree(repo):
|
||||
+ before_index=git(repo,'ls-files','--stage','.custodian-brief.md','evidence.json');before_head=git(repo,'rev-parse','HEAD')
|
||||
+ target=repo/'writeback.md';target.write_text('new status\n')
|
||||
+ assert actual_writeback()(str(repo),target,['synthetic status'])
|
||||
+ assert git(repo,'rev-parse','HEAD')!=before_head
|
||||
+ assert git(repo,'diff-tree','--no-commit-id','--name-only','-r','HEAD')=='writeback.md'
|
||||
+ assert git(repo,'show','HEAD:.custodian-brief.md')=='original'
|
||||
+ assert git(repo,'show',':.custodian-brief.md')=='staged unrelated'
|
||||
+ assert git(repo,'ls-files','--stage','.custodian-brief.md','evidence.json')==before_index
|
||||
+ assert (repo/'.custodian-brief.md').read_text()=='working unrelated\n'
|
||||
+ assert (repo/'evidence.json').read_text()=='dirty evidence\n'
|
||||
+
|
||||
+def test_noop_never_commits_unrelated_staged_change(repo):
|
||||
+ head=git(repo,'rev-parse','HEAD');index=git(repo,'ls-files','--stage')
|
||||
+ assert not actual_writeback()(str(repo),repo/'writeback.md',['no change'])
|
||||
+ assert git(repo,'rev-parse','HEAD')==head and git(repo,'ls-files','--stage')==index
|
||||
+
|
||||
+@pytest.mark.parametrize('name',['new writeback.md','-leading-dash.md'])
|
||||
+def test_new_target_and_argument_boundary(repo,name):
|
||||
+ target=repo/name;target.write_text('new file\n')
|
||||
+ assert actual_writeback()(str(repo),target,['new synthetic target'])
|
||||
+ assert git(repo,'diff-tree','--no-commit-id','--name-only','-r','HEAD')==name
|
||||
+ assert git(repo,'show',':.custodian-brief.md')=='staged unrelated'
|
||||
+
|
||||
+def test_normal_hook_refusal_preserves_unrelated_state(repo):
|
||||
+ hook=repo/'.git/hooks/pre-commit';hook.write_text('#!/bin/sh\nexit 1\n');hook.chmod(0o755)
|
||||
+ head=git(repo,'rev-parse','HEAD');index=git(repo,'ls-files','--stage','.custodian-brief.md','evidence.json')
|
||||
+ target=repo/'writeback.md';target.write_text('new status\n')
|
||||
+ assert not actual_writeback()(str(repo),target,['refused synthetic change'])
|
||||
+ assert git(repo,'rev-parse','HEAD')==head
|
||||
+ assert git(repo,'ls-files','--stage','.custodian-brief.md','evidence.json')==index
|
||||
+ assert git(repo,'show',':.custodian-brief.md')=='staged unrelated'
|
||||
--- a/workplans/STATE-WP-0095-historical-source-receiving.md
|
||||
+++ b/workplans/STATE-WP-0095-historical-source-receiving.md
|
||||
@@ -342,3 +342,18 @@
|
||||
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.
|
||||
+
|
||||
+
|
||||
+T09 source repair refinement, 2026-10-17: the attempted current-client regression
|
||||
+actually loaded an older packaged consistency checker from the owner virtualenv,
|
||||
+not the reviewed canonical source. That trial again failed parent-row hash
|
||||
+preservation; it is retained separately and does not count as R5 client proof.
|
||||
+Normal C-23 also legitimately normalized WP18 ready to active and created a
|
||||
+source commit, but the generic writeback commit included an unrelated staged
|
||||
+brief. A narrow successor scopes git add and git commit --only to the actual
|
||||
+writeback file, preserving unrelated index/worktree state. Real disposable Git
|
||||
+regression tests cover pre-existing staged changes, hooks, no-op and path names.
|
||||
+Verify the actually loaded CLI checker hash after ordinary owner package refresh
|
||||
+before any retry; capture legitimate source HEAD transitions rather than assume
|
||||
+a static revision. T07 and T10 remain wait. No original parent preimage recovery,
|
||||
+new runtime delivery or broader authority follows from this source repair.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"schema": "state95.writeback-validation.v1",
|
||||
"command": "python3 -m pytest --noconftest -q state-hub/tests/test_writeback_git_isolated.py",
|
||||
"tests_passed": 5,
|
||||
"seconds": 0.74,
|
||||
"real_disposable_git": true,
|
||||
"normal_hook_refusal_test": true,
|
||||
"fixture_refinement": "Initial hook refusal fixture inherited host global core.hooksPath; local disposable-repo hook directory now explicit. No product hook bypass.",
|
||||
"whole_unrelated_index_and_worktree_preserved": true,
|
||||
"owner_config_imported": false,
|
||||
"DB_access": false,
|
||||
"production_mutations": false,
|
||||
"canonical_source_mutations": false,
|
||||
"old_packaged_checker_trial_accepted": false,
|
||||
"T07": "wait",
|
||||
"T10": "wait_needs_human"
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"schema": "state95.client-proof-followthrough.v1",
|
||||
"base_revision": "c103cbf3c8bed5dba87a03e320595ddded9301a3",
|
||||
"files": [
|
||||
{
|
||||
"path": "workplans/STATE-WP-0095-historical-source-receiving.md",
|
||||
"base_sha256": "e386157304621ca2c174d6220fcd156e510592d43bb904c2bdbc5f73fa6a0722",
|
||||
"candidate_sha256": "df055846c34f4e4eb9dc1fcc88969efd40bfca9daad1df006f7527e9b6d6c9e7"
|
||||
}
|
||||
],
|
||||
"patch_sha256": "sha256:db6856796303234221b35c4c473822b14b591946c9283e0dfd1dd86a01a478ea",
|
||||
"new_UUIDs": false,
|
||||
"runtime_changed": false,
|
||||
"T07": "wait_depends_on_T10",
|
||||
"T09": "done",
|
||||
"T10": "wait_needs_human"
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
{
|
||||
"ok": true,
|
||||
"status": "applied",
|
||||
"repo_slug": "state-hub",
|
||||
"commit": "9eaa36143ea18111c522ea14d32978495b4e5030",
|
||||
"requests": 2,
|
||||
"elapsed_ms": 14163.6,
|
||||
"git": {
|
||||
"upstream": "origin/main",
|
||||
"behind": 0,
|
||||
"ahead": 0
|
||||
},
|
||||
"identifiers": {
|
||||
"schema": "repo-manager.identifier-assignment.v1",
|
||||
"ok": true,
|
||||
"executed": true,
|
||||
"repo": "state-hub",
|
||||
"namespace": "helixforge",
|
||||
"assignments": [],
|
||||
"historical_invalid_identifiers": [],
|
||||
"historical_identity_collisions": [],
|
||||
"files_changed": []
|
||||
},
|
||||
"receipt": {
|
||||
"schema": "state-hub.repository-projection-reconcile.v1",
|
||||
"instance_role": "primary",
|
||||
"instance_label": "railiance01",
|
||||
"expected_commit": "9eaa36143ea18111c522ea14d32978495b4e5030",
|
||||
"derived_commit": "9eaa36143ea18111c522ea14d32978495b4e5030",
|
||||
"outcome": {
|
||||
"schema": "state-hub.projection-reset.v1",
|
||||
"repo_slug": "state-hub",
|
||||
"commit": "9eaa36143ea18111c522ea14d32978495b4e5030",
|
||||
"status": "applied",
|
||||
"counts": {
|
||||
"created": 0,
|
||||
"updated": 70,
|
||||
"retired": 0,
|
||||
"refused": 0,
|
||||
"released": 0,
|
||||
"created_tasks": 0,
|
||||
"updated_tasks": 1,
|
||||
"cancelled_tasks": 0,
|
||||
"created_edges": 1,
|
||||
"removed_edges": 0,
|
||||
"unresolved_edges": 0,
|
||||
"external_edges": 6
|
||||
},
|
||||
"created": [],
|
||||
"updated": [
|
||||
"CUST-WP-0003",
|
||||
"CUST-WP-0011",
|
||||
"CUST-WP-0012",
|
||||
"CUST-WP-0038",
|
||||
"CUST-WP-0042",
|
||||
"STATE-WP-0043",
|
||||
"STATE-WP-0044",
|
||||
"STATE-WP-0045",
|
||||
"STATE-WP-0046",
|
||||
"STATE-WP-0047",
|
||||
"STATE-WP-0048",
|
||||
"STATE-WP-0049",
|
||||
"STATE-WP-0050",
|
||||
"STATE-WP-0051",
|
||||
"STATE-WP-0052",
|
||||
"STATE-WP-0053",
|
||||
"STATE-WP-0054",
|
||||
"STATE-WP-0055",
|
||||
"STATE-WP-0056",
|
||||
"STATE-WP-0057",
|
||||
"STATE-WP-0058",
|
||||
"STATE-WP-0059",
|
||||
"STATE-WP-0060",
|
||||
"STATE-WP-0061",
|
||||
"STATE-WP-0062",
|
||||
"STATE-WP-0063",
|
||||
"STATE-WP-0064",
|
||||
"STATE-WP-0065",
|
||||
"STATE-WP-0066",
|
||||
"STATE-WP-0067",
|
||||
"STATE-WP-0068",
|
||||
"STATE-WP-0069",
|
||||
"STATE-WP-0070",
|
||||
"STATE-WP-0071",
|
||||
"STATE-WP-0072",
|
||||
"STATE-WP-0073",
|
||||
"STATE-WP-0074",
|
||||
"STATE-WP-0075",
|
||||
"STATE-WP-0076",
|
||||
"STATE-WP-0077",
|
||||
"STATE-WP-0078",
|
||||
"STATE-WP-0079",
|
||||
"STATE-WP-0080",
|
||||
"STATE-WP-0081",
|
||||
"STATE-WP-0082",
|
||||
"STATE-WP-0083",
|
||||
"STATE-WP-0084",
|
||||
"STATE-WP-0085",
|
||||
"STATE-WP-0086",
|
||||
"STATE-WP-0087",
|
||||
"STATE-WP-0088",
|
||||
"STATE-WP-0089",
|
||||
"STATE-WP-0090",
|
||||
"STATE-WP-0091",
|
||||
"STATE-WP-0092",
|
||||
"STATE-WP-0093",
|
||||
"STATE-WP-0094",
|
||||
"STATE-WP-0095",
|
||||
"STATE-WP-0096",
|
||||
"STATE-WP-0097",
|
||||
"STATE-WP-0098",
|
||||
"STATE-WP-ADHOC-2026-06-04",
|
||||
"STATE-WP-ADHOC-2026-07-01",
|
||||
"STATE-WP-ADHOC-2026-08-08",
|
||||
"STATE-WP-ADHOC-2026-08-23",
|
||||
"STATE-WP-ADHOC-2026-08-30",
|
||||
"STATE-WP-ADHOC-2026-09-04",
|
||||
"STATE-WP-ADHOC-2026-09-22",
|
||||
"STATE-WP-ADHOC-2026-10-06",
|
||||
"STATE-WP-ADHOC-2026-10-13"
|
||||
],
|
||||
"retired": [],
|
||||
"released": [],
|
||||
"created_tasks": [],
|
||||
"updated_tasks": [
|
||||
"STATE-WP-0095-T10"
|
||||
],
|
||||
"cancelled_tasks": [],
|
||||
"created_edges": [
|
||||
{
|
||||
"workplan": "STATE-WP-0095",
|
||||
"from": "STATE-WP-0095-T07",
|
||||
"target": "STATE-WP-0095-T10",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "79579572-8120-5d3b-aca5-dd4aff60d058",
|
||||
"from_task_id": "cfd1ea99-e75b-542e-a434-acefb62f3c7d",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "88585c69-d9b9-5e1f-a256-6416ff71e824"
|
||||
}
|
||||
],
|
||||
"removed_edges": [],
|
||||
"unresolved_edges": [],
|
||||
"external_edges": [
|
||||
{
|
||||
"workplan": "STATE-WP-0098",
|
||||
"from": "STATE-WP-0098-T04",
|
||||
"target": "HUB-WP-0011-T01",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "0c68970e-295b-57fc-ac70-e472ee3a7bc4",
|
||||
"from_task_id": "077797c8-289b-55ec-9597-28f223891b02",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "588e4b58-4694-513c-8e7a-1fab38fb6ce3"
|
||||
},
|
||||
{
|
||||
"workplan": "CUST-WP-0038",
|
||||
"from": "CUST-WP-0038",
|
||||
"target": "CUST-WP-0011",
|
||||
"relationship_type": "blocks",
|
||||
"from_workplan_id": "ccb87fa4-6381-5f22-b097-6e026f56a9c1",
|
||||
"from_task_id": null,
|
||||
"to_workplan_id": "967baafb-d92d-405a-ba0b-0d00d37c4940",
|
||||
"to_task_id": null
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T04",
|
||||
"target": "HUB-WP-0011-T03",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "3a454438-1955-547f-a491-8eab7cd25710",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "61c727a5-f4b1-54c0-b634-7d71a5416496"
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T04",
|
||||
"target": "HUB-WP-0006-T06",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "3a454438-1955-547f-a491-8eab7cd25710",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "7c8225f7-03dc-57b0-a668-c3d781bd828a"
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T06",
|
||||
"target": "STATE-WP-0079-T04",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "9230c99b-af58-5794-9291-452e619a30fa",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "3a454438-1955-547f-a491-8eab7cd25710"
|
||||
},
|
||||
{
|
||||
"workplan": "STATE-WP-0079",
|
||||
"from": "STATE-WP-0079-T06",
|
||||
"target": "STATE-WP-0079-T05",
|
||||
"relationship_type": "starts_after",
|
||||
"from_workplan_id": "ed077b62-7048-5752-bf65-f90471f45854",
|
||||
"from_task_id": "9230c99b-af58-5794-9291-452e619a30fa",
|
||||
"to_workplan_id": null,
|
||||
"to_task_id": "f6cf8c01-eb59-5ca8-a541-23928d4a000c"
|
||||
}
|
||||
],
|
||||
"refused": [],
|
||||
"notes": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
from pathlib import Path
|
||||
import hashlib,json,importlib.util
|
||||
p=Path(__file__).resolve().parent;owner=Path('/home/worsch/state-hub')
|
||||
spec=importlib.util.spec_from_file_location('pub',p.parent/'owner-followthrough-r5/publish_followthrough_source.py');pub=importlib.util.module_from_spec(spec);spec.loader.exec_module(pub)
|
||||
d=json.loads((p/'manifest.json').read_text());e=d['files'][0];assert len(d['files'])==1
|
||||
assert pub.git(owner,'rev-parse','HEAD').stdout.decode().strip()==d['base_revision']
|
||||
assert hashlib.sha256((p/'state-hub-workplan.patch').read_bytes()).hexdigest()=='db6856796303234221b35c4c473822b14b591946c9283e0dfd1dd86a01a478ea'
|
||||
f=owner/e['path'];s=p/'state-hub'/e['path'];assert not f.is_symlink() and not s.is_symlink()
|
||||
assert hashlib.sha256(f.read_bytes()).hexdigest()==e['base_sha256']
|
||||
assert hashlib.sha256(s.read_bytes()).hexdigest()==e['candidate_sha256']
|
||||
assert not pub.git(owner,'diff','--cached','--',e['path']).stdout
|
||||
before=pub.preserved_state(owner,{e['path']});f.write_bytes(s.read_bytes());assert pub.preserved_state(owner,{e['path']})==before
|
||||
result=pub.commit_source(owner,d,pub.SCANNER,execute=True,push=False)
|
||||
assert pub.preserved_state(owner,{e['path']})==before
|
||||
(p/'source-commit-result.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
print(json.dumps({k:v for k,v in result.items() if k not in {'preserved_before','preserved_after'}}))
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from pathlib import Path
|
||||
import subprocess,json
|
||||
p=Path(__file__).resolve().parent
|
||||
r=subprocess.run(['make','sync','SYNC_PATH=.','SYNC_PUSH=1','STATE_HUB_API_BASE=http://127.0.0.1:8000'],cwd='/home/worsch/state-hub',capture_output=True,text=True,timeout=180)
|
||||
try:d=json.loads(r.stdout[r.stdout.index('{'):])
|
||||
except Exception:d={'status':'unverified','reason':'normal_sync_receipt_unparseable','exit':r.returncode}
|
||||
(p/'normal-sync-result.json').write_text(json.dumps(d,indent=2)+'\n')
|
||||
receipt=d.get('receipt',{})
|
||||
print(json.dumps({'exit':r.returncode,'status':d.get('status'),'commit':d.get('commit'),'instance_role':receipt.get('instance_role'),'instance_label':receipt.get('instance_label'),'expected_commit':receipt.get('expected_commit'),'derived_commit':receipt.get('derived_commit'),'identifier_assignments':d.get('identifiers',{}).get('assignments')}))
|
||||
raise SystemExit(r.returncode)
|
||||
Loading…
Add table
Add a link
Reference in a new issue