From 4bd07a4f44f38bb4f27cfb381cfe53d555c2c6fe Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 28 Sep 2026 11:22:56 +0200 Subject: [PATCH] Restore frontend reporting on grant-aware worker with monthly review packet Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e738-9e15-7600-87e6-90dec3a7beef --- Dockerfile | 2 +- Dockerfile.frontend-patterns | 10 +-- k8s/gitops/runtime.yaml | 2 +- k8s/railiance/20-runtime.yaml | 2 +- .../context_resolvers/frontend_patterns.py | 47 ++++++++++ tests/test_frontend_monthly_review.py | 87 +++++++++++++++++++ workplans/ACTIVITY-WP-0041-gitops-adoption.md | 23 ++++- 7 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 tests/test_frontend_monthly_review.py diff --git a/Dockerfile b/Dockerfile index a31cbc9..054ad05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml COPY schemas/ ./schemas/ COPY k8s/ ./k8s/ COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/ -RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py tests/test_release_transport.py tests/test_release_dispatch.py +RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_frontend_monthly_review.py tests/test_repository_grant.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py tests/test_release_transport.py tests/test_release_dispatch.py # Stage 2 — runtime image FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime diff --git a/Dockerfile.frontend-patterns b/Dockerfile.frontend-patterns index 4a933eb..e7a25fa 100644 --- a/Dockerfile.frontend-patterns +++ b/Dockerfile.frontend-patterns @@ -1,6 +1,6 @@ -# Bounded overlay of the deployed worker; no unrelated API/worker upgrade. -# Required local base image ID: -# sha256:cd4e924c2809f0d1d319e53a9ddd20db2e88a1b543d62e6442702c184b7842f4 -FROM activity-core:fi-publication-20260914 +# Preserve the deployed grant-aware runtime; restore only the domain resolver. +FROM forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd COPY src/activity_core/context_resolvers/frontend_patterns.py /app/src/activity_core/context_resolvers/frontend_patterns.py -COPY src/activity_core/context_resolvers/__init__.py /app/src/activity_core/context_resolvers/__init__.py +RUN printf '\nfrom activity_core.context_resolvers import frontend_patterns # noqa: F401\n' >> /app/src/activity_core/context_resolvers/__init__.py +# Image construction must prove both features coexist, without network or secrets. +RUN python -c "import activity_core.context_resolvers; from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY; from activity_core.repository_grant import RepositoryGrant; assert 'frontend-patterns' in CONTEXT_RESOLVER_REGISTRY; assert RepositoryGrant.model_validate({'version':'1','allowed_paths':['docs/'],'commit_count':{'min':1,'max':1},'publish':False}).payload()['publish'] is False" diff --git a/k8s/gitops/runtime.yaml b/k8s/gitops/runtime.yaml index 38e312f..9f2c916 100644 --- a/k8s/gitops/runtime.yaml +++ b/k8s/gitops/runtime.yaml @@ -1653,7 +1653,7 @@ spec: name: actcore-runtime-config - secretRef: name: actcore-runtime-secret - image: forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd + image: forgejo.coulomb.social/coulomb/activity-core@sha256:afd8a7af73132becba432e7b315d70f88d66676020169d5d8811641304136437 imagePullPolicy: IfNotPresent name: worker ports: diff --git a/k8s/railiance/20-runtime.yaml b/k8s/railiance/20-runtime.yaml index a616d24..97d433c 100644 --- a/k8s/railiance/20-runtime.yaml +++ b/k8s/railiance/20-runtime.yaml @@ -2200,7 +2200,7 @@ spec: name: actcore-runtime-config - secretRef: name: actcore-runtime-secret - image: forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd + image: forgejo.coulomb.social/coulomb/activity-core@sha256:afd8a7af73132becba432e7b315d70f88d66676020169d5d8811641304136437 imagePullPolicy: IfNotPresent name: worker ports: diff --git a/src/activity_core/context_resolvers/frontend_patterns.py b/src/activity_core/context_resolvers/frontend_patterns.py index 1f79d85..00a11a2 100644 --- a/src/activity_core/context_resolvers/frontend_patterns.py +++ b/src/activity_core/context_resolvers/frontend_patterns.py @@ -10,6 +10,7 @@ from contextvars import ContextVar from datetime import datetime, timezone import hashlib import json +import math from pathlib import PurePosixPath import re import time @@ -149,6 +150,44 @@ def reduce_events(rows: list[tuple[dict, str]], errors: list[str]) -> list[dict] return latest +def monthly_review(root_sha: str, now: datetime, start: datetime, end: datetime) -> dict: + """Bounded, source-pinned owner snapshot and measured research totals. + + This is review input, never authority or a live health attestation. + Missing/invalid input degrades the monthly packet while preserving receipts. + """ + data = json.loads(raw("frontend-patterns", root_sha, "feedback/monthly-review-input.json")) + require(data["schema_version"] == 1, "invalid_monthly_input") + stamp = datetime.fromisoformat(data["as_of"].replace("Z", "+00:00")) + require(stamp.tzinfo is not None and stamp <= now, "invalid_monthly_date") + health = data["health"] + keys = {"schedule", "collection", "verification", "publication", "rollback", "executor"} + require(isinstance(health, dict) and set(health) == keys, "invalid_monthly_health") + require(all(isinstance(v, str) and 0 < len(v) <= 80 for v in health.values()), "invalid_monthly_health") + require(path_ok(data["evidence_ref"]), "invalid_monthly_evidence") + # The cited file must exist at this same source revision. + raw("frontend-patterns", root_sha, data["evidence_ref"]) + decisions = data["decisions"] + require(isinstance(decisions, list) and len(decisions) <= 3, "invalid_monthly_decisions") + for decision in decisions: + require(isinstance(decision, dict) and set(decision) == {"question", "recommendation", "default"}, "invalid_monthly_decision") + require(all(isinstance(v, str) and 0 < len(v) <= 100 for v in decision.values()), "invalid_monthly_decision") + ledger = json.loads(raw("frontend-patterns", root_sha, "research/evolution/ledger.json")) + events = ledger["events"] + require(isinstance(events, list) and len(events) <= 1000, "invalid_learning_ledger") + selected = [e for e in events if start.date().isoformat() <= e["date"] < end.date().isoformat()] + totals = {} + for field in ("research_minutes", "execution_minutes", "reported_tokens", "cost_usd"): + values = [e["effort"][field] for e in selected] + require(all(v is None or (type(v) in (int, float) and math.isfinite(v) and v >= 0) for v in values), "invalid_effort") + totals[field] = {"known": round(sum(v for v in values if v is not None), 3), "unknown_cycles": sum(v is None for v in values)} + return {"basis": "owner snapshot; not live attestation", "as_of": data["as_of"], + "stale": (now - stamp).total_seconds() > 7 * 86400, + "health": health, "evidence_ref": data["evidence_ref"], "decisions": decisions, + "default": "Retain current policy/budgets; no new authority or maturity claim.", + "research_cycles": len(selected), "measured_effort": totals} + + def collect(mode: str = "daily", now: datetime | None = None) -> dict: require(mode in {"daily", "weekly", "monthly"}, "invalid_mode") now = now or datetime.now(timezone.utc) @@ -165,6 +204,7 @@ def collect(mode: str = "daily", now: datetime | None = None) -> dict: "workplan": "FEP-WP-0008", "executor_dependency": "GLAS-WP-0012 / HFACT-WP-0001", } config = {"sources": []} + root_sha = None rows = [] events_seen = set() total = 0 @@ -213,6 +253,13 @@ def collect(mode: str = "daily", now: datetime | None = None) -> dict: before = first - timedelta(days=1) start = before.replace(day=1) report["window"] = {"start": start.isoformat(), "end_exclusive": first.isoformat()} + try: + require(root_sha is not None, "missing_root_revision") + report["review"] = monthly_review(root_sha, now, start, first) + except (ValueError, KeyError, TypeError, AttributeError, OverflowError, httpx.HTTPError): + report["review"] = {"status": "unavailable", "health": "unknown", "decisions": [], + "default": "Retain current policy/budgets; no new authority or maturity claim."} + report["errors"].append("monthly_review_input_invalid_or_unavailable") # Select latest event as of the end of the month, not latest today. eligible_rows = [(e, d) for e, d in rows if datetime.fromisoformat(e["recorded_at"].replace("Z", "+00:00")) < first] latest = reduce_events(eligible_rows, report["errors"]) diff --git a/tests/test_frontend_monthly_review.py b/tests/test_frontend_monthly_review.py new file mode 100644 index 0000000..4b61ff4 --- /dev/null +++ b/tests/test_frontend_monthly_review.py @@ -0,0 +1,87 @@ +import copy +import json +from datetime import datetime, timezone +from pathlib import Path +import pytest +from activity_core.context_resolvers import frontend_patterns as f + +NOW = datetime(2026, 10, 1, 7, tzinfo=timezone.utc) +INPUT = {'schema_version': 1, 'as_of': '2026-09-28T09:00:00Z', + 'health': {k: 'unverified' for k in ('schedule','collection','verification','publication','rollback','executor')}, + 'evidence_ref': 'docs/evidence/report.json', 'decisions': []} +EVENT = {'date': '2026-09-28', 'effort': {'research_minutes': 4, 'execution_minutes': 1, 'reported_tokens': None, 'cost_usd': None}} + + +def setup(monkeypatch, data=None, events=None, unavailable=False): + data = copy.deepcopy(INPUT if data is None else data) + events = [EVENT] if events is None else events + def raw(repo, sha, path): + assert repo == 'frontend-patterns' and sha == 'a'*40 + if path == 'feedback/sources.json': return b'{"schema_version":1,"sources":[]}' + if path == 'feedback/monthly-review-input.json': + if unavailable: raise ValueError('unavailable') + return json.dumps(data).encode() + if path == 'research/evolution/ledger.json': return json.dumps({'events': events}).encode() + if path == 'docs/evidence/report.json': return b'{}' + raise ValueError('unexpected or missing file') + monkeypatch.setattr(f, 'raw', raw) + monkeypatch.setattr(f, 'revision', lambda *args: 'a'*40) + + +def test_october_packet_with_no_consumers_retains_health_and_unknown_cost(monkeypatch): + setup(monkeypatch) + r=f.collect('monthly', NOW) + assert r['status']=='no_consumers' and r['model_calls']==0 + assert r['window']=={'start':'2026-09-01T00:00:00+02:00','end_exclusive':'2026-10-01T00:00:00+02:00'} + assert r['review']['health']['executor']=='unverified' + assert r['review']['measured_effort']['cost_usd']=={'known':0,'unknown_cycles':1} + assert r['review']['measured_effort']['research_minutes']=={'known':4,'unknown_cycles':0} + assert r['execution_ready'] is False + assert len(json.dumps(r,sort_keys=True,separators=(',',':'))) <= 3900 + + +def test_incomplete_input_degrades_without_losing_receipt_summary(monkeypatch): + setup(monkeypatch, unavailable=True) + r=f.collect('monthly', NOW) + assert r['status']=='degraded' and r['real_uses']==0 + assert r['review']['health']=='unknown' and r['review']['decisions']==[] + + +@pytest.mark.parametrize('change', [ + {'as_of':'2026-10-02T00:00:00Z'}, {'as_of':'2026-09-28T00:00:00'}, + {'evidence_ref':'../secret'}, {'decisions':[{}]*4}, + {'health':{'executor':'ready'}}, {'decisions':[{'question':'x','recommendation':'y'}]}, + {'evidence_ref':'docs/evidence/missing.json'}, +]) +def test_invalid_review_input_never_claims_health(monkeypatch, change): + data=copy.deepcopy(INPUT); data.update(change); setup(monkeypatch,data) + r=f.collect('monthly', NOW) + assert r['status']=='degraded' and r['review']['health']=='unknown' + + +def test_month_filter_and_missing_effort(monkeypatch): + old=copy.deepcopy(EVENT); old['date']='2026-08-31' + future=copy.deepcopy(EVENT); future['date']='2026-10-01' + unknown=copy.deepcopy(EVENT); unknown['effort']['research_minutes']=None + setup(monkeypatch, events=[old,future,EVENT,unknown]) + r=f.collect('monthly', NOW)['review'] + assert r['research_cycles']==2 + assert r['measured_effort']['research_minutes']=={'known':4,'unknown_cycles':1} + + +def test_stale_owner_snapshot_is_not_live_health(monkeypatch): + data=copy.deepcopy(INPUT); data['as_of']='2026-09-01T00:00:00Z'; setup(monkeypatch,data) + r=f.collect('monthly',NOW)['review'] + assert r['stale'] is True and 'not live attestation' in r['basis'] + + +@pytest.mark.parametrize('value', [-1, float('nan'), True]) +def test_invalid_effort_does_not_invent_totals(monkeypatch, value): + event=copy.deepcopy(EVENT); event['effort']['cost_usd']=value + setup(monkeypatch, events=[event]) + assert f.collect('monthly', NOW)['status']=='degraded' + + +def test_daily_does_not_require_monthly_packet(monkeypatch): + setup(monkeypatch, unavailable=True) + assert f.collect('daily', NOW)['status']=='no_consumers' diff --git a/workplans/ACTIVITY-WP-0041-gitops-adoption.md b/workplans/ACTIVITY-WP-0041-gitops-adoption.md index b8a30fa..818ac8d 100644 --- a/workplans/ACTIVITY-WP-0041-gitops-adoption.md +++ b/workplans/ACTIVITY-WP-0041-gitops-adoption.md @@ -8,7 +8,7 @@ status: blocked owner: codex topic_slug: activity-core created: "2026-09-27" -updated: "2026-09-27" +updated: "2026-09-28" state_hub_workstream_id: "12798750-3eee-5d81-a411-c3c4c5ce2d2c" --- @@ -217,3 +217,24 @@ dispatcher and audit sink. Local code cannot supply those trust inputs. The new worker factory neither enables a schedule nor grants an identity; deployment and continuous health observation must be established through the existing owner lane. No new workplan/task, production mutation or authority expansion was introduced. + +## Attended frontend reporting repair — 2026-09-28 + +The grant-aware image promotion at `428f2d7` removed the frontend resolver. The +September 28 natural daily run fired but failed required-context resolution. +The user approved exact worker digest `afd8a7af73132becba432e7b315d70f88d66676020169d5d8811641304136437` +through GitOps (State Hub decision `abdea390-1039-4da1-b129-33aad00b4396`). +The overlay retains base `713bddad…` and all other Python source hashes/grant identity; +only the domain resolver and registration change. It also includes bounded monthly +owner-snapshot health/decisions and measured effort with unknown counts. No automatic +pattern execution or standing release authority is granted. + +58 focused tests, 17 GitOps tests, built-image October fixture, published digest +readback and server dry-run pass. Source and generated projection change only the +worker image; API/router and other resource fields stay unchanged. Rollback target: +`428f2d71b0f5ade11457e16d7b6341f571f6735f` (preserves grants but restores reporting +failure). The healthy interval is invalidated; restart it only after repair proof. +FEP-WP-0008 T03 retains integration, real use and October production packet; this +plan's T02/T03 remain wait for their original standing-authority/observation gates. +Repair provenance: frontend-patterns `a18d83f`, integrations/activity-core/ and +its docs/evidence/2026-09-28-reporting-* receipts. No new task/workplan.