diff --git a/Dockerfile.frontend-patterns b/Dockerfile.frontend-patterns new file mode 100644 index 0000000..4a933eb --- /dev/null +++ b/Dockerfile.frontend-patterns @@ -0,0 +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 +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 diff --git a/k8s/railiance/20-runtime.yaml b/k8s/railiance/20-runtime.yaml index f3432f3..bfa82cb 100644 --- a/k8s/railiance/20-runtime.yaml +++ b/k8s/railiance/20-runtime.yaml @@ -51,6 +51,139 @@ metadata: app.kubernetes.io/name: activity-core app.kubernetes.io/part-of: activity-core data: + frontend-patterns-daily.md: | + --- + id: frontend-patterns-daily + name: Frontend Patterns Daily Review + enabled: false + owner: frontend-patterns + governance: custodian + status: proposed + trigger: + type: cron + cron_expression: "0 2 * * *" + timezone: Europe/Berlin + misfire_policy: catchup_latest + catchup_window_seconds: 86400 + context_sources: + - type: frontend-patterns + query: daily + params: {required: true} + bind_to: context.daily_triage_digest + --- + + # Frontend patterns daily review + + Domain-owned by FEP-WP-0008. Read-only bounded receipt collection and deterministic + reporting; no LLM calls or repository mutations. Source collection is pinned to Git + revisions. Outcome assertions remain unverified. The weekly report explicitly carries + the blocked executor dependency; it does not emit unexecutable ops_run work or claim + an improvement has happened. Enable after live report-sink proof. + + ```instruction + id: frontend-patterns-daily-report + trusted_fields: [] + model: deterministic + temperature: 0 + max_tokens: 1 + prompt: Deterministic frontend-patterns receipt and execution-readiness digest. + output_schema: "" + review_advisory: false + report_sinks: + - type: state-hub-progress + event_type: fep_feedback_intake + author: activity-core + topic_id: fe2aaa78-9c20-4feb-b3d2-4fe0529572a3 + ``` + frontend-patterns-monthly.md: | + --- + id: frontend-patterns-monthly + name: Frontend Patterns Monthly Review + enabled: false + owner: frontend-patterns + governance: custodian + status: proposed + trigger: + type: cron + cron_expression: "0 9 1 * *" + timezone: Europe/Berlin + misfire_policy: catchup_latest + catchup_window_seconds: 86400 + context_sources: + - type: frontend-patterns + query: monthly + params: {required: true} + bind_to: context.daily_triage_digest + --- + + # Frontend patterns monthly review + + Domain-owned by FEP-WP-0008. Read-only bounded receipt collection and deterministic + reporting; no LLM calls or repository mutations. Source collection is pinned to Git + revisions. Outcome assertions remain unverified. The weekly report explicitly carries + the blocked executor dependency; it does not emit unexecutable ops_run work or claim + an improvement has happened. Enable after live report-sink proof. + + ```instruction + id: frontend-patterns-monthly-report + trusted_fields: [] + model: deterministic + temperature: 0 + max_tokens: 1 + prompt: Deterministic frontend-patterns receipt and execution-readiness digest. + output_schema: "" + review_advisory: false + report_sinks: + - type: state-hub-progress + event_type: fep_monthly_review + author: activity-core + topic_id: fe2aaa78-9c20-4feb-b3d2-4fe0529572a3 + ``` + frontend-patterns-weekly.md: | + --- + id: frontend-patterns-weekly + name: Frontend Patterns Weekly Review + enabled: false + owner: frontend-patterns + governance: custodian + status: proposed + trigger: + type: cron + cron_expression: "0 3 * * 2" + timezone: Europe/Berlin + misfire_policy: catchup_latest + catchup_window_seconds: 86400 + context_sources: + - type: frontend-patterns + query: weekly + params: {required: true} + bind_to: context.daily_triage_digest + --- + + # Frontend patterns weekly review + + Domain-owned by FEP-WP-0008. Read-only bounded receipt collection and deterministic + reporting; no LLM calls or repository mutations. Source collection is pinned to Git + revisions. Outcome assertions remain unverified. The weekly report explicitly carries + the blocked executor dependency; it does not emit unexecutable ops_run work or claim + an improvement has happened. Enable after live report-sink proof. + + ```instruction + id: frontend-patterns-weekly-report + trusted_fields: [] + model: deterministic + temperature: 0 + max_tokens: 1 + prompt: Deterministic frontend-patterns receipt and execution-readiness digest. + output_schema: "" + review_advisory: false + report_sinks: + - type: state-hub-progress + event_type: fep_improvement_review + author: activity-core + topic_id: fe2aaa78-9c20-4feb-b3d2-4fe0529572a3 + ``` + weekly-forgejo-package-prune.md: | --- id: weekly-forgejo-package-prune @@ -1408,7 +1541,7 @@ spec: fsGroup: 1000 containers: - name: worker - image: activity-core:fi-publication-20260914 + image: activity-core:fep-feedback-20260927 imagePullPolicy: Never command: ["python", "-m", "activity_core.worker"] ports: diff --git a/src/activity_core/context_resolvers/__init__.py b/src/activity_core/context_resolvers/__init__.py index 8911ac7..35fb7bf 100644 --- a/src/activity_core/context_resolvers/__init__.py +++ b/src/activity_core/context_resolvers/__init__.py @@ -1,5 +1,6 @@ from activity_core.context_resolvers import ( # noqa: F401 core_hub, + frontend_patterns, event_payload, kaizen, ops_inventory, diff --git a/src/activity_core/context_resolvers/frontend_patterns.py b/src/activity_core/context_resolvers/frontend_patterns.py new file mode 100644 index 0000000..1f79d85 --- /dev/null +++ b/src/activity_core/context_resolvers/frontend_patterns.py @@ -0,0 +1,254 @@ +"""Read-only, bounded collection for the frontend-patterns reference loop. + +Returns a compact JSON digest for the existing deterministic report instruction. +Never executes feedback, invokes a model, or authorizes repository mutation. +""" +from __future__ import annotations + +from collections import Counter, defaultdict +from contextvars import ContextVar +from datetime import datetime, timezone +import hashlib +import json +from pathlib import PurePosixPath +import re +import time +from typing import Any +from zoneinfo import ZoneInfo + +import httpx +import yaml + +from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY, ContextResolver + +HOST = "https://forgejo.coulomb.social" +TOPIC = "fe2aaa78-9c20-4feb-b3d2-4fe0529572a3" +SHA = re.compile(r"[0-9a-f]{40}") +DIGEST = re.compile(r"[0-9a-f]{64}") +SLUG = re.compile(r"[a-z0-9][a-z0-9-]{0,79}") +ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9:_.-]{0,159}") +MAX_BYTES = 131072 +MAX_RECEIPTS = 100 +DEADLINE: ContextVar[float | None] = ContextVar("frontend_patterns_deadline", default=None) + + +def require(ok: Any, message: str) -> None: + if not ok: + raise ValueError(message) + + +def path_ok(value: Any) -> bool: + return (isinstance(value, str) and bool(value) and len(value) <= 240 + and not PurePosixPath(value).is_absolute() + and all(part not in {"", ".", ".."} for part in value.split("/")) + and re.fullmatch(r"[A-Za-z0-9_./-]+", value) is not None) + + +def fetch(url: str) -> bytes: + # Callers construct only fixed-host URLs from validated slug/SHA/path parts. + deadline = DEADLINE.get() + remaining = deadline - time.monotonic() if deadline is not None else 15 + require(remaining > 0, "collection_time_limit") + with httpx.stream("GET", url, timeout=min(5, remaining), follow_redirects=False) as response: + response.raise_for_status() + body = bytearray() + for part in response.iter_bytes(): + require(deadline is None or time.monotonic() < deadline, "collection_time_limit") + body.extend(part) + require(len(body) <= MAX_BYTES, "response_size_limit") + return bytes(body) + + +def revision(repo: str, branch: str) -> str: + require(SLUG.fullmatch(repo) and SLUG.fullmatch(branch), "invalid_repository_or_branch") + data = json.loads(fetch(f"{HOST}/api/v1/repos/coulomb/{repo}/branches/{branch}")) + sha = data["commit"]["id"] + require(isinstance(sha, str) and SHA.fullmatch(sha), "invalid_revision") + return sha + + +def raw(repo: str, sha: str, path: str) -> bytes: + require(SLUG.fullmatch(repo) and SHA.fullmatch(sha) and path_ok(path), "invalid_source_path") + return fetch(f"{HOST}/coulomb/{repo}/raw/commit/{sha}/{path}") + + +def validate_event(event: Any, repo: str) -> None: + require(isinstance(event, dict) and event.get("schema_version") == 1, "invalid_event_schema") + for field in ("use_id", "event_id"): + require(isinstance(event.get(field), str) and ID.fullmatch(event[field]), "invalid_identity") + require(event.get("kind") in {"real", "synthetic"}, "invalid_kind") + previous = event.get("previous_event_sha256") + require(previous is None or isinstance(previous, str) and DIGEST.fullmatch(previous), "invalid_predecessor") + stamp = datetime.fromisoformat(event["recorded_at"].replace("Z", "+00:00")) + require(stamp.tzinfo is not None, "timestamp_requires_timezone") + require(stamp <= datetime.now(timezone.utc), "future_event") + consumer = event["consumer"] + require(consumer["repo"] == repo and SHA.fullmatch(consumer["revision"]), "consumer_mismatch") + require(all(isinstance(consumer[k], str) and consumer[k] for k in ("task_ref", "producer", "run_ref")), "missing_provenance") + reference = event["reference"] + require(SHA.fullmatch(reference["packaging_revision"]) and DIGEST.fullmatch(reference["manifest_sha256"]), "invalid_reference_pin") + require(isinstance(reference["version"], str) and reference["version"], "missing_reference_version") + context = event["context"] + require(isinstance(context["problem"], str) and 0 < len(context["problem"]) <= 1000, "invalid_problem") + require(isinstance(context["constraints"], list) and all(isinstance(x, str) and len(x) <= 500 for x in context["constraints"]), "invalid_constraints") + for field in ("considered", "selected"): + values = context[field] + require(isinstance(values, list) and len(values) <= 17 and len(set(values)) == len(values), "invalid_candidates") + require(all(isinstance(x, str) and re.fullmatch(r"CAND-\d{4}", x) and 1 <= int(x[5:]) <= 17 for x in values), "unknown_candidate") + require(set(context["selected"]) <= set(context["considered"]), "unconsidered_selection") + action = event["decision"]["action"] + require(action in {"applied", "adapted", "rejected", "no-match"}, "invalid_action") + require(bool(context["selected"]) == (action in {"applied", "adapted"}), "action_selection_mismatch") + require(isinstance(event["decision"]["reason"], str) and event["decision"]["reason"], "missing_decision_reason") + outcome = event["outcome"] + require(outcome["value"] in {"helpful", "harmful", "mixed", "no-effect", "unknown"}, "invalid_outcome") + require(outcome["basis"] in {"consumer-report", "model-assessment", "observed-result"}, "invalid_basis") + require(outcome["stage"] in {"decision", "implementation", "test", "operation"}, "invalid_stage") + require(isinstance(outcome["expectation"], str) and outcome["expectation"], "missing_expectation") + refs = outcome["evidence_refs"] + require(isinstance(refs, list) and len(refs) <= 10 and all(isinstance(x, str) and 0 < len(x) <= 1000 for x in refs), "invalid_evidence") + require(outcome["value"] == "unknown" or bool(refs) and isinstance(outcome["observation"], str) and bool(outcome["observation"]), "unevidenced_outcome") + + +def reduce_events(rows: list[tuple[dict, str]], errors: list[str]) -> list[dict]: + # A use is quarantined as a whole on identity collision or ambiguous lineage. + by_use: dict[str, dict[str, dict]] = defaultdict(dict) + identities: dict[tuple[str, str], tuple[str, str]] = {} + bad = set() + for event, digest in rows: + use = event["use_id"] + key = (event["consumer"]["repo"], event["event_id"]) + if key in identities and identities[key] != (use, digest): + bad.update((use, identities[key][0])) + identities[key] = (use, digest) + by_use[use][digest] = event + latest = [] + for use, nodes in by_use.items(): + roots = [d for d, e in nodes.items() if e["previous_event_sha256"] is None] + seen = set() + current = roots[0] if len(roots) == 1 else None + previous_event = None + while current is not None and current not in seen: + seen.add(current) + event = nodes[current] + if previous_event is not None: + same = all(event[k] == previous_event[k] for k in ("kind", "reference", "context", "decision")) + same = same and event["consumer"]["repo"] == previous_event["consumer"]["repo"] + newer = datetime.fromisoformat(event["recorded_at"].replace("Z", "+00:00")) >= datetime.fromisoformat(previous_event["recorded_at"].replace("Z", "+00:00")) + if not same or not newer: + bad.add(use) + children = [d for d, e in nodes.items() if e["previous_event_sha256"] == current] + if len(children) > 1: + bad.add(use) + previous_event = event + current = children[0] if len(children) == 1 else None + if len(seen) != len(nodes) or use in bad: + errors.append("quarantined_event_lineage") + elif previous_event is not None: + latest.append(previous_event) + return latest + + +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) + report: dict[str, Any] = { + "repo": "frontend-patterns", "mode": mode, "generated_at": now.isoformat(), + "status": "ok", "execution_ready": False, "model_calls": 0, + "eligible_tasks": None, "instrumented_repositories": 0, + "source_revisions": [], "receipt_events": 0, "duplicates": 0, + "real_uses": 0, "synthetic_uses": 0, "outcomes": {}, "decisions": {}, + "signals": [], "errors": [], + "limits": ["Producer outcomes are not independently verified.", + "No admitted budget-enforced repository executor; automatic edits disabled.", + "Missing consumer instrumentation means coverage is unknown."], + "workplan": "FEP-WP-0008", "executor_dependency": "GLAS-WP-0012 / HFACT-WP-0001", + } + config = {"sources": []} + rows = [] + events_seen = set() + total = 0 + try: + root_sha = revision("frontend-patterns", "main") + report["source_revisions"].append("frontend-patterns@" + root_sha) + config = json.loads(raw("frontend-patterns", root_sha, "feedback/sources.json")) + require(config["schema_version"] == 1 and isinstance(config["sources"], list) and len(config["sources"]) <= 10, "invalid_source_roster") + source_ids = [s["repo"] for s in config["sources"]] + require(len(set(source_ids)) == len(source_ids), "duplicate_source") + for source in config["sources"]: + repo = source["repo"] + try: + sha = revision(repo, source["branch"]) + report["source_revisions"].append(repo + "@" + sha) + index = json.loads(raw(repo, sha, source["index"])) + require(index["schema_version"] == 1 and isinstance(index["receipts"], list), "invalid_receipt_index") + total += len(index["receipts"]) + require(total <= MAX_RECEIPTS, "collection_limit_exceeded") + report["instrumented_repositories"] += 1 + for item in index["receipts"]: + try: + require(isinstance(item["sha256"], str) and DIGEST.fullmatch(item["sha256"]), "invalid_digest") + data = raw(repo, sha, item["path"]) + digest = hashlib.sha256(data).hexdigest() + require(digest == item["sha256"], "receipt_digest_mismatch") + event = yaml.safe_load(data) + validate_event(event, repo) + if digest in events_seen: + report["duplicates"] += 1 + continue + events_seen.add(digest) + rows.append((event, digest)) + except (ValueError, KeyError, TypeError, AttributeError, yaml.YAMLError, httpx.HTTPError): + report["errors"].append("receipt_invalid_or_unavailable") + except (ValueError, KeyError, TypeError, AttributeError, httpx.HTTPError): + report["errors"].append("consumer_source_invalid_or_unavailable") + except (ValueError, KeyError, TypeError, AttributeError, httpx.HTTPError): + report["errors"].append("roster_invalid_or_unavailable") + report["receipt_events"] = len(rows) + latest = reduce_events(rows, report["errors"]) + local = now.astimezone(ZoneInfo("Europe/Berlin")) + first = local.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + if mode == "monthly": + from datetime import timedelta + before = first - timedelta(days=1) + start = before.replace(day=1) + report["window"] = {"start": start.isoformat(), "end_exclusive": first.isoformat()} + # 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"]) + latest = [e for e in latest if datetime.fromisoformat(e["recorded_at"].replace("Z", "+00:00")) >= start] + report["synthetic_uses"] = sum(e["kind"] == "synthetic" for e in latest) + real = [e for e in latest if e["kind"] == "real"] + report["real_uses"] = len(real) + report["outcomes"] = dict(Counter(e["outcome"]["value"] for e in real)) + report["decisions"] = dict(Counter(e["decision"]["action"] for e in real)) + signals = [] + for e in real: + value = e["outcome"]["value"] + if value in {"harmful", "mixed", "helpful"}: + signals.append({"use_id": e["use_id"], "outcome": value, "verification_required": True}) + signals.sort(key=lambda s: ({"harmful": 0, "mixed": 1, "helpful": 2}[s["outcome"]], s["use_id"])) + report["signals"] = signals[:3] + report["unselected_signals"] = max(0, len(signals) - 3) + report["errors"] = dict(Counter(report["errors"])) + if report["errors"]: + report["status"] = "degraded" + elif not config["sources"]: + report["status"] = "no_consumers" + report["summary"] = f"Frontend patterns {mode}: {report['status']}; {len(real)} real uses, {len(signals)} unverified outcome signals. Automatic edits disabled." + return report + + +class FrontendPatternsResolver(ContextResolver): + def resolve(self, query: str, event: Any, params: dict[str, Any]) -> str: + token = DEADLINE.set(time.monotonic() + 45) + try: + report = collect(query) + finally: + DEADLINE.reset(token) + encoded = json.dumps(report, sort_keys=True, separators=(",", ":")) + require(len(encoded) <= 3900, "digest_size_limit") + return encoded + + +CONTEXT_RESOLVER_REGISTRY["frontend-patterns"] = FrontendPatternsResolver diff --git a/tests/test_frontend_patterns.py b/tests/test_frontend_patterns.py new file mode 100644 index 0000000..a29b354 --- /dev/null +++ b/tests/test_frontend_patterns.py @@ -0,0 +1,122 @@ +import copy +import hashlib +import json +from datetime import datetime, timezone +from pathlib import Path +import pytest +import yaml +from activity_core.context_resolvers import frontend_patterns as f +from activity_core.definition_parser import parse_file + + +def event(kind='real'): + return dict(schema_version=1, use_id='demo:task:decision', event_id='first', + previous_event_sha256=None, recorded_at='2026-08-20T12:00:00Z', kind=kind, + consumer=dict(repo='demo', revision='a'*40, task_ref='workplans/task', producer='agent', run_ref='run:1'), + reference=dict(version='0.1', packaging_revision='b'*40, manifest_sha256='c'*64), + context=dict(problem='Choose frontend boundaries', constraints=[], considered=['CAND-0001'], selected=['CAND-0001']), + decision=dict(action='applied', reason='Fits the task'), + outcome=dict(value='unknown', basis='consumer-report', stage='decision', expectation='Fewer coupling defects', observation=None, evidence_refs=[])) + + +def fixtures(monkeypatch, events, duplicate=False): + blobs = [yaml.safe_dump(e).encode() for e in events] + entries = [dict(path=f'receipts/{i}.yaml', sha256=hashlib.sha256(b).hexdigest()) for i,b in enumerate(blobs)] + if duplicate and entries: + entries.append(entries[0]) + def raw(repo, sha, path): + if repo == 'frontend-patterns': + return json.dumps(dict(schema_version=1,sources=[dict(repo='demo', branch='main', index='receipts/index.json')])).encode() + if path.endswith('index.json'): + return json.dumps(dict(schema_version=1,receipts=entries)).encode() + return blobs[int(path.split('/')[-1].split('.')[0])] + monkeypatch.setattr(f,'revision',lambda *args: 'd'*40) + monkeypatch.setattr(f,'raw',raw) + return entries + + +def test_unknown_real_duplicate_and_synthetic_separate(monkeypatch): + synthetic = event('synthetic'); synthetic['use_id']='fixture:task:decision'; synthetic['event_id']='fixture' + fixtures(monkeypatch,[event(),synthetic],True) + report=f.collect() + assert report['real_uses']==1 and report['synthetic_uses']==1 + assert report['duplicates']==1 and report['outcomes']=={'unknown':1} + assert report['model_calls']==0 and report['execution_ready'] is False + + +def test_delayed_outcome_and_month_boundary(monkeypatch): + root=event(); follow=copy.deepcopy(root) + follow.update(event_id='later',previous_event_sha256=hashlib.sha256(yaml.safe_dump(root).encode()).hexdigest(),recorded_at='2026-09-05T00:00:00Z') + follow['outcome'].update(value='helpful',observation='Test passed',evidence_refs=['tests/result']) + fixtures(monkeypatch,[follow,root]) + report=f.collect('monthly',datetime(2026,9,10,tzinfo=timezone.utc)) + assert report['outcomes']=={'unknown':1} + assert f.collect()['outcomes']=={'helpful':1} + assert f.collect()['signals'][0]['verification_required'] is True + + +def test_identity_collision_quarantines_use(monkeypatch): + conflict=copy.deepcopy(event()); conflict['decision']['reason']='different' + fixtures(monkeypatch,[event(),conflict]) + report=f.collect() + assert report['real_uses']==0 and report['status']=='degraded' + assert 'quarantined_event_lineage' in report['errors'] + + +def test_missing_predecessor_quarantines_use(monkeypatch): + e=event(); e['previous_event_sha256']='e'*64 + fixtures(monkeypatch,[e]) + assert f.collect()['real_uses']==0 + + +@pytest.mark.parametrize('value',['helpful','harmful','mixed','no-effect']) +def test_outcomes_need_evidence(value): + e=event(); e['outcome']['value']=value + with pytest.raises(ValueError): f.validate_event(e,'demo') + e['outcome'].update(observation='Observed result',evidence_refs=['result']) + f.validate_event(e,'demo') + + +@pytest.mark.parametrize('action',['rejected','no-match']) +def test_empty_selection_valid_for_abstention(action): + e=event(); e['context']['selected']=[]; e['decision']['action']=action + f.validate_event(e,'demo') + + +@pytest.mark.parametrize('path',['../secret','/etc/passwd','x//y','x/./y','x?token=a','https://evil/x']) +def test_reject_unsafe_path(path): + assert not f.path_ok(path) + + +def test_unavailable_is_not_no_data(monkeypatch): + monkeypatch.setattr(f,'revision',lambda *args: (_ for _ in ()).throw(ValueError('unavailable'))) + report=f.collect() + assert report['status']=='degraded' and report['errors'] + + +def test_empty_roster_is_no_consumers(monkeypatch): + monkeypatch.setattr(f,'revision',lambda *args:'a'*40) + monkeypatch.setattr(f,'raw',lambda *args:b'{"schema_version":1,"sources":[]}') + report=f.collect() + assert report['status']=='no_consumers' and report['eligible_tasks'] is None + assert len(f.FrontendPatternsResolver().resolve('daily',None,{})) < 3900 + + +def test_over_limit_is_explicit(monkeypatch): + entries=fixtures(monkeypatch,[event()]) + entries.extend([entries[0]]*100) + report=f.collect() + assert report['status']=='degraded' and report['receipt_events']==0 + + +def test_domain_definitions_parse(): + manifest = Path(__file__).parents[1] / 'k8s/railiance/20-runtime.yaml' + cm = next(d for d in yaml.safe_load_all(manifest.read_text()) if d and d.get('metadata', {}).get('name') == 'actcore-external-activity-definitions') + import tempfile + for name, body in cm['data'].items(): + if not name.startswith('frontend-patterns-'): continue + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / name + path.write_text(body) + d=parse_file(path) + assert d.instructions[0]['model']=='deterministic' diff --git a/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md b/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md new file mode 100644 index 0000000..f917379 --- /dev/null +++ b/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md @@ -0,0 +1,67 @@ +--- +id: ACTIVITY-WP-0040 +type: workplan +title: "Deterministic frontend-patterns feedback collection and reports" +domain: infotech +repo: activity-core +status: active +owner: codex +topic_slug: activity-core +created: "2026-09-27" +updated: "2026-09-27" +--- + +User requested activity-core setup for FEP-WP-0008's unattended loop. This workplan +owns the reporting runtime; it does not claim an admitted repository executor. + +## Implement bounded receipt collection + +```task +id: ACTIVITY-WP-0040-T01 +status: done +priority: high +``` + +The frontend-patterns resolver reads fixed-host, commit-pinned source indexes; +validates and reduces immutable receipts; quarantines conflicting histories; +separates synthetic use; reports errors and unknown coverage; emits no tasks or +model calls. New contract tests and existing instruction/sync tests verify this. +Domain definitions project into k8s/railiance/20-runtime.yaml, initially disabled. +Dockerfile.frontend-patterns overlays only two Python files on the deployed worker. + +## Deploy, verify sinks and enable schedules + +```task +id: ACTIVITY-WP-0040-T02 +status: wait +priority: high +``` + +Target ADMINISTER @ realm:kubernetes/railiance01. User authorizes setup, but the +estate's 2026-09-21 Kubernetes change gate requires Git/ArgoCD for unmapped targets. +No activity-core ArgoCD application or lower-tier mapping was found. Route through +ArgoCD adoption or an explicit scoped exception before live mutation. Prepare +server dry-run and client diff for only the worker and external-definition +ConfigMap. Observed node requests: 3430m/4000m CPU and 9376Mi memory; verify again +before rollout. Do not rewrite API/router images or unrelated definitions. + +After rollout, sync disabled definitions; manually exercise the report sinks; +enable all three domain definitions and matching projections; sync schedules; +record exact receipts and first natural scheduled execution. Rollback restores +activity-core:fi-publication-20260914 and disables the definitions with resync. + +## Preserve execution dependency and complete operational handoff + +```task +id: ACTIVITY-WP-0040-T03 +status: todo +priority: high +``` + +GLAS-WP-0012 / HFACT-WP-0001 retain production profile readiness and spend custody. +FEP-WP-0008-T03 retains weekly investigation and publication; no legacy fallback. +The resolver always reports execution_ready=false until a separately reviewed +integration replaces that declaration with measured readiness. Source enrollment, +producer hooks, pagination and real-use acceptance remain FEP-WP-0008-T01/T02. +Keep this workplan active until production reporting is evidenced and unresolved +operational tasks have live owners. Zero enrolled consumers is not success evidence.