diff --git a/.github/workflows/semantic-data-portal-hourly-review-repair.yml b/.github/workflows/semantic-data-portal-hourly-review-repair.yml new file mode 100644 index 000000000..c77979382 --- /dev/null +++ b/.github/workflows/semantic-data-portal-hourly-review-repair.yml @@ -0,0 +1,37 @@ +name: Semantic Data Portal Hourly Review Repair + +on: + schedule: + # Minute 59 is reserved for semantic-data-portal in the organization + # caller ledger and is unique among product heartbeats. GitHub may delay + # scheduled runs, so this is a heartbeat rather than a minute-zero surge + # avoidance guarantee. + - cron: "59 * * * *" + +concurrency: + group: semantic-data-portal-hourly-review-repair + # The queue scan is bounded and the worker has its own exact-head lease. Do not + # discard an in-flight RCA merely because the next hourly heartbeat arrives. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + dispatch-review-repair: + permissions: + contents: read + id-token: write + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/semantic-data-portal + base_branch: main + max_prs: "50" + max_dispatches: "1" + # Central OpenCode/NVIDIA NIM work can legitimately approach two hours. + # A two-hour same-head floor avoids duplicate writers without freezing the + # next eligible PR or confusing provider latency with a source-code defect. + retry_hours: "2" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/docs/doctoring/semantic-data-portal-hourly-review-caller.md b/docs/doctoring/semantic-data-portal-hourly-review-caller.md new file mode 100644 index 000000000..35b825f23 --- /dev/null +++ b/docs/doctoring/semantic-data-portal-hourly-review-caller.md @@ -0,0 +1,133 @@ +# Semantic Data Portal hourly review-repair caller + +## Decision + +ContextualWisdomLab operates one protected hourly caller for +`ContextualWisdomLab/semantic-data-portal`. The caller runs at minute 59, +delegates to the product-neutral central review-fix scheduler, inspects at most +50 open pull requests, and dispatches at most one bounded repair per heartbeat. +The minute is reserved for semantic-data-portal in the organization caller +ledger and is unique among the product caller slots. GitHub may delay scheduled +runs, so the cron is a heartbeat and does not promise avoidance of the +minute-zero runner surge. + +The caller does not implement review or mutation logic itself. It keeps the +portal independently operable while centralizing privileged automation in +`ContextualWisdomLab/.github`. The reusable worker performs exact-head +root-cause analysis, tests remediation feasibility, and edits only when one +small reversible action can change the diagnosed cause inside its sealed +writer authority. + +## Root-cause analysis and remediation feasibility + +The portal queue is dependency-ordered: the shared-base cryptography unlock +merges before the security lock, Keyverse claim aliases, the SQL gate pair, +and then the catalog-plane stack. An hourly redispatch of an unchanged head +does not accelerate that order; it duplicates writer pressure. The worker +therefore enforces these transitions: + +1. Refetch the exact live head, base, reviews, checks, changed paths, and + writer state. +2. Establish the causal chain rather than repeat the terminal symptom. +3. Enumerate materially distinct minimal remedies. +4. Reject remedies that lack writer authority, cross sealed paths, require + unavailable credentials or protected-setting changes, violate stack order, + cannot be verified, or do not alter the diagnosed cause. +5. Dispatch at most one feasible repair. Otherwise leave the tree unchanged so + another eligible pull request can be considered by a later heartbeat. + +A queued or pending check remains a merge blocker but is not itself a code +finding. The independent non-author approval remains an external authorization +gate and is never synthesized by the repair worker. Product-gap development +that a heartbeat cannot safely automate (for example the Data Management +Evidence Console) stays with the repository's own issue-driven loop; the caller +never fabricates UI evidence to fill it. + +## Cadence and concurrency + +The caller uses a single concurrency group and `cancel-in-progress: false`. +This preserves an in-flight bounded RCA instead of discarding its evidence when +the next hourly heartbeat arrives. The reusable scheduler cancels only its own +superseded short queue scan; the separately dispatched per-PR repair worker and +this product caller remain non-cancelling. The central scheduler and per-PR +worker also retain exact-head leases and mutation limits. + +The caller sets a **two-hour same-head retry floor**. Central OpenCode and +NVIDIA NIM work can legitimately approach two hours, so an hourly redispatch of +the same unchanged head would create duplicate writer pressure rather than +faster remediation. A later hourly scan can still select another eligible pull +request. + +GitHub scheduled workflows can be delayed under load and execute only from the +default branch. Consequently, the cron expression is a heartbeat rather than a +real-time service-level promise. Exact-head state, not elapsed wall-clock time, +controls every mutation and merge decision. + +## Credential and model boundary + +The queue-scanning caller has only `contents: read`. It maps only the +established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` scheduler +credentials and does not use `secrets: inherit`. + +Model execution remains inside the central worker. The model credential is the +GitHub Secret `NVIDIA_NIM_API_KEY`; the caller does not receive or forward it. +`COPILOT_GITHUB_TOKEN` is prohibited. GitHub tokens and GitHub Models are not +model credentials for this write-capable path. The independent review-agent +credential contract is unchanged. + +## Security, standalone operation, and modularity + +The caller adds no portal runtime dependency, database object, network +endpoint, tenant authority, or product credential. The semantic-data-portal +continues to run as a standalone FastAPI application and as a module beside +naruon, Keyverse, DiskSage, fast-mlsirm, TEPP, LineageWeave, RankWeave, and +contextual-orchestrator without weakening its local validation, +protected-branch, exact-head, approval, or security gates. Authority boundaries +stay fixed: identity belongs to Keyverse, policy truth to GRC, capability +targets to enterprise-architecture-core, document-KG writes to naruon, lineage +reconstruction to LineageWeave, and central governance to this repository. + +The reusable workflow source is bound to the called workflow repository, SHA, +ref, and file path before privileged scheduler logic runs. The worker cannot +approve, merge, release, weaken checks, change reviewer identities, or modify +protected settings. Queued, pending, absent, failed, cancelled, skipped-required, +neutral-required, stale-head, or synthetic-merge evidence is not success. + +## Verification and rollback + +Repository contracts require the exact cron, target repository, one-dispatch +budget, two-hour retry floor, non-cancelling single-flight policy, read-only +workflow token, explicit secret mapping, and absence of both +`NVIDIA_NIM_API_KEY` and `COPILOT_GITHUB_TOKEN` from the caller. + +Rollback is a reviewed source change. Do not disable exact-head binding, reduce +the independent approval requirement, increase dispatch volume, use inherited +secrets, or convert provider latency into a fabricated code edit. If the +heartbeat becomes too frequent or too slow, change only the caller cadence and +retry floor after examining observed run duration and queue throughput; preserve +the central RCA, feasibility, lease, and credential contracts. + +## APA 7th references + +GitHub. (n.d.). *Control the concurrency of workflows and jobs*. Retrieved +August 25, 2026, from +https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency + +GitHub. (n.d.). *Events that trigger workflows: Schedule*. Retrieved August 25, +2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule + +GitHub. (n.d.). *Reuse workflows*. Retrieved August 25, 2026, from +https://docs.github.com/en/actions/how-tos/sharing-automations/reusing-workflows + +NVIDIA. (n.d.). *NVIDIA NIM for large language models documentation*. Retrieved +August 25, 2026, from +https://docs.nvidia.com/nim/large-language-models/latest/ + +OpenCode. (n.d.). *OpenCode documentation*. Retrieved August 25, 2026, from +https://opencode.ai/docs/ + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure software development +framework (SSDF) version 1.1: Recommendations for mitigating the risk of +software vulnerabilities* (NIST Special Publication 800-218). National +Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 diff --git a/tests/test_semantic_data_portal_hourly_review_caller.py b/tests/test_semantic_data_portal_hourly_review_caller.py new file mode 100644 index 000000000..18cef1cdd --- /dev/null +++ b/tests/test_semantic_data_portal_hourly_review_caller.py @@ -0,0 +1,114 @@ +"""Contract tests for the semantic-data-portal bounded hourly review-repair caller.""" + +import re +from pathlib import Path + + +CALLER = Path(".github/workflows/semantic-data-portal-hourly-review-repair.yml") +DOCTORING = Path("docs/doctoring/semantic-data-portal-hourly-review-caller.md") + + +def _read(path: Path) -> str: + """Return one repository contract file as UTF-8 text.""" + return path.read_text(encoding="utf-8") + + +def _permission_map(caller: str, header: str) -> dict[str, str]: + """Parse one exact YAML permission block without widening test dependencies.""" + lines = caller.splitlines() + header_index = lines.index(header) + entry_indent = len(header) - len(header.lstrip()) + 2 + permissions: dict[str, str] = {} + for line in lines[header_index + 1 :]: + if not line.strip(): + continue + indent = len(line) - len(line.lstrip()) + if indent < entry_indent: + break + if indent != entry_indent: + continue + key, separator, value = line.strip().partition(":") + assert separator, f"malformed permission entry: {line!r}" + permissions[key] = value.strip() + return permissions + + +def test_semantic_data_portal_caller_is_hourly_bounded_and_non_cancelling() -> None: + """The portal receives one realistic repair opportunity without overlap cancellation.""" + caller = _read(CALLER) + + assert 'cron: "59 * * * *"' in caller + assert "group: semantic-data-portal-hourly-review-repair" in caller + assert "cancel-in-progress: false" in caller + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller + assert "target_repository: ContextualWisdomLab/semantic-data-portal" in caller + assert "base_branch: main" in caller + assert 'max_prs: "50"' in caller + assert 'max_dispatches: "1"' in caller + assert 'retry_hours: "2"' in caller + + +def test_semantic_data_portal_caller_preserves_credentials_and_read_only_token_scope() -> None: + """The queue scanner maps established credentials without exposing model secrets.""" + caller = _read(CALLER) + workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) + + assert _permission_map(workflow_scope, "permissions:") == {"contents": "read"} + assert _permission_map(jobs_scope, " permissions:") == { + "contents": "read", + "id-token": "write", + } + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller + assert "secrets: inherit" not in caller + assert "NVIDIA_NIM_API_KEY" not in caller + assert "COPILOT_GITHUB_TOKEN" not in caller + for forbidden in ( + "actions: write", + "contents: write", + "issues: write", + "pull-requests: write", + "statuses: write", + ): + assert forbidden not in caller + + +def test_semantic_data_portal_caller_cron_avoids_other_callers() -> None: + """Minute 59 does not collide with any other product caller heartbeat.""" + caller = _read(CALLER) + assert '- cron: "59 * * * *"' in caller + other_minutes = { + minute + for path in Path(".github/workflows").glob("*hourly-review-repair.yml") + if path != CALLER + for minute in re.findall(r'cron:\s*["\'](\d+) \* \* \* \*["\']', _read(path)) + } + assert "59" not in other_minutes + + +def test_semantic_data_portal_caller_doctoring_records_rca_feasibility_and_latency() -> None: + """Operators retain the exact rationale for the bounded two-hour retry policy.""" + doctoring = _read(DOCTORING) + + for phrase in ( + "root-cause analysis", + "remediation feasibility", + "two-hour same-head retry floor", + "exact-head", + "cancel-in-progress: false", + "NVIDIA_NIM_API_KEY", + "COPILOT_GITHUB_TOKEN", + "PR_REVIEW_MERGE_TOKEN", + "OPENCODE_APPROVE_TOKEN", + "ContextualWisdomLab/semantic-data-portal", + "minute 59", + ): + assert phrase in doctoring, phrase + + for reference in ( + "https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency", + "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule", + "https://docs.github.com/en/actions/how-tos/sharing-automations/reusing-workflows", + "https://doi.org/10.6028/NIST.SP.800-218", + ): + assert reference in doctoring, reference