diff --git a/.github/workflows/full-pytest.yml b/.github/workflows/full-pytest.yml index 60a4003f..b08b41d1 100644 --- a/.github/workflows/full-pytest.yml +++ b/.github/workflows/full-pytest.yml @@ -1,19 +1,20 @@ name: full-pytest -# Runs the full pytest suite, excluding tests marked @pytest.mark.quarantine -# via the conftest.py QUARANTINE registry. The intent is a ratchet: once a -# test is removed from the registry it must keep passing on this gate. +# Post-merge validation — runs the full pytest suite on every push to main. +# PRs are gated by the faster smoke workflow (smoke.yml); this catches +# anything outside the smoke suite within minutes of merge. # -# Quarantined tests are pre-existing failures that pre-date the substrate- -# liveness audit (bisect-confirmed against c1a1b7a). See: +# Quarantined tests are excluded via the conftest.py QUARANTINE registry. +# The intent is a ratchet: once a test is removed from the registry it +# must keep passing on this gate. +# +# See: # conftest.py — the QUARANTINE registry (one entry per quarantined test) # docs/test-debt-quarantine.md — cluster diagnoses + removal policy on: push: branches: [main] - pull_request: - branches: [main] permissions: contents: read @@ -24,7 +25,7 @@ concurrency: jobs: pytest: - name: full pytest (-m "not quarantine" -n 4) + name: full pytest (-m "not quarantine" -n 2) runs-on: ubuntu-latest timeout-minutes: 45 @@ -49,7 +50,7 @@ jobs: PYTHONPATH: ${{ github.workspace }} CORE_SHOWCASE_SKIP_BUDGET: "1" run: | - uv run pytest -m "not quarantine" -n 4 --tb=short -q --maxfail=10 + uv run pytest -m "not quarantine" -n 2 --tb=short -q --maxfail=10 - name: report quarantine size (informational) if: always() diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 00000000..0b70d8ae --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,52 @@ +name: smoke + +# Fast PR gate — runs the smoke suite (~2-3 min) on every PR push. +# Covers: chat runtime, pipeline, architectural invariants. +# +# Full pytest runs post-merge to main (see full-pytest.yml). +# Regressions caught here block the PR; anything outside the smoke +# suite is caught on main within minutes of merge. + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: smoke-${{ github.ref }} + cancel-in-progress: true + +jobs: + smoke: + name: smoke (-m "not quarantine") + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: set up uv + uses: astral-sh/setup-uv@v5 + with: + python-version: '3.11' + enable-cache: true + + - name: install dependencies + run: | + uv pip install -e ".[dev]" pyyaml + + - name: pytest smoke suite + env: + PYTHONPATH: ${{ github.workspace }} + run: | + uv run pytest -m "not quarantine" --tb=short -q \ + tests/test_chat_runtime.py \ + tests/test_achat.py \ + tests/test_runtime_config.py \ + tests/test_cognitive_turn_pipeline.py \ + tests/test_architectural_invariants.py diff --git a/docs/audit/substrate-liveness-ratchet.md b/docs/audit/substrate-liveness-ratchet.md index 4d28cce9..55d7c174 100644 --- a/docs/audit/substrate-liveness-ratchet.md +++ b/docs/audit/substrate-liveness-ratchet.md @@ -125,7 +125,11 @@ home (ADR-XXXX or new). - **Recommended:** decision belongs to operator review. If (b), it's a cleanup PR, not a wiring ADR. If (a), it's a new ADR explaining why pack-readback regains primacy. -- **Status:** ⏳ OPEN — operator decision required before sequencing. +- **Resolution:** path (b) chosen. `readback_from_intent` and its `SurfaceRealization` + return type deleted from `packs/common/runtime_rules.py` (zero callers confirmed; + `generate/realizer.py` is the live surface path). No per-language readback files + ever existed. Cleanup-as-you-find applied per [[feedback-cleanup-as-you-find]]. +- **Status:** ✅ CLOSED — phase 2 operator-decisions PR. ### W-007 — `DerivedRecognizer` integration into live turn loop @@ -204,7 +208,11 @@ home (ADR-XXXX or new). be the right level (anti-unification doesn't *need* pack vocabulary — it derives its own structure); pack consumption may be premature generalization. -- **Status:** ⏳ OPEN — operator decision required. +- **Resolution:** path (a) chosen. Token-level anti-unification is intentional — + the recognizer derives its own structure from taught examples without importing + pack vocabulary, which aligns with the thesis (decodes, not generates). Documented + in ADR-0143 amendment note. No code change. +- **Status:** ✅ CLOSED — operator decision: intentional by design. ### W-011 — Typed recognition refusals dropped at pipeline boundary (FIXED) @@ -263,7 +271,11 @@ home (ADR-XXXX or new). [[feedback-cleanup-as-you-find]] — the audit's "unambiguously dead" bar is not met here because the module is well-formed and test-covered, just unwired. Operator call. -- **Status:** ⏳ OPEN — operator decision required. +- **Resolution:** path (a) already landed before this audit entry was written. + `ChatRuntime.explain_last_turn()` at `chat/runtime.py:643` backs the `/explain` + REPL command wired in `core/cli.py:246`. `tests/test_explain_repl.py` pins + the contract. Ratchet was stale. +- **Status:** ✅ CLOSED — already wired; ratchet stale entry corrected. ### W-014 — `core/cognition/provenance.py` partially live (evals-only) @@ -277,9 +289,11 @@ home (ADR-XXXX or new). surfacing. - **(b)** Relocate to `evals/` and accept as offline-only. - **(c)** Leave as-is and document explicitly as evals-only. -- **Status:** ⏳ OPEN — lighter than W-013 because there IS a live - consumer (evals); the question is whether it should be promoted to - runtime use. +- **Resolution:** path (c) chosen. The module's own docstring already declares + *"Deployment scope (W-014): this module is evals-only infrastructure."* It + has a live consumer (`evals/provenance/runner.py`). Promoting to live runtime + is deferred; the evals path is sufficient for audit work. No code change. +- **Status:** ✅ CLOSED — accepted as evals-only per module's own docstring. ### W-015 — `session/context.py` post-generation unitize undocumented (FIXED) @@ -425,7 +439,11 @@ emphasize the rebase-onto-current-main step before PR creation. - **(c)** Leave as test-live library and document explicitly. - **Recommended:** (a) first as the smallest reachability fix; (b) follows naturally if W-017 materializes. -- **Status:** ⏳ OPEN — operator decision required. +- **Resolution:** path (a) already landed before this audit entry was written. + `cmd_teaching_propose_miner` and `cmd_teaching_propose_curriculum` are both + registered in `core/cli.py` (lines 3511–3553) as `core teaching propose + --from-miner` and `core teaching propose --from-curriculum`. Ratchet was stale. +- **Status:** ✅ CLOSED — already wired; ratchet stale entry corrected. --- diff --git a/packs/common/runtime_rules.py b/packs/common/runtime_rules.py index 82a6efbf..b9380eb4 100644 --- a/packs/common/runtime_rules.py +++ b/packs/common/runtime_rules.py @@ -4,7 +4,6 @@ from __future__ import annotations import hashlib import json -from dataclasses import dataclass from pathlib import Path from typing import Any @@ -20,15 +19,6 @@ from core_ingest.types import ( ) -@dataclass(frozen=True, slots=True) -class SurfaceRealization: - surface: str - language: str - field_target: str | None = None - energy_class: str | None = None - valence: dict[str, object] | None = None - - def read_jsonl(path: Path) -> list[dict[str, Any]]: return [ json.loads(line) @@ -99,24 +89,3 @@ def lift_from_pack(pack_dir: Path, analysis: object, *, language: str) -> list[C return [packet] -def readback_from_intent(field_state: object, intent: object, *, language: str) -> SurfaceRealization: - payload = analysis_payload(intent or {"surface": ""}) - surface = payload.get("surface") - if surface is None and "tokens" in payload: - surface = " ".join(str(token) for token in payload["tokens"]) - if surface is None and "lemma" in payload: - surface = str(payload["lemma"]) - if surface is None and "script_form" in payload: - surface = str(payload["script_form"]) - if surface is None: - energy = getattr(field_state, "energy", None) - surface = energy.energy_class.value if energy is not None else "" - energy = getattr(field_state, "energy", None) - valence = getattr(field_state, "valence", None) - return SurfaceRealization( - surface=str(surface), - language=language, - field_target=payload.get("field_target"), - energy_class=None if energy is None else energy.energy_class.value, - valence=None if valence is None else valence.to_payload(), - )