diff --git a/.claude/rules/contracts.md b/.claude/rules/contracts.md index 3ed4440b..d0938bf1 100644 --- a/.claude/rules/contracts.md +++ b/.claude/rules/contracts.md @@ -4,6 +4,7 @@ paths: - "chassis/**/*.py" - "tools/**/*.py" --- + # CEG Contracts (1–24) Enforced by `tools/contract_scanner.py` and `tools/verify_contracts.py`. @@ -14,7 +15,7 @@ Enforced by `tools/contract_scanner.py` and `tools/verify_contracts.py`. | 1 | Single Ingress | Only POST /v1/execute and GET /v1/health. Engine NEVER imports FastAPI/Starlette. | | 2 | Handler Interface | `async def handle_*(tenant: str, payload: dict) -> dict`. handlers.py is the ONLY engine file importing chassis (plus boot.py). | | 3 | Tenant Isolation | Tenant resolved BY chassis. Every Neo4j query scopes to tenant database. No cross-tenant reads. | -| 4 | Observability Inherited | Engine NEVER configures structlog/Prometheus. Uses `structlog.get_logger(__name__)` only. | +| 4 | Observability Inherited | Engine NEVER configures structlog/Prometheus. Logger getter is `logging.getLogger(__name__)` or `structlog.get_logger(__name__)`. | | 5 | Infrastructure is Template | Engine NEVER creates Dockerfile, docker-compose, CI pipeline. All in l9-template. | ## Layer 2 — Packet Protocol (6–8) @@ -44,7 +45,7 @@ Enforced by `tools/contract_scanner.py` and `tools/verify_contracts.py`. | # | Name | Rule | |---|------|------| | 17 | Test Requirements | Unit for pure functions, integration with testcontainers-neo4j, compliance for prohibited factors, <200ms p95. | -| 18 | L9_META Headers | Every file carries L9_META header. Injected by tools/l9_meta_injector.py. | +| 18 | L9_META Headers | Every tracked file carries an L9_META header (schema v2). Values resolve from `l9-meta.yaml` by path — write with `tools/l9_meta_injector.py apply`, verify with `check`, never hand-edit a header. | ## Layer 6 — Graph Intelligence (19–20) | # | Name | Rule | diff --git a/.cursorrules b/.cursorrules index 4a51555b..73bb854b 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,13 +1,12 @@ -# Made By: Igor Beylin # --- L9_META --- -# l9_schema: 1 +# l9_schema: 2 # origin: l9-template # engine: graph # layer: [agent-rules] -# tags: [L9_TEMPLATE, agent-rules, cursor] -# owner: platform +# tags: [governance] # status: active # --- /L9_META --- +# Made By: Igor Beylin # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # SESSION BOOTSTRAP — EXECUTE THIS BLOCK ON EVERY NEW CHAT WINDOW # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -68,7 +67,7 @@ # dimensions, GDS jobs, KGE embeddings, community detection. # # @docs/L9_Contract_Enforcement_System.md -# How contract_scanner.py + verify_contracts.py enforce the 20 contracts. +# How contract_scanner.py + verify_contracts.py enforce the 24 contracts. # Read before touching tools/ or .github/workflows/. # # ── TIER 4: ACTIVE DOMAIN SPEC (always in context) ────────────────────────── @@ -84,7 +83,7 @@ # # ── TIER 5: CONTRACT DOCS (load only the relevant one before touching a subsystem) # -# docs/contracts/ directory contains the 20 contract files. +# docs/contracts/ directory contains the 27 contract docs. # Do NOT bulk-load all of them. Load the specific file for the subsystem # you are about to modify: # @@ -93,6 +92,7 @@ # @docs/contracts/CYPHER_SAFETY.md # @docs/contracts/BANNED_PATTERNS.md # @docs/contracts/PYDANTIC_YAML_MAPPING.md +# @docs/contracts/BIDIRECTIONAL_MATCHING.md # # Before touching engine/handlers.py or chassis/: # @docs/contracts/HANDLER_PAYLOADS.md @@ -113,6 +113,8 @@ # Before touching engine/compliance/: # @docs/contracts/OBSERVABILITY.md # @docs/contracts/MEMORY_SUBSTRATE_ACCESS.md +# @docs/contracts/PROHIBITED_FACTORS.md +# @docs/contracts/PII_HANDLING.md # # Before touching domains/ or domain spec versioning: # @docs/contracts/DOMAIN_SPEC_VERSIONING.md @@ -122,9 +124,21 @@ # Before touching .env.template or env var naming: # @docs/contracts/ENV_VARS.md # +# Before touching engine/config/settings.py or gating a behavior change: +# @docs/contracts/FEATURE_FLAG_DISCIPLINE.md +# +# Before touching engine/scoring/ or engine/boot.py: +# @docs/contracts/SCORING_WEIGHT_CEILING.md +# +# Before touching engine/kge/: +# @docs/contracts/KGE_EMBEDDINGS.md +# +# Before adding any new tracked file: +# @docs/contracts/L9_META_HEADERS.md +# # ── INVARIANT CHECKS (verify silently on load) ─────────────────────────────── # -# 1. GateType enum = exactly 14 values (contract 13) +# 1. GateType enum = exactly 10 values (contract 13) # 2. ScoringAssembler = exactly 4 active dimensions (contract 13) # 3. No PR marked BLOCKED in workflow_state.md for the branch being worked on # 4. Task about to be started does NOT appear in DEFERRED.md @@ -136,7 +150,7 @@ # Do not guess its contents. Do not proceed without it. # # ============================================================================ -# 20 contracts. Violate any → revert and ask. +# 24 contracts. Violate any → revert and ask. # ============================================================================ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -429,7 +443,7 @@ # DEL-002 requests.post/get/etc (in engine/) → contract 8 # MEM-001 INSERT INTO packetstore (in engine/) → contract 7 # MEM-002 INSERT INTO memory_embeddings (in engine/) → contract 7 -# STUB-001 raise NotImplementedError (outside tests/) → zero-stub protocol +# STUB-001 raise NotImplementedError (in engine/) → zero-stub protocol # # HIGH — merge blocked: # ERR-001 bare except: → error handling @@ -441,8 +455,8 @@ # SHARED-001 class PacketEnvelope (redefining) → contract 7 # SHARED-002 class TenantContext (redefining) → contract 3 # SHARED-003 class ExecuteRequest (redefining) → contract 1 -# STUB-002 # TODO comment → zero-stub protocol -# STUB-003 # PLACEHOLDER comment → zero-stub protocol +# STUB-002 # TODO comment (in engine/) → zero-stub protocol +# STUB-003 # PLACEHOLDER/FIXME/XXX comment (in engine/) → zero-stub protocol # PKT-001 uppercase packet_type value → contract 7 # ENV-001 non-L9_ env var name for infra vars → convention @@ -454,7 +468,7 @@ # 1. PRE-COMMIT → ruff, mypy --strict, contract_scanner.py, verify_contracts.py # 2. CI LINT → same as pre-commit, repo-wide # 3. CI TESTS → pytest (unit + integration + compliance + performance) -# 4. CI AUDIT → verify_contracts.py (all 20 contract files exist + wired) +# 4. CI AUDIT → verify_contracts.py (all 27 contract docs exist + wired) # 5. LLM REVIEW → CodeRabbit + Qodo + Claude (contract-aware instructions) # # Branch protection: all 5 required status checks. No bypass. diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 03eb5d8d..8524f2ae 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -1,10 +1,9 @@ # --- L9_META --- -# l9_schema: 1 +# l9_schema: 2 # origin: l9-template # engine: graph # layer: [ci] -# tags: [L9_TEMPLATE, ci, audit, harness] -# owner: platform +# tags: [delivery, harness] # status: active # --- /L9_META --- name: L9 Audit Harness @@ -14,6 +13,10 @@ on: push: branches: [main] +permissions: + contents: read + pull-requests: write + jobs: audit: runs-on: ubuntu-latest @@ -35,3 +38,55 @@ jobs: with: name: l9-audit-reports path: artifacts/ + + - name: Post harness report to PR + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const marker = ''; + const reportPath = 'artifacts/harness_report.md'; + const pr = context.payload.pull_request; + + let report; + try { + report = fs.readFileSync(reportPath, 'utf8'); + } catch (err) { + report = '⚠️ Audit harness did not produce a report at `' + reportPath + + '`. Check the [workflow run](' + + `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + + ') for details.'; + } + + const maxLen = 60000; + if (report.length > maxLen) { + report = report.slice(0, maxLen) + '\n\n...(truncated — see workflow run artifacts for the full report)'; + } + const body = `${marker}\n${report}`; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100 + }); + const botComment = [...comments].reverse().find(c => + c.user.type === 'Bot' && c.body.includes(marker) + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + } diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index a4ded334..a735b39d 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -1,13 +1,12 @@ # --- L9_META --- -# l9_schema: 1 +# l9_schema: 2 # origin: l9-template # engine: graph # layer: [ci] -# tags: [L9_TEMPLATE, ci, contracts] -# owner: platform +# tags: [delivery, harness] # status: active # --- /L9_META --- -# L9 Contract Enforcement — 20 contracts as hard gates +# L9 Contract Enforcement — 24 invariants / 27 docs as hard gates # Blocks merge on missing contract files or scanner violations. name: Contract Enforcement @@ -35,6 +34,7 @@ jobs: - uses: actions/setup-python@v7 with: python-version: "3.12" + - run: pip install -r requirements-ci.txt - run: python tools/verify_contracts.py contract-scan: @@ -45,8 +45,23 @@ jobs: - uses: actions/setup-python@v7 with: python-version: "3.12" + - run: pip install -r requirements-ci.txt - run: python tools/contract_scanner.py + meta-headers: + name: Verify L9_META Headers + runs-on: ubuntu-latest + steps: + # fetch-depth is irrelevant here, but the checkout must be a real git repo: + # discovery enumerates via `git ls-files -z`, not a filesystem walk. + - uses: actions/checkout@v6 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install pyyaml + # Default mode is dry-run verification; there is no `check` subcommand. + - run: python tools/l9_meta_injector.py + lint: name: Lint + Type Check runs-on: ubuntu-latest @@ -63,7 +78,7 @@ jobs: test: name: Test Suite runs-on: ubuntu-latest - needs: [contract-files, contract-scan, lint] + needs: [contract-files, contract-scan, meta-headers, lint] steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v7 diff --git a/.github/workflows/l9-analysis.yml b/.github/workflows/l9-analysis.yml index aed27bfa..249c68bf 100644 --- a/.github/workflows/l9-analysis.yml +++ b/.github/workflows/l9-analysis.yml @@ -81,11 +81,14 @@ jobs: set -euo pipefail pip install --upgrade pip semgrep mkdir -p "artifacts/raw/semgrep/${L9_MATRIX_ID}" + # No `|| true` (Baseline Ratchet rejects fail-open). Also no + # `--error`: findings must reach normalize/publish so governance + # can decide blocking vs advisory; `--error` exits 1 before that. semgrep scan \ --config p/python \ --json \ --output "artifacts/raw/semgrep/${L9_MATRIX_ID}/report.json" \ - --error --quiet || true + --quiet env: L9_MATRIX_ID: ${{ env.L9_MATRIX_ID }} diff --git a/.github/workflows/l9-lint-test.yml b/.github/workflows/l9-lint-test.yml index a90d6188..d894d308 100644 --- a/.github/workflows/l9-lint-test.yml +++ b/.github/workflows/l9-lint-test.yml @@ -74,18 +74,16 @@ jobs: - name: mypy run: | set -euo pipefail - # mkdir required before --install-types on a cold cache, else mypy - # fails with the misleading "no mypy cache directory" error and - # masks any real type errors underneath it (python/mypy#10768). + # Align with make lint / ci.yml / ci-quality.yml: type-check engine/ + # only. mypy on SOURCE_DIR=. dual-maps tools/auditors and fails closed + # before any engine diagnostics are useful. mkdir -p .mypy_cache - MYPY_EXCLUDE_ARGS=() - if [ -n "${MYPY_EXCLUDE}" ]; then - MYPY_EXCLUDE_ARGS=(--exclude "${MYPY_EXCLUDE}") - fi - mypy "${SOURCE_DIR}" \ - "${MYPY_EXCLUDE_ARGS[@]}" \ + mypy engine/ \ + --config-file=pyproject.toml \ + --ignore-missing-imports \ + --exclude chassis \ --show-error-codes --pretty \ - --install-types --non-interactive --ignore-missing-imports + --install-types --non-interactive test: name: Test Suite diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index cebf215a..4b35d9d4 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -102,8 +102,9 @@ jobs: uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 with: fail-on-severity: high + # dependency-review-action rejects specifying both allow-licenses and + # deny-licenses. Keep the allow-list (stricter); deny-list is implied. allow-licenses: ${{ vars.ALLOWED_LICENSES || 'MIT, Apache-2.0, BSD-3-Clause, BSD-2-Clause, ISC' }} - deny-licenses: ${{ vars.DENIED_LICENSES || 'GPL-3.0, AGPL-3.0' }} comment-summary-in-pr: on-failure # ──────────────────────────────────────────────────────────────────────── diff --git a/.gitignore b/.gitignore index dfb12e56..9a9264fb 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ secrets.json # l9-ci-sdk runtime checkout (provisioned by tools/packet_envelope_gate.py) .l9/runtime/ +.venv-py312-mypy/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55a5523a..88f1e318 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -79,7 +79,7 @@ repos: language: system pass_filenames: false - # L9 contract enforcement (20 contracts) + # L9 contract enforcement (24 invariants, 27 docs) - repo: local hooks: - id: l9-contract-scan @@ -95,6 +95,21 @@ repos: pass_filenames: false always_run: true + # L9_META headers (Contract C-018) — resolves every tracked file against + # l9-meta.yaml. always_run because a rule edit changes the expected values + # for files that are not themselves staged. + - repo: local + hooks: + - id: l9-meta-check + name: L9_META Header Check + entry: python tools/l9_meta_injector.py check + language: python + # pre-commit builds an isolated venv, so l9-meta.yaml parsing needs + # pyyaml declared here — it is not inherited from the repo environment. + additional_dependencies: [pyyaml] + pass_filenames: false + always_run: true + # Audit harness (architecture + spec coverage + contract wiring) - repo: local hooks: diff --git a/.suite6-config.json b/.suite6-config.json deleted file mode 100644 index 7aaa72e8..00000000 --- a/.suite6-config.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "_l9_meta": { - "l9_schema": 1, - "origin": "l9-template", - "engine": "graph", - "layer": [ - "config" - ], - "tags": [ - "L9_TEMPLATE", - "config", - "suite6" - ], - "owner": "platform", - "status": "active" - }, - "suite_version": "6.0.0", - "workspace_id": "ws-20260301-144741", - "workspace_path": "/Users/ib-mac/Dropbox/Repo_Dropbox_IB/Graph Cognitive Engine", - "suite6_root": "/Users/ib-mac/Dropbox/Cursor Governance/Cursor Governance Suite 6 (L9)", - "governance_enabled": true, - "intelligence_active": true, - "monitoring_level": "standard", - "compliance_required": true, - "created": "2026-03-01T14:47:41.005566", - "last_validated": "2026-03-01T14:47:41.005569", - "features": { - "meta_learning": true, - "cursor_native_reasoning": true, - "formal_logic_validation": true, - "autonomous_operation": false, - "real_time_monitoring": false - }, - "api_endpoints": { - "governance_api": "http://localhost:8080", - "health_check": "http://localhost:8080/governance/health", - "validation": "http://localhost:8080/governance/validate" - } -} diff --git a/AGENTS.md b/AGENTS.md index fda25ec4..8152aa6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,12 @@ + + # AGENTS.md — L9 Graph Cognitive Engine Cross-tool agent instructions for the CEG repository. Read by Claude Code, Codex, Cursor, Copilot, Jules, Aider, CodeRabbit, and all AGENTS.md-compatible tools. @@ -65,7 +74,8 @@ tools/ # contract_scanner.py, verify_contracts.py, validate_do - Type hints on every function signature - Pydantic v2 BaseModel for all structured data - `ruff format .` before commit (Black-compatible, 88-char) -- `structlog.get_logger(__name__)` for logging — never configure structlog in engine +- `logging.getLogger(__name__)` or `structlog.get_logger(__name__)` for logging — match the + surrounding module, and never configure logging in engine (see `docs/contracts/OBSERVABILITY.md`) - Exception messages: `msg = f"..."; raise ValueError(msg)` (avoids EM101) - Nullable: `x: str | None = None` — never `Optional` - Datetime: always `datetime.now(tz=UTC)` @@ -118,4 +128,22 @@ tools/ # contract_scanner.py, verify_contracts.py, validate_do | `docs/TROUBLESHOOTING.md` | 10 common failure scenarios with diagnosis + resolution | Debugging errors, CI failures | | `docs/AI_AGENT_REVIEW_CHECKLIST.md` | PR review rubric, severity scoring, comment templates | Reviewing PRs (CodeRabbit, Qodo, Claude) | | `docs/CI_PIPELINE.md` | 7 CI phases, 15 pre-commit hooks, blocking vs advisory | CI failure diagnosis | +| `docs/CI_CONSTELLATION_BOUNDARY.md` | What's wired (`l9-ci-core`/`l9-ci-sdk`) vs. not (`l9-harness`/`l9-assurance`); extend-don't-replace rule for `audit.yml` | Before wiring any Quantum-L9 constellation repo, or touching `tools/audit_harness.py` / `.github/workflows/audit.yml` | | `.claude/rules/contracts.md` | 24 contracts + enforcement matrix (automated vs manual) | Contract uncertainty | + + + +## Formatter ownership + +Workspace class: `biome_default` — Default for every governed workspace: Biome owns JS/TS/JSON, Ruff owns Python. + +Exactly one formatter owns each language. Do not reformat a file with a tool other than its owner, and do not add config for a competing formatter: the result is a diff that churns on every save. + +| Languages | Owner | Note | +|---|---|---| +| `javascript`, `javascriptreact`, `typescript`, `typescriptreact`, `json`, `jsonc` | **biome** | bound by the governed IDE profile | +| `python` | **ruff** | bound by the governed IDE profile | + +Generated from `environment/ide/policy.json` in the governance clone by `ops/scripts/adapters/agentdocs.sh`. Edit the policy, not this block. + + diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0b6d8c73..745c37bd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -56,7 +56,7 @@ tests/ property/ Hypothesis-based property tests tools/ contract_scanner.py Scans generated Cypher for contract violations - verify_contracts.py Asserts all 24 contracts pass + verify_contracts.py Asserts every contract doc exists and is wired into agent files validate_domain.py Validates domain spec YAML against Pydantic schema ``` diff --git a/CLAUDE.md b/CLAUDE.md index e1194a20..3db15142 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,6 +98,24 @@ def proximity_gate(spec, domain): @docs/SEL4_UPGRADES.md ``` +## Contract Docs + +Load the specific doc for the subsystem you are about to touch — do not bulk-load. + +| Subsystem | Docs | +|---|---| +| `engine/gates/`, `engine/config/schema.py` | `docs/contracts/FIELD_NAMES.md`, `docs/contracts/CYPHER_SAFETY.md`, `docs/contracts/BANNED_PATTERNS.md`, `docs/contracts/PYDANTIC_YAML_MAPPING.md`, `docs/contracts/BIDIRECTIONAL_MATCHING.md` | +| `engine/handlers.py`, `chassis/` | `docs/contracts/HANDLER_PAYLOADS.md`, `docs/contracts/METHOD_SIGNATURES.md`, `docs/contracts/DEPENDENCY_INJECTION.md`, `docs/contracts/RETURN_VALUES.md` | +| `engine/packet/` | `docs/contracts/PACKET_ENVELOPE_FIELDS.md`, `docs/contracts/SHARED_MODELS.md`, `docs/contracts/DELEGATION_PROTOCOL.md`, `docs/contracts/PACKET_TYPE_REGISTRY.md` | +| `tests/` | `docs/contracts/TEST_PATTERNS.md`, `docs/contracts/ERROR_HANDLING.md` | +| `engine/compliance/` | `docs/contracts/OBSERVABILITY.md`, `docs/contracts/MEMORY_SUBSTRATE_ACCESS.md`, `docs/contracts/PROHIBITED_FACTORS.md`, `docs/contracts/PII_HANDLING.md` | +| `domains/`, spec versioning | `docs/contracts/DOMAIN_SPEC_VERSIONING.md`, `docs/contracts/FEEDBACK_LOOPS.md`, `docs/contracts/NODE_REGISTRATION.md` | +| `.env.template`, env naming | `docs/contracts/ENV_VARS.md` | +| `engine/config/settings.py`, feature gating | `docs/contracts/FEATURE_FLAG_DISCIPLINE.md` | +| `engine/scoring/`, `engine/boot.py` | `docs/contracts/SCORING_WEIGHT_CEILING.md` | +| `engine/kge/` | `docs/contracts/KGE_EMBEDDINGS.md` | +| Any new tracked file | `docs/contracts/L9_META_HEADERS.md` | + ## References Detailed reference material loads automatically from `.claude/rules/` when you edit relevant files: diff --git a/DEFERRED.md b/DEFERRED.md index dbb78d2a..bd601886 100644 --- a/DEFERRED.md +++ b/DEFERRED.md @@ -49,6 +49,33 @@ All inline TODO comments must be migrated here with a unique ID, owner, rational --- +## DEFERRED-003 + +**Title:** `contracts/contract_NN.yaml` registry does not conform to `test_contract_registry.py` schema + +**File:** `contracts/contract_01.yaml` through `contract_24.yaml` (all 24, committed via PR #70) + +**Owner:** engine-team + +**Rationale:** `tests/contracts/test_contract_registry.py` (added alongside the dual-chassis/SDK migration work) enforces a newer contract-YAML schema than the one the existing 24 `contracts/*.yaml` files were authored against: +- Every contract YAML is missing the required `docs: [...]` key entirely. +- Every contract's `verification.test` points at a retired per-contract test file (e.g. `tests/contracts/test_contract_01.py`) instead of a pytest node ID into the current monolithic `tests/contracts/test_contracts.py` (20 classes, not a 1:1 match against 24 contracts). +- Two aggregate checks also fail: every doc in `verify_contracts.py::REQUIRED_CONTRACTS` must be claimed by some contract's `docs` list, and every scanner rule ID in `tools/contract_scanner.py` must be claimed by some contract's `verification.scanner_rules` list. + +Fixing this requires a deliberate, per-contract mapping decision (which of the 29 `docs/contracts/*.md` files and which `test_contracts.py` class each of the 24 contracts corresponds to) — not a mechanical fix, and getting the mapping wrong would create false confidence in a compliance-tracking system. Deferred rather than guessed. + +**Acceptance Criteria:** +- Every `contracts/contract_NN.yaml` has a non-empty `docs` list of files that exist under `docs/contracts/` +- Every `contracts/contract_NN.yaml`'s `verification.test` is a resolvable `tests/contracts/test_contracts.py::ClassName` node ID +- Every `verification.scanner_rules` entry exists in `tools/contract_scanner.py` +- Every doc in `tools/verify_contracts.py::REQUIRED_CONTRACTS` is claimed by at least one contract's `docs` list +- Every scanner rule ID in `tools/contract_scanner.py` is claimed by at least one contract's `verification.scanner_rules` list +- `pytest tests/contracts/test_contract_registry.py` passes in full + +**Blocked by:** Requires the contract author (or someone with full context on the 24-contract ↔ 29-doc ↔ 20-test-class intended mapping) to make the mapping decisions + +**Priority:** MEDIUM — compliance/audit tooling gap, not a functional regression; existing `tools/verify_contracts.py` and `tools/contract_scanner.py` still run and enforce their own (older) contract set independently + ## DEFERRED-004 **Title:** Delete four superseded `docs/agent-tasks/` development playbooks diff --git a/Dockerfile b/Dockerfile index b771956e..79eb8c31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,10 @@ # ── Stage 1: Dependencies ────────────────────────────────── FROM python:3.12-slim AS deps +# git is required to resolve the git+https constellation-node-sdk dependency +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /build COPY requirements.txt requirements.txt RUN pip install --no-cache-dir --prefix=/install -r requirements.txt @@ -46,6 +50,6 @@ EXPOSE 8000 # Health check HEALTHCHECK --interval=15s --timeout=5s --retries=3 \ - CMD python -c "import httpx; r=httpx.get('http://localhost:8000/v1/health'); exit(0 if r.status_code==200 else 1)" + CMD python -c "import httpx; r=httpx.get('http://localhost:8000/v1/health'); exit(0 if r.status_code==200 and r.json().get('ready', True) else 1)" ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/Dockerfile.prod b/Dockerfile.prod index f9867759..8221dea6 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -14,13 +14,22 @@ FROM python:3.12-slim AS builder WORKDIR /build +# git is required to resolve the git+https constellation-node-sdk dependency +# (both by `poetry install` below and by the pip fallback) +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + # Install build deps RUN pip install --no-cache-dir poetry && \ poetry config virtualenvs.create false COPY pyproject.toml poetry.lock* ./ -RUN poetry install --no-dev --no-interaction --no-ansi 2>/dev/null || \ - pip install --no-cache-dir fastapi uvicorn neo4j pydantic pydantic-settings pyyaml apscheduler redis structlog numpy scipy RestrictedPython +# --only main: install runtime deps, skip the [dev] group. +# --no-root: install dependencies only, not the l9-engine project itself +# (the "current project" install needs README.md, which isn't in the +# build context yet, and isn't needed — engine/domains/chassis are +# copied as raw source into the runtime stage below, not pip-installed). +RUN poetry install --only main --no-root --no-interaction --no-ansi COPY . . @@ -50,6 +59,6 @@ ENV L9_PROJECT=$L9_PROJECT \ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/v1/health')" + CMD python -c "import json,sys,urllib.request; sys.exit(0 if json.load(urllib.request.urlopen('http://localhost:8000/v1/health')).get('ready', True) else 1)" CMD ["uvicorn", "chassis.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] diff --git a/LICENSE b/LICENSE index ed5a0b41..8762ae8f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,104 @@ -MIT License - -Copyright (c) 2026 L9 Labs (Igor Beylin) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +QUANTUM AI PARTNERS — L9 PROPRIETARY SOFTWARE LICENSE +Version 1.0 — 2026 + +Copyright (c) 2026 Quantum AI Partners ("Licensor"). All rights reserved. + +This software and associated documentation files (the "Software") are the +proprietary and confidential property of Quantum AI Partners. The Software +is made source-available on this repository for transparency, audit, and +evaluation purposes only, under the terms below. NO OPEN-SOURCE LICENSE IS +GRANTED. This is NOT the MIT License, and no prior license grant by Licensor +(if any) shall be construed as a waiver of these terms. + +1. DEFINITIONS + + "Software" means the source code, object code, documentation, domain + specs, configuration, and all other files contained in this repository, + and any modifications or derivative works thereof. + + "Commercial Use" means any use of the Software, in whole or in part, + directly or indirectly, from which any person or entity derives revenue, + profit, cost savings, competitive advantage, or other commercial benefit. + This includes, without limitation: selling, sublicensing, or hosting the + Software or a derivative work; incorporating the Software into a product + or service offered to third parties (including as part of a SaaS, + managed service, or consulting engagement); and internal use by a + for-profit entity in its business operations beyond internal evaluation. + + "You" / "Licensee" means any individual or entity that accesses, clones, + copies, or otherwise makes use of the Software. + +2. LIMITED GRANT + + Subject to Your compliance with this License, Licensor grants You a + limited, non-exclusive, non-transferable, revocable license to view, + clone, and use the Software solely for personal, academic, or internal + evaluation purposes that do NOT constitute Commercial Use. + +3. COMMERCIAL USE REQUIRES A PAID LICENSE + + Any Commercial Use of the Software requires a separate written commercial + license agreement with Quantum AI Partners, negotiated in advance, which + may include license fees, royalties, or a revenue/profit share. Engaging + in Commercial Use without such an agreement is a material breach of this + License and constitutes copyright infringement. + + To request a commercial license, contact: eng@l9.dev + +4. RESTRICTIONS + + Except as expressly permitted under Section 2, You may NOT, without prior + written consent from Licensor: + + a. Copy, reproduce, or redistribute the Software, in source or object + form, to any third party; + b. Modify, create derivative works of, reverse-engineer, or decompile + the Software, except as necessary for permitted evaluation; + c. Sublicense, sell, rent, lease, or otherwise transfer any rights in + the Software; + d. Host, deploy, or offer the Software (or a derivative work) as a + hosted or managed service to any third party; + e. Remove, obscure, or alter any copyright, trademark, or proprietary + notice contained in the Software; + f. Use the Software to build, train, or benchmark a directly competing + product or service. + +5. OWNERSHIP + + The Software is licensed, not sold. Licensor retains all right, title, + and interest in and to the Software, including all intellectual property + rights therein. No rights are granted to You other than as expressly set + forth in this License. + +6. TERMINATION + + This License terminates automatically, without notice, if You breach any + term of this License. Upon termination, You must cease all use of the + Software and destroy all copies in Your possession or control. Sections + 3, 5, 7, and 8 survive termination. + +7. NO WARRANTY + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. + +8. LIMITATION OF LIABILITY + + IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING + FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +9. GOVERNING LAW + + This License shall be governed by the laws of the jurisdiction in which + Quantum AI Partners is organized, without regard to conflict-of-law + principles. [Confirm jurisdiction/venue with counsel before relying on + this in a dispute.] + +--- +NOTE: This template was drafted to match your stated intent (restrict +copying, require payment for commercial/profit-driven use) but has not been +reviewed by an attorney. Have counsel review before relying on it in an +actual dispute or before this repository accepts external contributions. diff --git a/TESTING.md b/TESTING.md index b1caf575..b331a92e 100644 --- a/TESTING.md +++ b/TESTING.md @@ -11,7 +11,7 @@ tests/ unit/ Pure function tests. No Neo4j. No I/O. Fast (<100ms total). integration/ Full pipeline via testcontainers-neo4j. Requires Docker. compliance/ Prohibited factor enforcement. Gate compile-time blocking. - contracts/ All 24 behavioral contracts (via verify_contracts.py). + contracts/ All 24 behavioral contracts, one TestContractNN class each. invariants/ Engine invariants: score bounds, weight sums, determinism. property/ Hypothesis-based property tests for scoring math. ``` @@ -125,11 +125,17 @@ def test_gender_scoring_dimension_blocked(): ## Contract Tests -All 24 contracts are verified via: +All 24 contracts are verified by three complementary gates: + ```bash -python tools/verify_contracts.py +python -m pytest tests/contracts/ # behavioral assertions, one class per contract +python tools/verify_contracts.py # the 27 contract docs exist and are wired +python tools/contract_scanner.py # banned-pattern regex rules ``` +`tests/contracts/test_contract_registry.py` is the drift gate: it fails if a YAML contract, +its `docs:` pointers, its `scanner_rules`, and its `verification.test` node ID ever disagree. + Contracts are documented in `docs/contracts/` and `.claude/rules/contracts.md`. Contract C-001 through C-024 must all pass before any merge. diff --git a/TODO.md b/TODO.md index e7ef2dc3..3c3a59ab 100644 --- a/TODO.md +++ b/TODO.md @@ -19,7 +19,7 @@ status: active ### 2. Preflight Check - [ ] Run `make lint` — ruff check + mypy -- [ ] Run `python tools/verify_contracts.py` — 20 contracts present +- [ ] Run `python tools/verify_contracts.py` — 27 contract docs present and wired - [ ] Run `python tools/contract_scanner.py` — no violations - [ ] Verify `.env` / secrets configured (KUBECONFIG, SLACK_WEBHOOK_URL) - [ ] Review uncommitted changes in git status @@ -57,6 +57,9 @@ status: active ### Automation - [ ] Inject L9_META headers into all files that have it missing using a script +### Tech Debt +- [ ] DEFERRED-003: `contracts/contract_NN.yaml` registry (24 files) doesn't conform to `test_contract_registry.py` schema — missing `docs` field, stale `verification.test` pointers. See `DEFERRED.md`. + --- ## 📝 Notes diff --git a/agents/cursor/cursor_system_prompt.md b/agents/cursor/cursor_system_prompt.md index ae3574a5..c346bc50 100644 --- a/agents/cursor/cursor_system_prompt.md +++ b/agents/cursor/cursor_system_prompt.md @@ -46,7 +46,7 @@ engine/ handlers.py ← ONLY bridge between chassis and engine. Registers all actions. config/ ← Domain spec YAML loader, settings, units domains/ ← Per-vertical domain spec YAMLs (plastics-recycling, etc.) - gates/ ← 14 gate types: GateCompiler, GateType enum, null semantics, registry + gates/ ← 10 gate types: GateCompiler, GateType enum, null semantics, registry scoring/ ← ScoringAssembler, 4 dimensions, temporal decay, scoring explainer traversal/ ← TraversalAssembler, traversal steps, match directions resolver/ ← ParameterResolver, derived parameter computation @@ -61,32 +61,29 @@ tests/ unit/ ← One test file per engine module integration/ ← End-to-end handler flow tests compliance/ ← Security scan, prohibited factor tests -docs/contracts/ ← All 20 contract files (FIELDNAMES, METHODSIGNATURES, etc.) +docs/contracts/ ← All 27 contract docs (FIELD_NAMES, METHOD_SIGNATURES, etc.) --- -## 14 GATE TYPES — IMPLEMENT ALL 14, NEVER SILENTLY SKIP - -The CEG spec defines exactly 14 WHERE gate types. Your GateType enum MUST have 14 values. -Your gate registry MUST map 14 handlers. Unknown gate types MUST raise, never pass-through. - -Gate types (canonical): - 1. exact_match - 2. range_check - 3. enum_membership - 4. geo_radius - 5. taxonomy_overlap - 6. list_intersection - 7. threshold_min - 8. threshold_max - 9. null_check - 10. regex_match - 11. computed_expression ← use safeeval dispatch table, NEVER eval() - 12. graph_affinity - 13. temporal_window - 14. compliance_exclusion - -Self-check: Count your GateType enum values. Count your registry entries. Both must equal 14. +## 10 GATE TYPES — IMPLEMENT ALL 10, NEVER SILENTLY SKIP + +`GateType` in `engine/config/schema.py` has exactly 10 values. Your gate registry MUST map +10 handlers. Unknown gate types MUST raise, never pass-through. + +Gate types (canonical — these are the literal enum values): + 1. range + 2. threshold + 3. boolean + 4. composite + 5. enummap + 6. exclusion + 7. selfrange + 8. freshness + 9. temporalrange + 10. traversal + +Self-check: Count your GateType enum values. Count your registry entries. Both must equal 10. +Asserted by `tests/contracts/test_contracts.py::TestContract13GateThenScore`. --- @@ -393,7 +390,7 @@ docker-compose.prod.yml must NOT expose debug ports or hardcode credentials. [ ] No pickle.loads, no yaml.load without SafeLoader ### COMPLETENESS - [ ] 14 gate types in enum, 14 entries in registry + [ ] 10 gate types in enum, 10 entries in registry [ ] 4 scoring dimensions in ScoringAssembler [ ] All action handlers registered in handlers.py [ ] Zero NotImplementedError / TODO / PLACEHOLDER / FIXME diff --git a/agents/cursor/cursor_workflow_kernel.yaml b/agents/cursor/cursor_workflow_kernel.yaml index 8633c315..6cf46403 100644 --- a/agents/cursor/cursor_workflow_kernel.yaml +++ b/agents/cursor/cursor_workflow_kernel.yaml @@ -1,15 +1,11 @@ -# ============================================================================ -# L9_META -# ============================================================================ -# l9_schema: 1 +# --- L9_META --- +# l9_schema: 2 # origin: l9-template # engine: graph -# layer: [agent-rules, kernel] -# tags: [cursor, kernel, ceg, governance, workflow] -# owner: Founder +# layer: [agent-rules] +# tags: [governance] # status: active -# ============================================================================ - +# --- /L9_META --- # ============================================================================ # CEG CURSOR WORKFLOW KERNEL — BINDING CONTRACT # ============================================================================ @@ -30,7 +26,7 @@ author: Cursor description: > Governs Cursor agent behavior within the Cognitive.Engine.Graphs repository. - Binds Cursor to the 20 contracts in .cursorrules, the CEG file structure, + Binds Cursor to the 24 contracts in contracts/*.yaml, the CEG file structure, the enforcement pipeline, and confidence-based decision logic. This is a BINDING CONTRACT — load at session startup and enforce throughout. @@ -79,7 +75,7 @@ init: This file is NEVER modified by Cursor. session_checks: - - verify: ".cursorrules is loaded and all 20 contracts are in context" + - verify: ".cursorrules is loaded and all 24 contracts are in context" - verify: "GateType enum count = 14 (contract 13)" - verify: "ScoringAssembler has exactly 4 dimensions in scope" - verify: "No open PRs marked BLOCKED in workflow_state.md" @@ -132,7 +128,7 @@ authority: note: Review comments only. No merge authority. # ============================================================================ -# CONTRACT REGISTRY (from .cursorrules — 20 contracts) +# CONTRACT REGISTRY (SSOT: contracts/*.yaml — 24 contracts) # ============================================================================ contracts: layer_1_chassis_boundary: @@ -235,13 +231,13 @@ contracts: rule: > Gates (hard filter) compile to Cypher WHERE clauses — zero Python post-filtering. Scoring dimensions compile to a single WITH/ORDER BY clause. - GateType enum = exactly 14 values. Registry = exactly 14 handlers. + GateType enum = exactly 10 values. Registry = exactly 10 handlers. 10 gate types: range, threshold, boolean, composite, enum_map, exclusion, self_range, freshness, temporal_range, traversal. 9 scoring computations: geo_decay, log_normalized, community_match, inverse_linear, candidate_property, weighted_rate, price_alignment, temporal_proximity, custom_cypher. scanner_ids: [] - invariant: "GateType enum count MUST equal 14 at all times" + invariant: "GateType enum count MUST equal 10 at all times" - id: C14 name: NULL SEMANTICS ARE DETERMINISTIC @@ -290,9 +286,11 @@ contracts: - id: C18 name: L9_META ON EVERY FILE rule: > - Every tracked file carries an L9_META header (schema version 1). - Fields: l9_schema, origin, engine, layer, tags, owner, status. - Injected by tools/l9_meta_injector.py — not manually. + Every tracked file carries an L9_META header (schema version 2). + Fields: l9_schema, origin, engine, layer, tags, status (owner dropped in v2). + Values resolve from l9-meta.yaml by path, not per file: write with + `tools/l9_meta_injector.py apply`, verify with `check`. To change a value, + edit the l9-meta.yaml rule — never the header. scanner_ids: [] layer_6_graph_intelligence: @@ -435,7 +433,7 @@ enforcement_pipeline: layer_4_ci_audit: tool: verify_contracts.py - checks: "all 20 contract docs exist in docs/contracts/ and are wired" + checks: "all 27 contract docs exist in docs/contracts/ and are wired" layer_5_llm_review: tools: [CodeRabbit, Qodo, Claude] @@ -485,7 +483,7 @@ pr_evidence_block: - [ ] GateType enum count → 14 - [ ] ScoringAssembler dimension count → 4 - [ ] All new engine/*.py have tests/unit/test_*.py → confirmed - - [ ] L9_META header on all new files → confirmed + - [ ] python tools/l9_meta_injector.py check → exit 0 missing_evidence_block: "PR is INCOMPLETE — send back, do not review" diff --git a/agents/cursor/governance-reference.md b/agents/cursor/governance-reference.md index 152763ad..e8a008e2 100644 --- a/agents/cursor/governance-reference.md +++ b/agents/cursor/governance-reference.md @@ -112,7 +112,7 @@ chassis/middleware/ ← auth, tenant resolution, rate-limit docker-compose.prod.yml ← production infrastructure Dockerfile.prod ← production container graph-cognitive-engine-spec-v1.1.0.yaml ← canonical domain spec (read-only reference) -docs/contracts/ ← all 20 contract files (FIELDNAMES, etc.) +docs/contracts/ ← all 27 contract docs (FIELD_NAMES, etc.) .github/workflows/ ← CI pipeline definitions .pre-commit-config.yaml ← pre-commit hooks .gitleaks.toml ← secret scanning rules @@ -145,7 +145,7 @@ These invariants hold at all times. Any PR that breaks them is BLOCKED: | Invariant | Rule | |-------------------------------|---------------------------------------------------------------------| -| **14 gate types** | `GateType` enum has exactly 14 values. Registry has exactly 14 handlers. | +| **10 gate types** | `GateType` enum has exactly 10 values. Registry has exactly 10 handlers. | | **4 scoring dimensions** | `ScoringAssembler` computes exactly 4 dimensions per domain spec. | | **Parameterized Cypher only** | Zero f-string values in any Cypher string. All use `$param`. | | **sanitize_label() on labels**| All node/relationship labels f-stringed into Cypher use `sanitize_label()` first. | @@ -231,7 +231,7 @@ Every PR must include in its description: - [ ] grep -rn "NotImplementedError" engine/ → empty - [ ] grep -rn "eval(" engine/ → empty (or only safeeval.py dispatch) - [ ] grep -rn "f\".*{" engine/**/*.py → reviewed, all label-only f-strings -- [ ] Gate count: GateType enum = 14 values, registry = 14 entries +- [ ] Gate count: GateType enum = 10 values, registry = 10 entries - [ ] All new engine/*.py have corresponding tests/unit/test_*.py ``` diff --git a/agents/cursor/prompts/action_prompts/CEG PR Review.md b/agents/cursor/prompts/action_prompts/CEG PR Review.md index ed9f4abf..41ac847a 100644 --- a/agents/cursor/prompts/action_prompts/CEG PR Review.md +++ b/agents/cursor/prompts/action_prompts/CEG PR Review.md @@ -138,7 +138,7 @@ Answer these questions specifically for this PR: If yes: Are they reachable end-to-end (chassis → handlers.py → engine module)? 3. Does this PR modify ScoringAssembler or gate compilation? - If yes: Are the 4 scoring dimensions still intact? Are all 14 gate types still registered? + If yes: Are the 4 scoring dimensions still intact? Are all 10 gate types still registered? 4. Does this PR touch domain spec schema (schema.py)? If yes: Are all new fields snake_case? Do they have defaults or are they Optional? diff --git a/artifacts/harness_report.md b/artifacts/harness_report.md new file mode 100644 index 00000000..45610875 --- /dev/null +++ b/artifacts/harness_report.md @@ -0,0 +1,50 @@ +# L9 Audit Harness Report + +- **Generated:** 2026-07-24T21:03:15.354248+00:00 +- **Repo root:** `/Users/ib-mac/Dropbox/Repo_Dropbox_IB/Cognitive.Engine.Graphs` +- **Overall result:** ✅ PASSED +- **Exit code:** 0 + +## Step Results + +| Step | Status | Exit Code | Notes | +|------|--------|-----------|-------| +| Architecture Audit | ✅ Passed | 0 | | +| Spec Coverage | ✅ Passed | 0 | | +| Contract Wiring | ✅ Passed | 0 | | + +## Architecture Audit Findings + +| Severity | Count | +|----------|-------| +| 🔴 CRITICAL | 0 | +| 🟠 HIGH | 0 | +| 🟡 MEDIUM | 25 | +| 🔵 LOW | 0 | + +See `artifacts/audit_report.md` for full details. + +## Spec Coverage + +- ✅ Implemented: 32 +- ⚠️ Partial: 9 +- ❌ Missing: 0 +- **Total features:** 41 + +| Category | Implemented | Partial | Missing | Total | +|----------|-------------|---------|---------|-------| +| gates | 10 | 0 | 0 | 10 | +| scoring | 7 | 0 | 0 | 7 | +| v1.1_node | 2 | 0 | 0 | 2 | +| v1.1_edge | 2 | 0 | 0 | 2 | +| v1.1_action | 0 | 2 | 0 | 2 | +| v1.1_scoring | 1 | 1 | 0 | 2 | +| action_handler | 0 | 6 | 0 | 6 | +| gds_algorithm | 5 | 0 | 0 | 5 | +| research_pattern | 5 | 0 | 0 | 5 | + +See `artifacts/coverage_report.md` for full details. + +## Next Steps + +All checks passed. Safe to merge. diff --git a/chassis/Dockerfile.chassis b/chassis/Dockerfile.chassis index d7ef9ac8..b30fdd64 100644 --- a/chassis/Dockerfile.chassis +++ b/chassis/Dockerfile.chassis @@ -20,6 +20,10 @@ ARG PYTHON_VERSION=3.12 # ── Stage 1: Dependencies ──────────────────────────────────── FROM python:${PYTHON_VERSION}-slim AS deps +# git is required to resolve the git+https constellation-node-sdk dependency +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /build COPY requirements.txt requirements.txt RUN pip install --no-cache-dir --prefix=/install -r requirements.txt @@ -57,6 +61,6 @@ EXPOSE 8000 # Health check HEALTHCHECK --interval=15s --timeout=5s --retries=3 \ - CMD python -c "import httpx; r=httpx.get('http://localhost:8000/v1/health'); exit(0 if r.status_code==200 else 1)" + CMD python -c "import httpx; r=httpx.get('http://localhost:8000/v1/health'); exit(0 if r.status_code==200 and r.json().get('ready', True) else 1)" ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/chassis/actions.py b/chassis/actions.py index fb6f34bd..3e0929ae 100644 --- a/chassis/actions.py +++ b/chassis/actions.py @@ -36,28 +36,12 @@ def _init_engine() -> None: return try: - from engine.handlers import ( - handle_admin, - handle_enrich, - handle_health, - handle_healthcheck, - handle_match, - handle_outcomes, - handle_resolve, - handle_sync, - ) + from engine.handlers import ACTION_HANDLERS from engine.packet.chassis_contract import deflate_egress, inflate_ingress - _engine_handlers = { - "match": handle_match, - "sync": handle_sync, - "admin": handle_admin, - "outcomes": handle_outcomes, - "resolve": handle_resolve, - "health": handle_health, - "healthcheck": handle_healthcheck, - "enrich": handle_enrich, - } + # CONTRACT-02: consume the single source of truth rather than + # rebuilding the action list here — see engine/handlers.py. + _engine_handlers = dict(ACTION_HANDLERS) _inflate_ingress = inflate_ingress _deflate_egress = deflate_egress logger.info("Engine handlers initialized: %d actions registered", len(_engine_handlers)) diff --git a/chassis/entrypoint.py b/chassis/entrypoint.py new file mode 100644 index 00000000..9a5f6f3b --- /dev/null +++ b/chassis/entrypoint.py @@ -0,0 +1,62 @@ +""" +--- L9_META --- +l9_schema: 2 +origin: chassis +engine: graph +layer: [api] +tags: [platform, chassis] +status: active +--- /L9_META --- + +chassis/entrypoint.py +Single uvicorn target for both chassis implementations. + + L9_CHASSIS=legacy (default) -> chassis.chassis_app.create_app + L9_CHASSIS=sdk -> chassis.node_app.create_app + +Every launch site (scripts/entrypoint.sh, Dockerfile.prod, Makefile) points +at chassis.entrypoint:create_app so switching chassis is a config change, +not a command change. +""" + +from __future__ import annotations + +import logging +import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from fastapi import FastAPI + +logger = logging.getLogger(__name__) + +LEGACY = "legacy" +SDK = "sdk" +_VALID = (LEGACY, SDK) + + +def resolve_chassis() -> str: + """Return the selected chassis name, validating L9_CHASSIS.""" + selected = os.environ.get("L9_CHASSIS", LEGACY).strip().lower() + if selected not in _VALID: + msg = f"L9_CHASSIS must be one of {_VALID}, got {selected!r}" + raise ValueError(msg) + return selected + + +def create_app() -> FastAPI: + """Build the app for the chassis selected by L9_CHASSIS.""" + selected = resolve_chassis() + logger.info("Chassis selected: %s", selected) + + if selected == SDK: + from chassis.node_app import create_app as build_sdk_app + + return build_sdk_app() + + from chassis.chassis_app import create_app as build_legacy_app + + return build_legacy_app() + + +__all__ = ["LEGACY", "SDK", "create_app", "resolve_chassis"] diff --git a/chassis/handler_registration.py b/chassis/handler_registration.py new file mode 100644 index 00000000..fc9de925 --- /dev/null +++ b/chassis/handler_registration.py @@ -0,0 +1,113 @@ +""" +--- L9_META --- +l9_schema: 2 +origin: chassis +engine: graph +layer: [api] +tags: [platform, chassis] +status: active +--- /L9_META --- + +chassis/handler_registration.py +Registers engine.handlers.ACTION_HANDLERS with the constellation-node-sdk +handler registry, wrapping each handler with the PacketEnvelope audit side +effect that chassis/actions.py performs on the legacy path. +""" + +from __future__ import annotations + +import logging +import time +import uuid +from typing import TYPE_CHECKING, Any + +from constellation_node_sdk import register_handler + +from engine.handlers import ACTION_HANDLERS +from engine.packet.chassis_contract import deflate_egress, inflate_ingress +from engine.packet.packet_store import get_packet_store + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + +logger = logging.getLogger(__name__) + +_ENGINE_VERSION = "1.1.0" +_RESPONDING_NODE = "graph-engine" + + +def _with_packet_audit( + action: str, + fn: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]], +) -> Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]: + """Wrap an engine handler so each call emits a request/response packet pair. + + The wrapper keeps an explicit two-parameter signature: the SDK's + ``_invoke_handler`` dispatches on ``len(inspect.signature(handler).parameters)``, + so ``*args`` would silently reroute the call to the one-arg (packet) form. + """ + + async def wrapped(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + trace_id = str(uuid.uuid4()) + start = time.time() + + request_packet = inflate_ingress( + action=action, + payload=payload, + tenant=tenant, + trace_id=trace_id, + source_node="gate", + ) + + try: + engine_data = await fn(tenant, payload) + status = "success" + except Exception: + # Re-raise so the SDK builds a failure TransportPacket, but record + # the failed pair first so the audit trail is not lossy. + response_packet = deflate_egress( + request=request_packet, + engine_data={"error": "handler_failed"}, + status="failed", + processing_ms=(time.time() - start) * 1000, + engine_version=_ENGINE_VERSION, + responding_node=_RESPONDING_NODE, + ) + await _persist(request_packet, response_packet) + raise + + response_packet = deflate_egress( + request=request_packet, + engine_data=engine_data, + status=status, + processing_ms=(time.time() - start) * 1000, + engine_version=_ENGINE_VERSION, + responding_node=_RESPONDING_NODE, + ) + await _persist(request_packet, response_packet) + return engine_data + + wrapped.__name__ = f"{action}_with_packet_audit" + return wrapped + + +async def _persist(request: Any, response: Any) -> None: + """Persist a packet pair; store failures are warnings, never fatal.""" + try: + await get_packet_store().persist(request, response) + except Exception as store_exc: + logger.warning("PacketStore.persist failed (non-fatal): %s", store_exc) + + +def register_engine_handlers() -> None: + """Register every action in ACTION_HANDLERS with the SDK registry.""" + for action, handler in ACTION_HANDLERS.items(): + register_handler(action, _with_packet_audit(action, handler)) + logger.info( + "SDK handler registry populated with %d actions: %s", + len(ACTION_HANDLERS), + ", ".join(ACTION_HANDLERS), + ) + + +__all__ = ["register_engine_handlers"] diff --git a/chassis/node_app.py b/chassis/node_app.py new file mode 100644 index 00000000..03743104 --- /dev/null +++ b/chassis/node_app.py @@ -0,0 +1,163 @@ +""" +--- L9_META --- +l9_schema: 2 +origin: chassis +engine: graph +layer: [api] +tags: [platform, chassis] +status: active +--- /L9_META --- + +chassis/node_app.py +SDK-native chassis: builds the FastAPI app via constellation-node-sdk +create_node_app, with GraphLifecycle adapted to the SDK LifecycleHook. + +Selected by L9_CHASSIS=sdk (see chassis/entrypoint.py). The legacy +chassis/chassis_app.py remains the default until parity tests pass. +""" + +from __future__ import annotations + +import json +import logging +import os +from typing import TYPE_CHECKING, Any + +from constellation_node_sdk import LifecycleHook as SdkLifecycleHook +from constellation_node_sdk import create_node_app +from fastapi.responses import JSONResponse + +from chassis.handler_registration import register_engine_handlers + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + + from fastapi import FastAPI, Request + from starlette.responses import Response + +logger = logging.getLogger(__name__) + +_EXECUTE_PATH = "/v1/execute" + + +def _env_bool(name: str, default: bool) -> bool: + raw = os.environ.get(name) + if raw is None: + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +def _gate_ingress_violation(body: dict[str, Any], gate_node: str) -> str | None: + """Return a rejection reason if the packet is not Gate-authored, else None. + + NodeRuntimeConfig has no gate-only ingress field, so this is enforced here + rather than by the SDK. Checks mirror the Gate's own dispatch-authority + rules: origin_kind == "gate", resolved_by_gate, and source_node == gate. + """ + provenance = body.get("provenance") + address = body.get("address") + if not isinstance(provenance, dict) or not isinstance(address, dict): + return "packet must carry provenance and address" + + origin_kind = str(provenance.get("origin_kind", "")).strip().lower() + if origin_kind != "gate": + return f"provenance.origin_kind must be 'gate', got {origin_kind!r}" + + if not provenance.get("resolved_by_gate", False): + return "provenance.resolved_by_gate must be true" + + source_node = str(address.get("source_node", "")).strip().lower() + if source_node != gate_node: + return f"address.source_node must be {gate_node!r}, got {source_node!r}" + + return None + + +def _install_gate_only_ingress(app: FastAPI, *, gate_node: str) -> None: + """Reject non-Gate-authored packets on /v1/execute before the SDK sees them.""" + + @app.middleware("http") + async def gate_only_ingress( + request: Request, + call_next: Callable[[Request], Awaitable[Response]], + ) -> Response: + if request.method != "POST" or request.url.path != _EXECUTE_PATH: + return await call_next(request) + + raw = await request.body() + + # BaseHTTPMiddleware consumes the request stream; replay it so the + # SDK route can still parse the body. + async def receive() -> dict[str, Any]: + return {"type": "http.request", "body": raw, "more_body": False} + + # Documented Starlette workaround: BaseHTTPMiddleware has no public + # API for replaying a consumed request stream. + request._receive = receive + + try: + body = json.loads(raw) + except ValueError: + # Malformed JSON is the SDK route's 400 to raise, not ours. + return await call_next(request) + + if not isinstance(body, dict): + return await call_next(request) + + reason = _gate_ingress_violation(body, gate_node) + if reason is not None: + logger.warning("Gate-only ingress rejected packet: %s", reason) + return JSONResponse( + status_code=403, + content={"detail": f"gate-only ingress: {reason}"}, + ) + + return await call_next(request) + + +class SdkLifecycleAdapter(SdkLifecycleHook): + """Adapt engine.boot.GraphLifecycle onto the SDK LifecycleHook ABC. + + GraphLifecycle subclasses the *legacy* chassis LifecycleHook. Both + interfaces are startup/shutdown only, but they are unrelated classes, + so the SDK requires an explicit adapter. Keeping the adapter here + leaves engine/boot.py byte-identical during dual-run. + """ + + def __init__(self) -> None: + from engine.boot import GraphLifecycle + + self._inner = GraphLifecycle() + + async def startup(self) -> None: + await self._inner.startup() + + async def shutdown(self) -> None: + await self._inner.shutdown() + + +def create_app() -> FastAPI: + """Build the SDK-native node app with engine handlers registered. + + auto_register_with_gate=False: GraphLifecycle.startup() already calls + register_node_with_gate(), so letting the SDK lifespan also call + register_from_env() would double-register whenever GATE_URL is set. + """ + register_engine_handlers() + logger.info("Building SDK chassis app (L9_CHASSIS=sdk)") + app = create_node_app( + lifecycle_hook=SdkLifecycleAdapter(), + auto_register_with_gate=False, + ) + + if _env_bool("L9_ENFORCE_GATE_ONLY_INGRESS", default=True): + gate_node = os.environ.get("L9_GATE_NODE_NAME", "gate").strip().lower() + _install_gate_only_ingress(app, gate_node=gate_node) + logger.info("Gate-only ingress enforced (gate node: %s)", gate_node) + else: + logger.warning("Gate-only ingress DISABLED - /v1/execute accepts any packet origin") + + return app + + +__all__ = ["SdkLifecycleAdapter", "create_app"] diff --git a/contracts/README.md b/contracts/README.md index b94768ba..903069dc 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -9,10 +9,31 @@ Each YAML file defines one CEG contract with: - **preconditions**: What triggers this contract - **postconditions**: What must be true after - **verification.scanner_rules**: contract_scanner.py rule IDs -- **verification.test**: Test file(s) that verify this contract +- **verification.test**: pytest node ID (`file.py::TestClass`) that verifies this contract +- **docs**: the prose contract doc(s) in `docs/contracts/` covering this invariant + +## Relationship to `docs/contracts/` + +The two folders are one registry split by concern: + +| Folder | Owns | +|---|---| +| `contracts/*.yaml` | Contract **identity and wiring** — id, layer, level, scope, scanner rules, test node ID, docs pointer | +| `docs/contracts/*.md` | **Prose** — the human/agent-facing explanation, correct/wrong examples | + +`tests/contracts/test_contract_registry.py` is the drift gate: it fails if a `docs:` path +does not exist, a `scanner_rules` entry is not registered in `tools/contract_scanner.py`, +a `verification.test` node ID does not resolve, or a required doc is claimed by no +contract. ## Usage -These specs are both documentation and the source of truth for `tools/contract_report.py`. -Run `python tools/contract_report.py` to generate a coverage matrix showing which contracts -have static checks, unit tests, integration tests, and property tests. +```bash +python3 tools/contract_report.py # prints a per-contract verification-coverage table +python3 tools/verify_contracts.py # asserts every required doc exists and is wired +python3 tools/contract_scanner.py # greps the codebase for banned patterns +``` + +`contract_report.py` prints its table to stdout; it writes no artifact. Do not confuse it +with `artifacts/coverage_matrix.json`, which is produced by `tools/spec_extract.py` and +measures **domain-spec feature coverage**, not contract verification coverage. diff --git a/contracts/contract_01.yaml b/contracts/contract_01.yaml index 5e3a1f57..3aabacca 100644 --- a/contracts/contract_01.yaml +++ b/contracts/contract_01.yaml @@ -9,9 +9,12 @@ preconditions: - Any import statement in engine/ code postconditions: - No FastAPI, Starlette, or uvicorn imports present +docs: +- docs/contracts/BANNED_PATTERNS.md verification: scanner_rules: - ARCH-001 - ARCH-002 - ARCH-003 - test: tests/contracts/test_contract_01.py + - SHARED-003 + test: tests/contracts/test_contracts.py::TestContract01SingleIngress diff --git a/contracts/contract_02.yaml b/contracts/contract_02.yaml index b35542a7..f584387e 100644 --- a/contracts/contract_02.yaml +++ b/contracts/contract_02.yaml @@ -10,6 +10,12 @@ preconditions: postconditions: - 'Signature is async def handle_*(tenant: str, payload: dict) -> dict' - Registered via chassis.router.register_handler() +docs: +- docs/contracts/HANDLER_PAYLOADS.md +- docs/contracts/RETURN_VALUES.md +- docs/contracts/METHOD_SIGNATURES.md +- docs/contracts/DEPENDENCY_INJECTION.md verification: - scanner_rules: [] - test: tests/contracts/test_contract_02.py + scanner_rules: + - DI-001 + test: tests/contracts/test_contracts.py::TestContract02HandlerInterface diff --git a/contracts/contract_03.yaml b/contracts/contract_03.yaml index b39bab38..2031cf74 100644 --- a/contracts/contract_03.yaml +++ b/contracts/contract_03.yaml @@ -10,7 +10,9 @@ preconditions: postconditions: - Query scoped to tenant database - No cross-tenant reads +docs: +- docs/contracts/SHARED_MODELS.md verification: scanner_rules: - SHARED-002 - test: tests/contracts/test_contract_03.py + test: tests/contracts/test_contracts.py::TestContract03TenantIsolation diff --git a/contracts/contract_04.yaml b/contracts/contract_04.yaml index 0691b12d..64631a00 100644 --- a/contracts/contract_04.yaml +++ b/contracts/contract_04.yaml @@ -8,10 +8,15 @@ scope: preconditions: - Logging or metrics code in engine/ postconditions: -- Only structlog.get_logger(__name__) used +- Logger obtained via logging.getLogger(__name__) or structlog.get_logger(__name__) - No structlog.configure() or logging.basicConfig() +docs: +- docs/contracts/OBSERVABILITY.md +- docs/contracts/ERROR_HANDLING.md verification: scanner_rules: - OBS-001 - OBS-002 - test: tests/contracts/test_contract_04.py + - ERR-001 + - ERR-002 + test: tests/contracts/test_contracts.py::TestContract04ObservabilityInherited diff --git a/contracts/contract_05.yaml b/contracts/contract_05.yaml index 33c5e9c6..8d61cb52 100644 --- a/contracts/contract_05.yaml +++ b/contracts/contract_05.yaml @@ -10,6 +10,10 @@ preconditions: - Infrastructure file created or modified postconditions: - No Dockerfile, docker-compose, CI pipeline, or Terraform in engine/ +docs: +- docs/contracts/BANNED_PATTERNS.md +- docs/contracts/ENV_VARS.md verification: - scanner_rules: [] - test: tests/contracts/test_contract_05.py + scanner_rules: + - ENV-001 + test: tests/contracts/test_contracts.py::TestContract05InfrastructureIsTemplate diff --git a/contracts/contract_06.yaml b/contracts/contract_06.yaml index 91db9dd2..e5fa70cc 100644 --- a/contracts/contract_06.yaml +++ b/contracts/contract_06.yaml @@ -11,6 +11,14 @@ preconditions: postconditions: - Wrapped via inflate_ingress() at entry - Wrapped via deflate_egress() at exit +docs: +- docs/contracts/PACKET_ENVELOPE_FIELDS.md +- docs/contracts/PACKET_TYPE_REGISTRY.md +- docs/contracts/MEMORY_SUBSTRATE_ACCESS.md +- docs/contracts/FEEDBACK_LOOPS.md verification: - scanner_rules: [] - test: tests/contracts/test_contract_06.py + scanner_rules: + - PKT-001 + - MEM-001 + - MEM-002 + test: tests/contracts/test_contracts.py::TestContract06TransportPacket diff --git a/contracts/contract_07.yaml b/contracts/contract_07.yaml index 4f5c108a..a8742508 100644 --- a/contracts/contract_07.yaml +++ b/contracts/contract_07.yaml @@ -10,9 +10,10 @@ preconditions: postconditions: - Mutations via .derive() only - content_hash SHA-256 UNIQUE constraint +docs: +- docs/contracts/PACKET_ENVELOPE_FIELDS.md +- docs/contracts/SHARED_MODELS.md verification: scanner_rules: - - MEM-001 - - MEM-002 - SHARED-001 - test: tests/contracts/test_contract_07.py + test: tests/contracts/test_contracts.py::TestContract07ImmutabilityPayloadHash diff --git a/contracts/contract_08.yaml b/contracts/contract_08.yaml index 1bd90bfd..0e31b870 100644 --- a/contracts/contract_08.yaml +++ b/contracts/contract_08.yaml @@ -11,8 +11,12 @@ postconditions: - parent_id, root_id set - generation incremented - hop_trace append-only +docs: +- docs/contracts/DELEGATION_PROTOCOL.md +- docs/contracts/PACKET_ENVELOPE_FIELDS.md +- docs/contracts/NODE_REGISTRATION.md verification: scanner_rules: - DEL-001 - DEL-002 - test: tests/contracts/test_contract_08.py + test: tests/contracts/test_contracts.py::TestContract08LineageAudit diff --git a/contracts/contract_09.yaml b/contracts/contract_09.yaml index d23aac1a..20b5dfe5 100644 --- a/contracts/contract_09.yaml +++ b/contracts/contract_09.yaml @@ -10,6 +10,9 @@ preconditions: postconditions: - Labels/types pass sanitize_label() regex ^[A-Za-z_][A-Za-z0-9_]*$ - Values always parameterized ($batch, $query) +docs: +- docs/contracts/CYPHER_SAFETY.md +- docs/contracts/BANNED_PATTERNS.md verification: scanner_rules: - SEC-001 @@ -19,4 +22,4 @@ verification: - SEC-005 - SEC-006 - SEC-007 - test: tests/contracts/test_contract_09.py + test: tests/contracts/test_contracts.py::TestContract09CypherInjectionPrevention diff --git a/contracts/contract_10.yaml b/contracts/contract_10.yaml index 5ea5bed8..08b940cb 100644 --- a/contracts/contract_10.yaml +++ b/contracts/contract_10.yaml @@ -11,6 +11,8 @@ preconditions: postconditions: - Protected fields blocked at compile-time - Violation logged via audit_on_violation +docs: +- docs/contracts/PROHIBITED_FACTORS.md verification: scanner_rules: [] - test: tests/compliance/ + test: tests/contracts/test_contracts.py::TestContract10ProhibitedFactors diff --git a/contracts/contract_11.yaml b/contracts/contract_11.yaml index b81eed07..298a932b 100644 --- a/contracts/contract_11.yaml +++ b/contracts/contract_11.yaml @@ -10,6 +10,8 @@ preconditions: postconditions: - 'Handling per spec: hash|encrypt|redact|tokenize' - Engine never logs PII values +docs: +- docs/contracts/PII_HANDLING.md verification: scanner_rules: [] - test: tests/compliance/ + test: tests/contracts/test_contracts.py::TestContract11PIIHandling diff --git a/contracts/contract_12.yaml b/contracts/contract_12.yaml index a2a7fb2a..f96cb613 100644 --- a/contracts/contract_12.yaml +++ b/contracts/contract_12.yaml @@ -10,6 +10,11 @@ preconditions: postconditions: - All behavior from domain_spec.yaml via DomainConfig Pydantic - Never raw YAML or dicts +docs: +- docs/contracts/PYDANTIC_YAML_MAPPING.md +- docs/contracts/FIELD_NAMES.md +- docs/contracts/DOMAIN_SPEC_VERSIONING.md verification: - scanner_rules: [] - test: tests/contracts/test_contract_12.py + scanner_rules: + - NAME-001 + test: tests/contracts/test_contracts.py::TestContract12DomainSpecSourceOfTruth diff --git a/contracts/contract_13.yaml b/contracts/contract_13.yaml index fdb183d0..4a52de38 100644 --- a/contracts/contract_13.yaml +++ b/contracts/contract_13.yaml @@ -12,6 +12,9 @@ postconditions: - Gates compile to Cypher WHERE - Scoring compiles to WITH/ORDER BY - 10 gate types, 13 scoring computations +docs: +- docs/contracts/METHOD_SIGNATURES.md +- docs/contracts/FIELD_NAMES.md verification: scanner_rules: [] - test: tests/contracts/test_contract_13.py + test: tests/contracts/test_contracts.py::TestContract13GateThenScore diff --git a/contracts/contract_14.yaml b/contracts/contract_14.yaml index dac4f959..fc588149 100644 --- a/contracts/contract_14.yaml +++ b/contracts/contract_14.yaml @@ -10,6 +10,9 @@ preconditions: postconditions: - 'null_behavior: pass wraps in IS NULL OR predicate' - 'null_behavior: fail rejects NULL' +docs: +- docs/contracts/FIELD_NAMES.md +- docs/contracts/PYDANTIC_YAML_MAPPING.md verification: scanner_rules: [] - test: tests/unit/ + test: tests/contracts/test_contracts.py::TestContract14NullSemantics diff --git a/contracts/contract_15.yaml b/contracts/contract_15.yaml index de720261..1b58f2eb 100644 --- a/contracts/contract_15.yaml +++ b/contracts/contract_15.yaml @@ -9,6 +9,8 @@ preconditions: - 'Gate with invertible: true' postconditions: - candidate_prop and query_param swapped when direction reverses +docs: +- docs/contracts/BIDIRECTIONAL_MATCHING.md verification: scanner_rules: [] - test: tests/unit/ + test: tests/contracts/test_contracts.py::TestContract15BidirectionalMatching diff --git a/contracts/contract_16.yaml b/contracts/contract_16.yaml index 30d1aa2f..494f44fc 100644 --- a/contracts/contract_16.yaml +++ b/contracts/contract_16.yaml @@ -9,6 +9,8 @@ preconditions: - New directory created postconditions: - No new top-level directories without architectural approval +docs: +- docs/contracts/BANNED_PATTERNS.md verification: scanner_rules: [] - test: tests/contracts/test_contract_16.py + test: tests/contracts/test_contracts.py::TestContract16FileStructure diff --git a/contracts/contract_17.yaml b/contracts/contract_17.yaml index 8d296911..8bdfec55 100644 --- a/contracts/contract_17.yaml +++ b/contracts/contract_17.yaml @@ -5,12 +5,21 @@ level: MUST scope: paths: - tests/**/*.py + - engine/**/*.py preconditions: - Code change submitted postconditions: - Unit tests for pure functions - Integration with testcontainers-neo4j - Performance <200ms p95 +- No stubs in engine/ - a NotImplementedError, TODO, or PLACEHOLDER is an untestable path; + ship the code or record it in DEFERRED.md +docs: +- docs/contracts/TEST_PATTERNS.md +- docs/contracts/BANNED_PATTERNS.md verification: - scanner_rules: [] - test: tests/ + scanner_rules: + - STUB-001 + - STUB-002 + - STUB-003 + test: tests/contracts/test_contracts.py::TestContract17TestRequirements diff --git a/contracts/contract_18.yaml b/contracts/contract_18.yaml index 6d96cd83..fd6e2989 100644 --- a/contracts/contract_18.yaml +++ b/contracts/contract_18.yaml @@ -1,15 +1,37 @@ +# --- L9_META --- +# l9_schema: 2 +# origin: l9-template +# engine: graph +# layer: [contracts] +# tags: [governance, compliance] +# status: active +# --- /L9_META --- id: CONTRACT-18 name: L9_META Headers layer: testing level: SHOULD scope: + # Every tracked file with a known format, not just engine/ and chassis/. + # The eligible set is computed by tools/l9_meta/discover.py from the git index + # minus config `exclude`; enumerating it here would go stale immediately. paths: - - engine/**/*.py - - chassis/**/*.py + - '**/*.py' + - '**/*.yaml' + - '**/*.yml' + - '**/*.md' + - '**/*.json' + - '**/*.toml' + - '**/*.sh' + - Dockerfile* + - Makefile preconditions: - New file created postconditions: -- L9_META header present (schema v1) +- L9_META header present (schema v2; `owner` dropped, zero consumers verified) +- Values match l9-meta.yaml resolution (`l9-meta check` exits 0) +docs: +- docs/contracts/L9_META_HEADERS.md verification: scanner_rules: [] - test: tools/l9_meta_injector.py + test: tests/contracts/test_contracts.py::TestContract18L9Meta + command: python tools/l9_meta_injector.py check diff --git a/contracts/contract_19.yaml b/contracts/contract_19.yaml index f1bd004d..ec8866e1 100644 --- a/contracts/contract_19.yaml +++ b/contracts/contract_19.yaml @@ -11,6 +11,9 @@ postconditions: - Declared in spec.gds_jobs - 'Schedule type: cron|manual' - Projections spec-driven +docs: +- docs/contracts/METHOD_SIGNATURES.md +- docs/contracts/FIELD_NAMES.md verification: scanner_rules: [] - test: tests/unit/ + test: tests/contracts/test_contracts.py::TestContract19GDSDeclarative diff --git a/contracts/contract_20.yaml b/contracts/contract_20.yaml index 204472a6..c262fd04 100644 --- a/contracts/contract_20.yaml +++ b/contracts/contract_20.yaml @@ -10,6 +10,8 @@ preconditions: postconditions: - CompoundE3D 256-dim - Domain-specific, never cross-tenant +docs: +- docs/contracts/KGE_EMBEDDINGS.md verification: scanner_rules: [] - test: tests/unit/ + test: tests/contracts/test_contracts.py::TestContract20KGEEmbeddings diff --git a/contracts/contract_21.yaml b/contracts/contract_21.yaml index 780063ff..6a6927da 100644 --- a/contracts/contract_21.yaml +++ b/contracts/contract_21.yaml @@ -11,6 +11,8 @@ postconditions: - Gated by bool flag in settings.py - True for safety, False for experimental - Documented in FEATURE_GATES.md +docs: +- docs/contracts/FEATURE_FLAG_DISCIPLINE.md verification: scanner_rules: [] - test: tests/contracts/test_contract_21.py + test: tests/contracts/test_contracts.py::TestContract21FeatureFlagDiscipline diff --git a/contracts/contract_22.yaml b/contracts/contract_22.yaml index e48c9b6d..51e9b299 100644 --- a/contracts/contract_22.yaml +++ b/contracts/contract_22.yaml @@ -11,6 +11,8 @@ preconditions: postconditions: - Sum of defaults <= 1.0 - Enforced by _assert_default_weight_sum() +docs: +- docs/contracts/SCORING_WEIGHT_CEILING.md verification: scanner_rules: [] - test: tests/contracts/test_contract_22.py + test: tests/contracts/test_contracts.py::TestContract22ScoringWeightCeiling diff --git a/contracts/contract_23.yaml b/contracts/contract_23.yaml index d1424ae5..0c9bf572 100644 --- a/contracts/contract_23.yaml +++ b/contracts/contract_23.yaml @@ -12,6 +12,8 @@ postconditions: - Returns {status, subaction} - Logs with tenant/trace_id - Validates with _require_key() +docs: +- docs/contracts/HANDLER_PAYLOADS.md verification: scanner_rules: [] - test: tests/contracts/test_contract_23.py + test: tests/contracts/test_contracts.py::TestContract23AdminSubactionRegistration diff --git a/contracts/contract_24.yaml b/contracts/contract_24.yaml index 957570ee..010489cf 100644 --- a/contracts/contract_24.yaml +++ b/contracts/contract_24.yaml @@ -12,6 +12,9 @@ postconditions: - Circuit breaker 3/30s - Caches bounded + TTL - No module-level globals +docs: +- docs/contracts/DEPENDENCY_INJECTION.md +- docs/contracts/METHOD_SIGNATURES.md verification: scanner_rules: [] - test: tests/contracts/test_contract_24.py + test: tests/contracts/test_contracts.py::TestContract24ResiliencePatterns diff --git a/docs/AUDIT_HARNESS.md b/docs/AUDIT_HARNESS.md index e52b88c8..c4856d42 100644 --- a/docs/AUDIT_HARNESS.md +++ b/docs/AUDIT_HARNESS.md @@ -26,7 +26,7 @@ make harness | **Cypher injection pattern detection** | Regex-scans f-strings for label interpolation without `sanitize_label()` | Flags `f"MERGE (n:{spec.target_node} ...)"` with evidence snippet | | **Lifecycle anchor verification** | Checks that match/sync/GDS flows reference expected components | Warns if `GateCompiler`, `TraversalAssembler`, `ScoringAssembler` aren't referenced in handler chain | | **Spec coverage scanning** | Extracts features from spec YAML, scans codebase for implementation evidence | Reports which gates, scoring types, ontology nodes are IMPLEMENTED / PARTIAL / MISSING | -| **Contract wiring verification** | Checks all 20 contract docs exist and are referenced in `.cursorrules` / `CLAUDE.md` | Fails if `FIELD_NAMES.md` exists but isn't wired into agent rules | +| **Contract wiring verification** | Checks all 27 contract docs exist and are referenced in `.cursorrules` / `CLAUDE.md` / `AGENTS.md` | Fails if `FIELD_NAMES.md` exists but isn't wired into agent rules | | **Evidence-based output** | Every finding includes file path + line range + 7-line code snippet | You see the exact code, not a vague description | | **Severity-gated CI exit codes** | Returns exit code 1 if CRITICAL or HIGH findings exist | CI blocks merge; `make harness` fails locally | | **Consolidated report** | Writes `artifacts/harness_report.md` combining all step results | Single doc for PR review | diff --git a/docs/CI_CONSTELLATION_BOUNDARY.md b/docs/CI_CONSTELLATION_BOUNDARY.md new file mode 100644 index 00000000..11d5b914 --- /dev/null +++ b/docs/CI_CONSTELLATION_BOUNDARY.md @@ -0,0 +1,126 @@ + + +# CI_CONSTELLATION_BOUNDARY.md — Quantum-L9 CI Constellation Boundary + +**Read this before wiring `l9-ci-core`, `l9-ci-sdk`, `l9-harness`, or `l9-assurance` +into this repo, or before proposing any change that touches +`tools/audit_harness.py`, `.github/workflows/audit.yml`, or +`.github/workflows/baseline-ratchet-caller.yml`.** + +## The non-negotiable rule + +> **Extend, never replace.** This repo's own audit/CI tooling +> (`tools/audit_harness.py`, `.github/workflows/audit.yml`) is not a stand-in +> for the constellation and must not be deleted, gutted, or silently +> superseded when constellation adoption expands. If/when `l9-harness` is +> ever adopted here, it sits **above** `audit.yml`, orchestrating the +> existing harness as one observation source among several — it does not +> absorb, rewrite, or disable it. Any change that would remove or bypass +> `audit_harness.py`'s CI-gating role requires an explicit human decision, +> not an autonomous agent action. + +## Current state: what's already wired (real, not theoretical) + +Two of the four constellation repos are already integrated — narrowly, for +one purpose each, both pinned to immutable commit SHAs, both following the +"thin caller" pattern (this repo supplies inputs only; all logic lives +upstream): + +| Constellation repo | Where it's wired | Purpose | Pinned revision | +|---|---|---|---| +| `Quantum-L9/l9-ci-core` | `.github/workflows/baseline-ratchet-caller.yml` (reusable workflow call) | Baseline Ratchet — 4 branch-protection checks: `Required Tests`, `Quarantined Debt`, `Workflow Integrity`, `Ratchet Verdict` | `d81a06ed821106a487df2e5ad06d93e347392af6` | +| `Quantum-L9/l9-ci-sdk` | `tools/packet_envelope_gate.py` (provisioned via shallow clone into gitignored `.l9/runtime/sdk/`) | `l9_ci baseline scan-packet-envelope` + `compare-scan` — the deterministic AST scanner behind the `packet-envelope-prohibited` pre-commit hook and the `Quarantined Debt` CI check | `0779fca8238011f8abea551895f96584676e9d17` | + +Governed ledgers (human-owned, CODEOWNERS-protected, **never** written by CI +or an agent): + +- `.l9/baselines/packet-envelope.yml` — deprecated `PacketEnvelope` debt, one + entry per owner/issue/expiry (see `docs/contracts/SHARED_MODELS.md` for the + `TransportPacket` migration this ledger tracks) +- `.l9/baselines/test-quarantine.yml` — pre-existing test debt; entries may + only shrink, never grow + +**Scope of this integration: narrow.** It exists solely to run the +PacketEnvelope-debt ratchet and test-quarantine ratchet. It does **not** +mean "CI runs through the constellation" — `ci.yml`, `ci-quality.yml`, and +`audit.yml` remain fully independent, CEG-owned pipelines. See +`docs/CI_PIPELINE.md` for the full 8-phase pipeline this sits inside. + +## What's NOT wired: `l9-harness` and `l9-assurance` + +Neither repo is integrated here. The only mention of `l9-harness` anywhere +in this repo is a passing name in a loop in +`docs/github-ruleset-diagnostics.md` (an org-wide ruleset audit) — not a +functional call, dependency, or CLI invocation. + +| | `Quantum-L9/l9-ci-core` + `l9-ci-sdk` (wired above) | `Quantum-L9/l9-harness` (not wired) | +|---|---|---| +| Role in the 4-party authority model | CI Core orchestrates/publishes; CI SDK executes checks and emits observations | Exercises public contracts, preserves bytes, deterministic local execution/replay/shadow-comparison — explicitly **never** a required hop, never publishes GitHub checks, never issues verdicts | +| Scope | One narrow gate (PacketEnvelope + test-quarantine debt ratchets) | Whole-constellation conformance/replay/corpus-sync tool, if ever adopted | +| Maturity (as of 2026-07-24) | Actively running, gating merges today | Private repo, created 2026-07-04, self-described fail-closed pending upstream authority (`BUILD_AUTHORIZATION.md`, `VALIDATION.md`) | +| `l9-assurance` | Not present at all — that's the party that would eventually "admit evidence and issue verdicts," a role currently played ad hoc by the `CI Gate` / `quality-gate` fan-in jobs in `ci.yml` / `ci-quality.yml` | Not present | + +Full technical comparison against CEG's own `tools/audit_harness.py` +(different tool, different purpose — a single-repo static-analysis +orchestrator, not a constellation component) is preserved in the session +transcript; ask for it again if needed rather than assuming this doc +duplicates it. + +## If an agent is asked to instantiate `l9-harness` or `l9-assurance` here + +1. **Do not touch** `tools/audit_harness.py`, `docs/AUDIT_HARNESS.md`, or the + `Post harness report to PR` step in `.github/workflows/audit.yml` as part + of that work. Those stay as-is regardless of constellation adoption. +2. **Follow the existing pattern exactly** — it is already proven in this + repo: + - Pin the constellation repo to an immutable commit SHA (never a branch + or tag that can move). + - Add a **thin wrapper/caller** (see `tools/packet_envelope_gate.py` and + `.github/workflows/baseline-ratchet-caller.yml`) that supplies + repo-specific inputs only — zero scanning/comparison logic lives + locally. + - Provision any cloned SDK/harness code into a gitignored path under + `.l9/runtime/` (see `.gitignore` line for `.l9/runtime/`) — never + commit vendored constellation code. + - Any ledger the new gate produces is human-owned and CODEOWNERS-gated + (see `.github/CODEOWNERS` entry for `/.l9/baselines/`); CI and agents + read ledgers, never write them. +3. **Layer above `audit.yml`, don't fold into it.** If `l9-harness` reaches + the point of orchestrating local execution/replay across this repo's + checks, treat `audit_harness.py`'s three checks (`audit_engine.py`, + `spec_extract.py`, `verify_contracts.py`) as one SDK-level observation + source it *collects*, not a target it rewrites or a workflow it deletes. +4. **`l9-assurance` adoption is a bigger decision than the others.** It + would change who "issues verdicts" for this repo — currently that's the + `CI Gate` fan-in in `ci.yml` and the `quality-gate` fan-in in + `ci-quality.yml` (see `docs/CI_PIPELINE.md`). Do not adopt it + autonomously; this requires an explicit human decision given its + pre-production status upstream (`l9-harness`'s own docs list required + authority records — release commit, schema digests, SDK identity — as + not yet supplied). +5. If genuinely unsure whether a proposed change counts as "instantiating + the constellation" vs. "extending the existing narrow integration," + stop and ask rather than guessing. + +## Related documents + +- `docs/CI_PIPELINE.md` — the 8-phase CI pipeline this integration sits + inside; source of truth for blocking vs. advisory jobs +- `docs/AUDIT_HARNESS.md` — what `tools/audit_harness.py` does and doesn't do + (L9 template doc, shared across L9 repos — do not add constellation-specific + content there; it belongs here instead) +- `docs/contracts/SHARED_MODELS.md` — the `PacketEnvelope` → `TransportPacket` + migration the packet-envelope ledger tracks +- `docs/github-ruleset-diagnostics.md` — the org-wide ruleset rollout + investigation where `l9-harness` was mentioned only as a repo name, not a + dependency +- `.claude/rules/system-state.md` — tracks `l9-harness`/`l9-assurance` as + dormant (unwired) constellation members diff --git a/docs/Commands.md b/docs/Commands.md new file mode 100644 index 00000000..1d6cb9d2 --- /dev/null +++ b/docs/Commands.md @@ -0,0 +1,59 @@ +what are the prerequisites for a full Org-level ruleset enforcement and how to activate it? + +total: 39 +archived: 0 +forks: 0 +private: 10 +public: 29 + "html": { + "href": "http://github.com/organizations/Quantum-L9/settings/policies/repositories/18226001" + } + } +} +python3 << 'EOF' +import json, subprocess +prs = json.load(open("/tmp/pr_map.json")) +# add l9-tools +prs.append(("l9-tools", "1")) + +summary = [] +for repo, num in prs: + r = subprocess.run( + ["gh", "pr", "checks", num, "--repo", f"Quantum-L9/{repo}", "--json", "name,state,bucket"], + capture_output=True, text=True + ) + if r.returncode != 0: + summary.append((repo, num, "API_ERROR")) + continue + try: + checks = json.loads(r.stdout) + except json.JSONDecodeError: + summary.append((repo, num, "PARSE_ERROR")) + continue + total = len(checks) + failing = [c["name"] for c in checks if c.get("bucket") == "fail"] + pending = [c["name"] for c in checks if c.get("bucket") == "pending"] + summary.append((repo, num, f"total={total} failing={len(failing)} pending={len(pending)}", failing)) + +for repo, num, stat, *rest in summary: + fail_list = rest[0] if rest else [] + print(f"{repo:<35} #{num:<5} {stat}") + if fail_list: + print(" failing:", ", ".join(fail_list)) +EOF + + + +gh repo list Quantum-L9 --limit 300 --json name -q '.[].name' > /tmp/all_org_repos.txt +python3 << 'EOF' +import json +results = json.load(open("/tmp/l9_ci_rollout_results.json")) +covered = {r["repo"] for r in results} | {"l9-tools"} +all_repos = set(open("/tmp/all_org_repos.txt").read().splitlines()) +not_covered = sorted(all_repos - covered) +print("total org repos:", len(all_repos)) +print("covered by rollout:", len(covered)) +print("NOT covered:", len(not_covered)) +for r in not_covered: + print(" -", r) +EOF diff --git a/docs/FEATURE_GATES.md b/docs/FEATURE_GATES.md index 00d8933b..8899e7fb 100644 --- a/docs/FEATURE_GATES.md +++ b/docs/FEATURE_GATES.md @@ -41,6 +41,9 @@ and rollback procedure. | Strict Null Gates | `STRICT_NULL_GATES` | `True` | active | | Param Strict Mode | `PARAM_STRICT_MODE` | `True` | active | | LLM Security (ValidatedLLMClient) | `LLM_PROVIDER` | — | stub | +| Outcome Persistence | `OUTCOME_PERSISTENCE_ENABLED` | `False` | dormant | +| Tenant Auth (JWT `allowed_tenants`) | `TENANT_AUTH_ENABLED` | `True` | active | +| Capability Auth (domain-spec model) | `CAPABILITY_AUTH_ENABLED` | `True` | active | | Constellation Orchestration | — | — | dormant | | PostgreSQL Persistence | — | — | dormant | @@ -189,6 +192,39 @@ the chassis bridge directly. --- +## 10. Outcome Persistence (W2-02b) + +**State**: Dormant +**Flag**: `OUTCOME_PERSISTENCE_ENABLED=True` +**Prerequisites**: PacketStore reachable (`PACKET_STORE_ENABLED`, `PACKET_STORE_DSN`). + +Writes match outcomes through to the PacketStore. This is a second gate on top of +`FEEDBACK_ENABLED`: the feedback loop can run in-memory without it, and enabling it +without a reachable PacketStore degrades to logged warnings rather than hard failure. + +--- + +## 11. Tenant Auth (W3-01) + +**State**: Active +**Flag**: `TENANT_AUTH_ENABLED=True` (default on) + +Enforces the JWT `allowed_tenants` claim against the resolved tenant. Setting this to +`False` disables that check — acceptable only for single-tenant local development. + +--- + +## 12. Capability Auth (W3-02 / W3-03) + +**State**: Active +**Flag**: `CAPABILITY_AUTH_ENABLED=True` (default on) + +Enforces the domain-spec capability model, mapping each action to the permissions it +requires. Disabling it removes per-action authorization while leaving tenant resolution +intact. + +--- + ## Querying Feature Status Use the `feature_status` admin subaction to get current state of all gates: diff --git a/docs/L9_Contract_Enforcement_System.md b/docs/L9_Contract_Enforcement_System.md index f0af9e5a..412fdcf4 100644 --- a/docs/L9_Contract_Enforcement_System.md +++ b/docs/L9_Contract_Enforcement_System.md @@ -1,22 +1,21 @@ # L9 Contract Enforcement System -## Making 20 Contracts Enforced Law — Not Aspirational Guidelines +## Making 24 Contracts Enforced Law — Not Aspirational Guidelines ### Version 1.0.0 | 2026-03-01 --- ## The Problem -You have 20 contracts. Agents read them *if they feel like it*. Nothing stops a PR +You have 24 contracts. Agents read them *if they feel like it*. Nothing stops a PR with `eval()`, a redefined `PacketEnvelope`, or a hand-rolled `httpx.post()` to another node from merging. The contracts are documentation, not law. @@ -50,7 +49,7 @@ Developer / Agent writes code | passes v +-------------------------+ -| CI - contract audit | <- Verifies all 20 files exist & unmodified +| CI - contract audit | <- Verifies all 27 docs exist & are wired | | <- Verifies no contract violations in code | | <- Blocks merge on ANY finding +-----------+-------------+ @@ -58,7 +57,7 @@ Developer / Agent writes code v +-------------------------+ | 3-LLM PR Review | <- CodeRabbit + Qodo + Claude -| (configured with | <- Each reviewer knows the 20 contracts +| (configured with | <- Each reviewer knows the 24 contracts | contract awareness) | <- Blocks merge on CRITICAL findings +-----------+-------------+ | all pass @@ -111,13 +110,13 @@ repos: ## Layer 2: `tools/contract_scanner.py` (The Enforcer) -This single script encodes ALL 20 contracts as scannable regex rules. +This script encodes the mechanically detectable subset of the 24 contracts as regex rules. Runs on pre-commit (per-file) and in CI (full repo). Exit 1 = blocked. ```python """ L9 Contract Violation Scanner -Encodes all 20 contracts as grep-able rules. +Encodes the grep-able subset of the 24 contracts as regex rules. Exit code 1 = violations found = commit/merge blocked. """ @@ -300,7 +299,7 @@ the architecture section above. Each rule maps to one contract. ## Layer 3: `tools/verify_contracts.py` (File Existence + Wiring) -Verifies all 20 contract files exist AND are referenced in `.cursorrules` +Verifies all 27 contract docs exist AND are referenced in `.cursorrules` and `CLAUDE.md`. Blocks CI if any are missing or unwired. ```python @@ -449,11 +448,12 @@ reviews: | 18 | OBSERVABILITY.md | OBS-001, OBS-002 | Yes | Yes | Yes | | 19 | MEMORY_SUBSTRATE_ACCESS.md | MEM-001, MEM-002 | Yes | Yes | Yes | | 20 | SHARED_MODELS.md | SHARED-001 to 003 | Yes | Yes | Yes | -| - | All 20 files exist | verify_contracts.py | Yes | Yes | - | -| - | All 20 wired in CLAUDE.md | verify_contracts.py | Yes | Yes | - | +| - | All 27 docs exist | verify_contracts.py | Yes | Yes | - | +| - | All 27 wired in agent files | verify_contracts.py | Yes | Yes | - | +| - | YAML ↔ docs ↔ rules ↔ tests agree | test_contract_registry.py | Yes | Yes | - | | - | Zero-Stub Protocol | STUB-001 to 003 | Yes | Yes | Yes | -**16 of 20 contracts have automated scanner rules. The other 4 are semantic +**10 of 24 contracts have automated scanner rules. The other 14 are semantic (field names, method signatures, test patterns, domain versioning) and are enforced by mypy, pytest, and LLM review.** diff --git a/docs/SEL4_UPGRADES.md b/docs/SEL4_UPGRADES.md index f5f160ba..c76a22e0 100644 --- a/docs/SEL4_UPGRADES.md +++ b/docs/SEL4_UPGRADES.md @@ -162,7 +162,7 @@ Of these 39 concepts, the reality filter classified: | ID | Enhancement | Technical Mechanism | |----|------------|---------------------| -| W5-01 | Contract Verification Test Suite | `tests/contracts/test_contracts.py`: One test per CEG contract (20 contracts from `.cursorrules`). Tests exercise contract boundaries — e.g., Contract 1 asserts no FastAPI import in engine namespace, Contract 9 asserts startup weight-sum assertion fires on violation. | +| W5-01 | Contract Verification Test Suite | `tests/contracts/test_contracts.py`: One test class per CEG contract (24 invariants from `contracts/*.yaml`). Tests exercise contract boundaries — e.g., Contract 1 asserts no FastAPI import in engine namespace, Contract 9 asserts startup weight-sum assertion fires on violation. | | W5-02 | Invariant Regression Tests | `tests/invariants/`: 31 regression tests, one per audit defect. Each reproduces the exact defect trigger condition, asserts the fix holds. Tagged with `@pytest.mark.finding("T1-03")`. CI gate: all invariant tests must pass on every PR. | | W5-03 | Score Quality Benchmark Suite | `tests/scoring/benchmark.py`: Runs labeled test graphs through full match pipeline. Records: good-pair average, bad-pair average, separation (good_avg − bad_avg), distribution moments (mean, std, skew). CI fails if separation < 0.20 or std < 0.05. | | W5-04 | Domain-Spec Validation Tool | `tools/validate_domain.py`: Standalone CLI: `python -m ceg.tools.validate_domain path/to/spec.yaml [--strict]`. Runs all W1-01 through W1-04 validators, outputs structured pass/fail report with YAML path references. Pre-commit hook integration. | @@ -200,7 +200,7 @@ Of these 39 concepts, the reality filter classified: | `engine/auth/capabilities.py` | W3 | Capability model, derivation trees, delegation | | `engine/state.py` | W4 | EngineState dataclass — centralized mutable state | | `engine/errors.py` | W6 | FeatureNotEnabled error class | -| `tests/contracts/test_contracts.py` | W5 | 20 contract verification tests | +| `tests/contracts/test_contracts.py` | W5 | 24 contract verification test classes | | `tests/invariants/` | W5 | 31 invariant regression tests | | `tests/scoring/benchmark.py` | W5 | Score quality benchmark suite | | `tests/property/test_gates.py` | W5 | Property-based gate tests (Hypothesis) | @@ -267,7 +267,7 @@ All flags are added to `engine/config/settings.py` and controllable via environm | W2 | 71 | Unit/Integration | Calibration detects out-of-range scores, feedback gradient ±0.02, normalization preserves ranking, monoculture flagged | | W3 | 54 | Unit/Integration | Unauthorized tenant → 403, missing capability → 403, delegation creates Neo4j edge, revocation removes edge | | W4 | 37 | Unit/Integration | EngineState singleton, circuit opens after 3 failures, TTL cache evicts after 30s, flush triggers at buffer limit | -| W5 | 92 | Contract/Invariant/Property/Benchmark | 20 contracts verified, 31 regressions covered, score separation > 0.20, property tests for all weight vectors | +| W5 | 92 | Contract/Invariant/Property/Benchmark | 24 contracts verified, 31 regressions covered, score separation > 0.20, property tests for all weight vectors | | W6 | 30 | Unit/Integration | KGE activation smoke-test, erasure idempotency, GDS history returns entries, feature_status reports all gates | | **Total** | **316** | | | diff --git a/docs/contracts/BANNED_PATTERNS.md b/docs/contracts/BANNED_PATTERNS.md index c2574267..42efa004 100644 --- a/docs/contracts/BANNED_PATTERNS.md +++ b/docs/contracts/BANNED_PATTERNS.md @@ -59,3 +59,70 @@ Quick-reference for ALL agents. If you see yourself writing any of these, STOP. | `Field(alias="...")` | Use snake_case directly | PYDANTIC_YAML_MAPPING.md | | `candidateprop` | `field` (per GateSpec) | FIELD_NAMES.md contract | ``` + +## Infrastructure (CONTRACT-05) + +The engine never authors infrastructure. Dockerfiles, `docker-compose.yml`, CI pipelines, +Terraform modules, and k8s manifests all live in `l9-template`. The only infrastructure +surface the engine owns is adding engine-specific variables to `.env.template` +(see [ENV_VARS.md](ENV_VARS.md)). + +| ❌ Banned in this repo | ✅ Instead | +|---|---| +| Creating a new `Dockerfile` / `docker-compose.yml` | Use the template's | +| Adding a `.github/workflows/*.yml` build pipeline | Use the template's | +| Terraform / Helm / k8s manifests | `l9-iac` template | + +## File Structure (CONTRACT-16) + +The `engine/` layout is fixed. Each concern has exactly one home: + +| Path | Owns | +|---|---| +| `engine/handlers.py` | The **only** chassis bridge (`register_all`) | +| `engine/config/` | Domain spec schema, loader, settings, units | +| `engine/gates/` | Gate compiler, null semantics, registry, `types/` | +| `engine/scoring/` | Scoring assembler | +| `engine/traversal/` | Traversal assembler and resolver | +| `engine/sync/` | Sync generator | +| `engine/gds/` | GDS scheduler | +| `engine/graph/` | Neo4j async driver wrapper | +| `engine/compliance/` | Prohibited factors, PII, audit | +| `engine/packet/` | PacketEnvelope bridge | +| `engine/utils/` | `safe_eval`, `sanitize_label` | + +Do not create new top-level directories under `engine/` without architectural approval. +`engine/api/` must not exist — it is a post-refactor ghost. + +## Zero-Stub Protocol (CONTRACT-17) + +`engine/` ships working code. An unimplemented path is an untestable path, so the stub +markers below are banned there and enforced by `tools/contract_scanner.py`. + +| Rule | Pattern | Severity | +|---|---|---| +| `STUB-001` | `raise NotImplementedError` | CRITICAL | +| `STUB-002` | `# TODO` | HIGH | +| `STUB-003` | `# PLACEHOLDER`, `# FIXME`, `# XXX` | HIGH | + +When you cannot finish the work now, record it in `DEFERRED.md` and remove the marker. +A tracked deferral is reviewable; an inline comment is not. + +```python +# 🚫 BANNED — the gap is invisible outside this file +def compile_traversal_gate(spec: GateSpec) -> str: + raise NotImplementedError # TODO: needs multi-hop support +``` + +```python +# ✅ CORRECT — fail loudly at compile time, and log the gap in DEFERRED.md +def compile_traversal_gate(spec: GateSpec) -> str: + if spec.hops > 1: + msg = f"Multi-hop traversal gates are not supported (hops={spec.hops})" + raise ValueError(msg) + ... +``` + +**Scope note:** these rules apply to `engine/` only. Abstract base classes in `chassis/` +(for example `AuditSink.write_batch`) raise `NotImplementedError` as their defining +contract — that is the intended use, not a stub. diff --git a/docs/contracts/BIDIRECTIONAL_MATCHING.md b/docs/contracts/BIDIRECTIONAL_MATCHING.md new file mode 100644 index 00000000..734ab2d8 --- /dev/null +++ b/docs/contracts/BIDIRECTIONAL_MATCHING.md @@ -0,0 +1,86 @@ + + +# Bidirectional Matching Contract + +**Enforces:** `CONTRACT-15` (`contracts/contract_15.yaml`) +**Closes:** Agents hand-writing direction-specific branches inside gate implementations + +## Rule + +Gate implementations are **direction-unaware**. The compiler decides what a gate means +when the match direction reverses. Two spec fields control this: + +| Field | Type | Effect | +|---|---|---| +| `invertible` | `bool` (default `False`) | Swaps the candidate property and query parameter roles | +| `matchdirections` | `list[str] \| None` (default `None`) | Restricts the gate to the listed directions; `None` = all directions | + +## Direction scoping + +`GateCompiler` (`engine/gates/compiler.py`) skips any gate whose `matchdirections` does +not contain the active `match_direction`: + +```python +if gate.matchdirections and match_direction not in gate.matchdirections: + continue +``` + +A gate with `matchdirections: null` fires in every direction. This same scoping applies +to scoring dimensions — see `FIELD_NAMES.md`. + +## Inversion + +`invertible: true` flips which side of the predicate holds the collection. For an +`enum_map` gate: + +```python +if gate.invertible: + return f"${gate.queryparam} IN candidate.{prop}" +return f"candidate.{prop} IN ${gate.queryparam}" +``` + +Read plainly: the non-inverted form asks *"is the candidate's single value in the query's +list?"*; the inverted form asks *"is the query's single value in the candidate's list?"* + +## Correct + +```yaml +gates: + - name: material_compatibility + type: enum_map + candidateprop: accepted_materials # candidate holds a list + queryparam: material_code # query holds one value + invertible: true + + - name: supplier_only_freshness + type: freshness + candidateprop: last_updated + matchdirections: [supplier_to_buyer] # never fires buyer_to_supplier +``` + +## Wrong + +```python +# WRONG — direction logic inside a gate type +def compile_where(self, spec, domain, direction): + if direction == "buyer_to_supplier": + return f"candidate.{spec.candidateprop} IN ${spec.queryparam}" + return f"${spec.queryparam} IN candidate.{spec.candidateprop}" +``` + +Gate types receive no direction argument. If a gate needs different behavior per +direction, express it with `invertible` or `matchdirections` in the spec, or declare two +gates each scoped to one direction. + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract15BidirectionalMatching` +- `tests/unit/` (gate compilation) diff --git a/docs/contracts/DELEGATION_PROTOCOL.md b/docs/contracts/DELEGATION_PROTOCOL.md index 57d15f67..ffc8f26d 100644 --- a/docs/contracts/DELEGATION_PROTOCOL.md +++ b/docs/contracts/DELEGATION_PROTOCOL.md @@ -67,7 +67,7 @@ response = build_response_packet( > **Note:** This pattern predates the Gate SDK. New code MUST use `GateClient`. ```python -from l9.chassis.contract import delegate_to_node +from engine.packet.chassis_contract import delegate_to_node response_packet = await delegate_to_node( envelope=current_packet, # The packet you're currently processing diff --git a/docs/contracts/DEPENDENCY_INJECTION.md b/docs/contracts/DEPENDENCY_INJECTION.md index d440845d..090f906e 100644 --- a/docs/contracts/DEPENDENCY_INJECTION.md +++ b/docs/contracts/DEPENDENCY_INJECTION.md @@ -32,7 +32,7 @@ def init_dependencies(graph_driver: GraphDriver, domain_loader: DomainPackLoader ## Chassis Startup Sequence ```python -# chassis/app.py (or equivalent startup hook) +# chassis/chassis_app.py (or equivalent startup hook) from engine.handlers import init_dependencies, register_all async def lifespan(app): @@ -60,3 +60,31 @@ from fastapi import Depends # BANNED — chassis concern ``` ``` + +## Resilience Patterns (CONTRACT-24) + +Injected dependencies carry the resilience, so handlers do not have to. + +**All Neo4j access goes through `GraphDriver.execute_query()`.** Raw `.session()` calls +outside `engine/graph/driver.py` and `engine/graph/circuit_breaker.py` are banned — they +bypass the circuit breaker. + +The breaker is configured, not hardcoded: `neo4j_circuit_threshold`, +`neo4j_circuit_cooldown`, and `neo4j_circuit_half_open_max` in +`engine/config/settings.py`. + +**Caches are bounded.** `domain_cache_maxsize` bounds the domain pack cache; any new cache +uses `cachetools.TTLCache` or an equivalent with an explicit ceiling. An unbounded dict +used as a cache is a memory leak with a slow fuse. + +```python +# ❌ bypasses the circuit breaker +async with driver.session() as session: ... + +# ❌ unbounded +_cache: dict[str, DomainSpec] = {} + +# ✅ +result = await graph_driver.execute_query(cypher, params) +_cache: TTLCache = TTLCache(maxsize=settings.domain_cache_maxsize, ttl=settings.domain_cache_ttl) +``` diff --git a/docs/contracts/ENV_VARS.md b/docs/contracts/ENV_VARS.md index f71f5a8e..78168104 100644 --- a/docs/contracts/ENV_VARS.md +++ b/docs/contracts/ENV_VARS.md @@ -76,3 +76,13 @@ API_KEY=... # WRONG → L9_API_KEY_HASH (hash, not plaintext) ``` ``` + +## Scope (CONTRACT-05) + +`.env.template` is the single infrastructure file the engine may extend. Adding an +engine-specific variable here is in scope; changing Docker, CI, or IaC to consume it is +not — those live in `l9-template` (see [BANNED_PATTERNS.md](BANNED_PATTERNS.md)). + +Feature flags follow their own naming rule: the setting is `snake_case` ending in +`_enabled`, the env var is the same name uppercased. See +[FEATURE_FLAG_DISCIPLINE.md](FEATURE_FLAG_DISCIPLINE.md). diff --git a/docs/contracts/FEATURE_FLAG_DISCIPLINE.md b/docs/contracts/FEATURE_FLAG_DISCIPLINE.md new file mode 100644 index 00000000..7f164fa5 --- /dev/null +++ b/docs/contracts/FEATURE_FLAG_DISCIPLINE.md @@ -0,0 +1,77 @@ + + +# Feature Flag Discipline Contract + +**Enforces:** `CONTRACT-21` (`contracts/contract_21.yaml`) +**Closes:** Agents shipping behavioral changes that activate on merge with no operator control + +## Rule + +Every behavioral change is gated by a boolean flag declared in +`engine/config/settings.py` and documented in `docs/FEATURE_GATES.md`. The engine proves +the mechanism; the operator decides the policy. + +## Three requirements + +1. **Declared in `Settings`** — flag name ends with `_enabled` and is annotated `bool`. +2. **Documented** — the flag (or its uppercase env var form) appears in + `docs/FEATURE_GATES.md`. +3. **Defaults chosen deliberately** — new behavior generally ships `False`; hardening that + restores an invariant may ship `True`. + +## Correct + +```python +# engine/config/settings.py +class Settings(BaseSettings): + score_clamp_enabled: bool = True + outcome_persistence_enabled: bool = False +``` + +```python +# engine/scoring/assembler.py +def validate_weights(weights: dict[str, float] | None = None) -> None: + if not settings.score_clamp_enabled: + return + ... +``` + +Then add a row to `docs/FEATURE_GATES.md`: + +| Capability | Env Var | Default | Status | +|---|---|---|---| +| Score Clamping | `SCORE_CLAMP_ENABLED` | `True` | active | + +## Wrong + +```python +# WRONG — new behavior on by default with no flag and no doc row +def assemble_scoring_clause(...): + score = clamp(score, 0.0, 1.0) # silently changes every existing domain's output +``` + +```python +# WRONG — flag typed as str, so `if settings.foo_enabled` is true for "false" +foo_enabled: str = "false" +``` + +## Naming + +- Setting: `snake_case`, suffix `_enabled` +- Env var: the same name uppercased (`SCORE_CLAMP_ENABLED`) +- No `Field(alias=...)` — YAML/env keys equal Python field names (`NAME-001`) + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract21FeatureFlagDiscipline` + - flags are declared `bool` + - `docs/FEATURE_GATES.md` exists + - every `*_enabled` flag appears in that doc diff --git a/docs/contracts/FIELD_NAMES.md b/docs/contracts/FIELD_NAMES.md index 9f0f0fca..5ec71670 100644 --- a/docs/contracts/FIELD_NAMES.md +++ b/docs/contracts/FIELD_NAMES.md @@ -115,3 +115,17 @@ endpoint.idproperty # WRONG: missing underscore - Integration tests must instantiate DomainSpec from real YAML and access all fields ``` + +## Null Semantics Are Per-Gate (CONTRACT-14) + +Every gate declares `nullbehavior` (`NullBehavior.PASS` by default, or `FAIL`). This is a +per-gate decision, not a global setting, and the compiler applies it — callers never +handle NULL themselves. + +| `nullbehavior` | Compiled predicate | +|---|---| +| `pass` | `(candidate.prop IS NULL OR )` | +| `fail` | `` — a NULL candidate is rejected | + +The wrapping happens in `engine/gates/null_semantics.py`. Do not hand-write +`IS NULL OR` into a gate's `compile_where()`; set `nullbehavior` in the spec instead. diff --git a/docs/contracts/HANDLER_PAYLOADS.md b/docs/contracts/HANDLER_PAYLOADS.md index 0854deee..a1aee35a 100644 --- a/docs/contracts/HANDLER_PAYLOADS.md +++ b/docs/contracts/HANDLER_PAYLOADS.md @@ -70,3 +70,23 @@ async def handle_match(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: ``` ``` + +## Admin Subaction Registration (CONTRACT-23) + +`handle_admin` dispatches on a `subaction` key. Two rules: + +1. **Resolve it through `_require_key()`** — never `payload.get("subaction")`. A missing + key must raise, not fall through to a default branch. +2. **Names are `snake_case`** — matching `[a-z][a-z0-9_]*`. No camelCase, no dots, no + spaces. + +```python +# ✅ +subaction = _require_key(payload, "subaction", "admin", tenant) +if subaction == "trigger_gds_job": + ... + +# ❌ +subaction = payload.get("subaction", "describe") # silent default +if subaction == "triggerGDSJob": # not snake_case +``` diff --git a/docs/contracts/KGE_EMBEDDINGS.md b/docs/contracts/KGE_EMBEDDINGS.md new file mode 100644 index 00000000..7645583f --- /dev/null +++ b/docs/contracts/KGE_EMBEDDINGS.md @@ -0,0 +1,78 @@ + + +# KGE Embeddings Contract + +**Enforces:** `CONTRACT-20` (`contracts/contract_20.yaml`) +**Closes:** Agents wiring embeddings as a side channel or sharing vectors across tenants + +## Status: dormant + +`kge_enabled` defaults to `False` in `engine/config/settings.py`. The subsystem exists and +is tested, but no production path runs it. Activating it is an operator decision — see +`FEATURE_FLAG_DISCIPLINE.md`. + +## Rule + +Knowledge-graph embeddings are a **scoring dimension**, not a parallel ranking system. +KGE scores feed the same `WITH` clause as every other dimension and are subject to the +same weight ceiling. + +## Model + +| Property | Value | +|---|---| +| Model | CompoundE3D | +| Default dimension | 256 (`kge_embedding_dim`, must match `KGESpec.embeddingdim`) | +| Confidence threshold | 0.3 (`kge_confidence_threshold`) | +| Training relations | Derived from `spec.ontology.edges` | +| Link prediction | Beam search, width 10, depth 3 | +| Vector index | Neo4j, cosine similarity | + +Ensemble strategies: `weighted_average`, `rank_aggregation`, `mixture_of_experts`. + +## Tenant isolation + +Embeddings are **domain-specific and never shared across tenants**. A vector trained on +one tenant's graph must not be read, indexed, or ensembled into another tenant's scoring. +This is the same isolation boundary as every Neo4j query (`CONTRACT-03`) — the vector +index does not get an exception. + +## Correct + +```yaml +scoring: + dimensions: + - name: kge_similarity + computation: custom_cypher + weight: 0.15 # counts against the 1.0 ceiling like any other dimension +``` + +## Wrong + +```python +# WRONG — embeddings ranked separately, then merged in Python +kge_ranked = await kge_service.rank(query) +gate_ranked = await run_match(tenant, payload) +return merge(kge_ranked, gate_ranked) +``` + +Post-hoc merging in Python violates `CONTRACT-13` (gate-then-score in Cypher). KGE scores +belong in the single scoring clause. + +```python +# WRONG — cross-tenant vector reuse +index = shared_vector_index # violates tenant isolation +``` + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract20KGEEmbeddings` +- `tests/unit/` (KGE scoring dimension) diff --git a/docs/contracts/L9_META_HEADERS.md b/docs/contracts/L9_META_HEADERS.md new file mode 100644 index 00000000..575e9dad --- /dev/null +++ b/docs/contracts/L9_META_HEADERS.md @@ -0,0 +1,102 @@ + + +# L9_META Header Contract + +**Enforces:** `CONTRACT-18` (`contracts/contract_18.yaml`) +**Closes:** Agents hand-writing metadata headers with invented fields or the wrong comment syntax + +## Rule + +Every tracked source file carries an L9_META header (schema version 1). Headers are +**injected by `tools/l9_meta_injector.py`**, not typed by hand. Run the injector, commit, +done. + +## Fields + +| Field | Meaning | +|---|---| +| `l9_schema` | Header schema version — always `1` | +| `origin` | `l9-template` (shared) or `engine-specific` (this repo) | +| `engine` | `graph` | +| `layer` | List, e.g. `[config]`, `[docs, contracts]`, `[tools]` | +| `tags` | List of free-form tags | +| `owner` | `platform` or `engine-team` | +| `status` | `active`, `deprecated` | + +## Format by filetype + +The syntax changes with the comment style of the host language; the field set does not. + +**Python** — inside the module docstring: + +```python +""" +--- L9_META --- +l9_schema: 1 +origin: engine-specific +engine: graph +layer: [config] +tags: [compliance, prohibited-factors] +owner: engine-team +status: active +--- /L9_META --- + +Module description follows. +""" +``` + +**Markdown / HTML** — HTML comment at the top: + +```markdown + +``` + +**YAML / shell** — `#` comment block: + +```yaml +# --- L9_META --- +# l9_schema: 1 +# origin: engine-specific +# engine: graph +# layer: [domains] +# tags: [domain-spec] +# owner: engine-team +# status: active +# --- /L9_META --- +``` + +**JSON** uses an `"_l9_meta"` object key; **TOML** uses an `[l9_meta]` table. + +## Exemptions + +`__init__.py` files are exempt from the engine header check. + +## Wrong + +```python +# L9: graph engine, owned by team # WRONG → not the schema, no delimiters +``` + +Do not invent fields, reorder them arbitrarily, or place the block below imports. If a +file is missing a header, run the injector rather than pasting one in. + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract18L9Meta` +- `tools/l9_meta_injector.py` diff --git a/docs/contracts/MEMORY_SUBSTRATE_ACCESS.md b/docs/contracts/MEMORY_SUBSTRATE_ACCESS.md index 90fd4e29..d46c1491 100644 --- a/docs/contracts/MEMORY_SUBSTRATE_ACCESS.md +++ b/docs/contracts/MEMORY_SUBSTRATE_ACCESS.md @@ -21,9 +21,22 @@ substrate table. The LangGraph DAG handles validation, embedding, graph sync, insight extraction, and checkpointing. ## The Only Write Path + +Inside this engine, persistence goes through `PacketStore` — never raw SQL: + ```python -from l9.memory.ingestion import ingest_packet +from engine.packet.packet_store import get_packet_store + +await get_packet_store().write(packet) +``` + +`PacketStore` is gated by `packet_store_enabled` and requires `PACKET_STORE_DSN` +(see [../FEATURE_GATES.md](../FEATURE_GATES.md)). +When delegating to the constellation memory substrate node, the ingestion contract is +`ingest_packet()` on that node — reached via the delegation protocol, not by importing it: + +```python await ingest_packet(PacketEnvelopeIn( packet_type="enrichment_result", payload={"entity_id": "abc-123", "enriched_fields": {...}}, @@ -47,9 +60,11 @@ await ingest_packet(PacketEnvelopeIn( ## The Only Read Path -```python -from l9.memory.retrieval import PipelineRouter +Retrieval is a memory-substrate-node capability reached through delegation +(see [DELEGATION_PROTOCOL.md](DELEGATION_PROTOCOL.md)). The engine does not query the +substrate's tables or embeddings directly. +```python results = await PipelineRouter.retrieve( query="HDPE contamination tolerance for Houston facilities", tenant_id="acme", diff --git a/docs/contracts/METHOD_SIGNATURES.md b/docs/contracts/METHOD_SIGNATURES.md index e3d65ce3..eb37d4b3 100644 --- a/docs/contracts/METHOD_SIGNATURES.md +++ b/docs/contracts/METHOD_SIGNATURES.md @@ -100,3 +100,35 @@ class GDSScheduler: 4. Update tests 5. Run `make test` to verify no signature mismatches ``` + +## Gate-Then-Score (CONTRACT-13) + +Matching is two phases, both compiled to Cypher — never post-filtered in Python: + +| Phase | Assembler | Produces | +|---|---|---| +| Gates (hard filter) | `GateCompiler.compile_all_gates()` | one `WHERE` clause | +| Scoring (soft rank) | `ScoringAssembler.assemble_scoring_clause()` | one `WITH ... ORDER BY` clause | + +Clause order is fixed: `MATCH` → traversal → `WHERE` gates → `WITH` scoring → +`RETURN candidate, score` → `ORDER BY score DESC`. + +There is no iterative scoring loop and no Python-side filtering of returned rows. If a +predicate cannot be expressed in Cypher, it is not a gate. + +## GDS Jobs Are Declarative (CONTRACT-19) + +`GDSScheduler` reads `spec.gds_jobs` and creates APScheduler jobs from it. No algorithm +call is hardcoded. + +`GDSJobSpec` fields: `name`, `algorithm`, `schedule` (`cron` | `manual`), `projection` +(declares `node_labels` + `edge_types`), and spec-driven write targets — `writeproperty`, +`writeto`, `writeedge`, `writeproperties`, `sourceedge`, `filter`. + +```python +# ❌ hardcoded algorithm dispatch +await driver.execute_query("CALL gds.louvain.write(...)") # BANNED + +# ✅ scheduler reads the spec +scheduler.load_jobs(spec.gds_jobs) +``` diff --git a/docs/contracts/OBSERVABILITY.md b/docs/contracts/OBSERVABILITY.md index b86b58bb..171d4e29 100644 --- a/docs/contracts/OBSERVABILITY.md +++ b/docs/contracts/OBSERVABILITY.md @@ -36,24 +36,40 @@ Every log line emitted by the chassis includes: ## Engine Logging Pattern +The enforced invariant is **the engine never configures logging** — not which getter you call. +Both APIs below satisfy CONTRACT-04. + +`logging.getLogger(__name__)` is the prevailing pattern in `engine/` (~80 modules). +`structlog.get_logger(__name__)` is used in a handful of newer modules (`engine/intake/`, +`engine/causal/serializer.py`, `engine/feedback/drift_detector.py`, `engine/security/`) and is +equally acceptable. Match the surrounding module; do not convert existing files. + ```python import logging logger = logging.getLogger(__name__) -# ✅ CORRECT — use stdlib logger, chassis configures structlog +# ✅ CORRECT — stdlib logger, chassis owns handlers and formatting logger.info("Gate compilation complete", extra={ - "gate_count": 14, + "gate_count": 10, "match_direction": "buyer_to_seller", }) -# ❌ WRONG — configuring logging in engine +# ✅ ALSO CORRECT — structlog getter, still zero configuration import structlog -structlog.configure(...) # BANNED — chassis does this +logger = structlog.get_logger(__name__) +logger.info("gate_compilation_complete", gate_count=10) + +# ❌ WRONG — configuring logging in engine +structlog.configure(...) # BANNED (OBS-001) — chassis does this # ❌ WRONG — creating custom formatters -logging.basicConfig(format="...") # BANNED — chassis does this +logging.basicConfig(format="...") # BANNED (OBS-002) — chassis does this ``` +> **Note:** no `structlog.configure()` call exists in `chassis/` in this repo either. Until the +> chassis adds one, `structlog` emits through stdlib logging defaults. That is a chassis gap, +> not an engine one — do not fix it from `engine/`. + ## Prometheus Metrics (chassis-owned) @@ -68,16 +84,21 @@ These metrics are auto-exported. Engines MUST NOT create their own. ## Engine Custom Metrics (if absolutely needed) -```python -# Register via chassis metric factory — never create raw Prometheus objects -from chassis.metrics import register_histogram - -gate_compilation_time = register_histogram( - "l9_gate_compilation_seconds", # MUST start with l9_ - "Time to compile all gates", - labels=["tenant", "match_direction"], -) -``` +> **Status: not available in this repo.** There is no `chassis/metrics.py` and no +> Prometheus registry in `chassis/`. The engine currently emits no custom metrics. +> +> If a custom metric becomes necessary, the chassis must first grow a metric factory — +> the engine still never creates raw Prometheus objects. The intended shape is: +> +> ```python +> gate_compilation_time = register_histogram( +> "l9_gate_compilation_seconds", # MUST start with l9_ +> "Time to compile all gates", +> labels=["tenant", "match_direction"], +> ) +> ``` +> +> Adding that factory is a chassis change, not an engine change (`CONTRACT-04`). ## Trace Propagation diff --git a/docs/contracts/PACKET_ENVELOPE_FIELDS.md b/docs/contracts/PACKET_ENVELOPE_FIELDS.md index f42d0594..833fbb53 100644 --- a/docs/contracts/PACKET_ENVELOPE_FIELDS.md +++ b/docs/contracts/PACKET_ENVELOPE_FIELDS.md @@ -181,3 +181,22 @@ onBehalfOf # WRONG → on_behalf_of (inside TenantContext) ``` ``` + +## The Only Data Container (CONTRACT-06) + +Every inter-service payload is a `PacketEnvelope`. The boundary functions are the only +sanctioned way in and out: + +```python +from engine.packet.chassis_contract import deflate_egress, inflate_ingress +``` + +- `inflate_ingress()` at boundary **entry** +- `deflate_egress()` at boundary **exit** +- Engine code **between** boundaries works with typed dicts and Pydantic models, never + raw envelopes + +`packet_type` values are lowercase (`PKT-001`). Persistence goes through the memory +substrate — direct `INSERT INTO packetstore` or `INSERT INTO memory_embeddings` from +engine code is banned (`MEM-001`, `MEM-002`); see +[MEMORY_SUBSTRATE_ACCESS.md](MEMORY_SUBSTRATE_ACCESS.md). diff --git a/docs/contracts/PACKET_TYPE_REGISTRY.md b/docs/contracts/PACKET_TYPE_REGISTRY.md index 525de23d..54c27c4b 100644 --- a/docs/contracts/PACKET_TYPE_REGISTRY.md +++ b/docs/contracts/PACKET_TYPE_REGISTRY.md @@ -64,7 +64,7 @@ NEVER be renamed or removed. Use EXACTLY these strings. ## Adding a New Packet Type 1. Add to this file -2. Add to `PacketType` enum in `l9/packet/envelope.py` +2. Add to the `PacketType` enum in `engine/packet/packet_envelope.py` 3. Add validation in `PacketValidator` 4. Update `tools/audit_rules.yaml` if compliance checks apply ``` diff --git a/docs/contracts/PII_HANDLING.md b/docs/contracts/PII_HANDLING.md new file mode 100644 index 00000000..3942b803 --- /dev/null +++ b/docs/contracts/PII_HANDLING.md @@ -0,0 +1,80 @@ + + +# PII Handling Contract + +**Enforces:** `CONTRACT-11` (`contracts/contract_11.yaml`) +**Closes:** Agents logging raw PII or inventing ad-hoc masking schemes + +## Rule + +PII fields are declared in the domain spec, never inferred. Each domain picks exactly one +handling mode, and the encryption key always comes from a declared source — never from a +literal in code. + +## Spec shape + +`PIISpec` (`engine/config/schema.py`): + +```yaml +compliance: + pii: + fields: + - contact_email + - contact_phone + handling: hash # hash | encrypt | redact | tokenize + encryptionkeysource: env # env | vault | kms +``` + +| Field | Default | Values | +|---|---|---| +| `handling` | `hash` | `hash`, `encrypt`, `redact`, `tokenize` | +| `encryptionkeysource` | `env` | `env`, `vault`, `kms` | + +## Handling modes + +| Mode | Behavior | Reversible | +|---|---|---| +| `hash` | One-way digest; equality matching still works | No | +| `encrypt` | Symmetric encryption using the declared key source | Yes, with the key | +| `redact` | Value replaced with a fixed marker | No | +| `tokenize` | Value swapped for an opaque token resolved out of band | Yes, via the token store | + +## Logging + +The engine **never** logs PII values. Log the field *name* and the handling decision, not +the content. structlog filters are installed by the chassis (see `OBSERVABILITY.md`); the +engine does not configure them and must not assume they will catch a mistake. + +## Correct + +```python +logger.info("pii_field_hashed", field="contact_email", handling=spec.compliance.pii.handling) +``` + +## Wrong + +```python +logger.info("processing contact", email=candidate["contact_email"]) # WRONG → PII in logs +key = "s3cr3t-aes-key" # WRONG → hardcoded key +``` + +## Key sources + +`encryptionkeysource` declares *where* the key lives, never the key itself: + +- `env` — read from an environment variable at startup +- `vault` — fetched from HashiCorp Vault +- `kms` — fetched from a cloud KMS + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract11PIIHandling` +- `tests/compliance/` diff --git a/docs/contracts/PROHIBITED_FACTORS.md b/docs/contracts/PROHIBITED_FACTORS.md new file mode 100644 index 00000000..83426c48 --- /dev/null +++ b/docs/contracts/PROHIBITED_FACTORS.md @@ -0,0 +1,91 @@ + + +# Prohibited Factors Contract + +**Enforces:** `CONTRACT-10` (`contracts/contract_10.yaml`) +**Closes:** Agents compiling gates or sync mappings that reference protected attributes + +## Rule + +Protected attributes are blocked at **compile time**, not at query time. A domain spec +that references a prohibited field fails gate compilation with a `ValueError` — it never +reaches Neo4j, so there is no runtime path that can leak the factor. + +## Where the block happens + +`ProhibitedFactorValidator` (`engine/compliance/prohibited_factors.py`) is constructed +from `domain_spec.compliance.prohibitedfactors` and owns a `blocked_fields` set. +`ComplianceEngine` (`engine/compliance/engine.py`) calls it on three surfaces: + +| Surface | Method | Checks | +|---|---|---| +| Gate compilation | `validate_gate()` | `gate.candidateprop`, `gate.queryparam` | +| Sync endpoint definition | `validate_sync_endpoint()` | endpoint field mappings, `idproperty` | +| Sync batch payload | audit path | every field name in the inbound batch | + +If `prohibitedfactors.enabled` is `False` or the section is absent, `blocked_fields` is +empty and every check short-circuits — the mechanism ships dormant. + +## Spec shape + +```yaml +compliance: + prohibitedfactors: + enabled: true + blockedfields: + - race + - ethnicity + - religion + - gender + - age + - disability + - familial_status + - national_origin + audit_on_violation: true +``` + +## Correct + +```yaml +gates: + - name: capacity_threshold + type: threshold + candidateprop: monthly_capacity_tons # operational attribute + queryparam: required_capacity +``` + +## Wrong + +```yaml +gates: + - name: demographic_filter + type: enum_map + candidateprop: ethnicity # WRONG → compile-time ValueError + queryparam: target_ethnicity +``` + +The failure is loud and early: + +``` +ValueError: Gate 'demographic_filter' references prohibited field 'ethnicity'. +Blocked fields: {'race', 'ethnicity', ...} +``` + +## Audit + +With `audit_on_violation: true`, every blocked attempt is recorded through the audit +path before the exception propagates. A rejected spec leaves a trail; it does not fail +silently. + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract10ProhibitedFactors` +- `tests/compliance/` diff --git a/docs/contracts/README.md b/docs/contracts/README.md index 569bbde6..73505fbc 100644 --- a/docs/contracts/README.md +++ b/docs/contracts/README.md @@ -1,3 +1,4 @@ + # docs/contracts/ > Production-grade contract documentation for the L9 Cognitive Engine Graph (CEG). Every contract traces back to a specific source file in this repository. @@ -65,9 +66,10 @@ pytest tests/contracts/ # contract suite only ``` `make agent-check` is the completion gate: it runs `tools/verify_contracts.py` -(the 20 contract markdown files present and referenced from the agent rule -files), `tools/contract_scanner.py` (banned pattern scan), lint, types, and the -full test suite. A green `agent-check` means green CI. +(the 27 contract markdown files present and referenced from the agent rule +files), `tools/contract_scanner.py` (banned pattern scan), +`tools/contract_report.py` (contract-to-verification coverage), lint, types, and +the full test suite. A green `agent-check` means green CI. Run `pytest tests/contracts/` **without** `-m contract`. The `contract` marker covers under a fifth of the tests in this directory and deselects the rest, diff --git a/docs/contracts/SCORING_WEIGHT_CEILING.md b/docs/contracts/SCORING_WEIGHT_CEILING.md new file mode 100644 index 00000000..3885086e --- /dev/null +++ b/docs/contracts/SCORING_WEIGHT_CEILING.md @@ -0,0 +1,89 @@ + + +# Scoring Weight Ceiling Contract + +**Enforces:** `CONTRACT-22` (`contracts/contract_22.yaml`) +**Closes:** Agents adding a scoring dimension whose weight pushes the composite score above 1.0 + +## Rule + +Default scoring weights sum to **at most 1.0**, and the sum is asserted at startup. A +misconfigured deployment fails to boot rather than silently emitting scores outside +`[0, 1]`. + +## Defaults + +`engine/config/settings.py`: + +| Setting | Default | +|---|---| +| `w_structural` | 0.30 | +| `w_geo` | 0.25 | +| `w_reinforcement` | 0.20 | +| `w_freshness` | 0.10 | +| **Sum** | **0.85** | + +The 0.15 headroom is deliberate — it is the budget for an additional dimension (KGE, +causal, persona) without breaching the ceiling. + +## Startup assertion + +`engine/boot.py`: + +```python +_WEIGHT_CEILING = 1.0 + +def _assert_default_weight_sum() -> None: + weight_sum = settings.w_structural + settings.w_geo + settings.w_reinforcement + settings.w_freshness + if weight_sum > _WEIGHT_CEILING + _WEIGHT_SUM_TOLERANCE: + raise ValueError(f"... exceeding {_WEIGHT_CEILING}") + logger.info("W1-02: Default weight sum validated: %.4f <= %.1f", weight_sum, _WEIGHT_CEILING) +``` + +A float tolerance is applied so that weights summing to exactly 1.0 do not trip on +binary-representation error. + +## Adding a dimension + +Take the weight from the headroom or rebalance the existing four. Do not add on top. + +## Correct + +```bash +# 0.30 + 0.25 + 0.20 + 0.10 = 0.85, plus a 0.15 KGE dimension = 1.00 +W_STRUCTURAL=0.30 +W_GEO=0.25 +W_REINFORCEMENT=0.20 +W_FRESHNESS=0.10 +``` + +## Wrong + +```bash +# WRONG — 1.30 total; boot raises ValueError +W_STRUCTURAL=0.50 +W_GEO=0.50 +W_REINFORCEMENT=0.20 +W_FRESHNESS=0.10 +``` + +## Per-request weights + +Weights supplied in a `match` payload are validated separately by the scoring assembler +when `score_clamp_enabled` is set. The startup assertion covers **defaults only** — it +cannot see request-time overrides. + +## Verified by + +- `tests/contracts/test_contracts.py::TestContract22ScoringWeightCeiling` + - `_assert_default_weight_sum` exists and `_WEIGHT_CEILING == 1.0` + - shipped defaults are within the ceiling + - the assertion raises when the sum is exceeded diff --git a/docs/contracts/SHARED_MODELS.md b/docs/contracts/SHARED_MODELS.md index 6ce79f34..6d38c165 100644 --- a/docs/contracts/SHARED_MODELS.md +++ b/docs/contracts/SHARED_MODELS.md @@ -61,11 +61,9 @@ types.py \# PacketType enum, shared type aliases ## Import Pattern ```python -# ✅ CORRECT — import from l9.core (internal operations) -from l9.core.envelope import PacketEnvelope, TenantContext, PacketLineage -from l9.core.contract import ExecuteRequest, ExecuteResponse -from l9.core.delegation import delegate_to_node -from l9.core.types import PacketType +# ✅ CORRECT — import from engine.packet (internal operations) +from engine.packet.packet_envelope import PacketEnvelope, PacketLineage, PacketType, TenantContext +from engine.packet.chassis_contract import deflate_egress, delegate_to_node, inflate_ingress # ❌ WRONG — redefining in engine code class PacketEnvelope(BaseModel): # BANNED — already in l9-core @@ -111,3 +109,22 @@ dependencies = [ ``` ``` + +## Tenant Isolation (CONTRACT-03) + +`TenantContext` is a shared model — the engine never redefines it (`SHARED-002`) and never +resolves it. The chassis performs the 5-level resolution (header → subdomain → key prefix +→ envelope → default) and hands the engine a plain `tenant: str`. + +Every Neo4j query scopes to that tenant's database. There is no cross-tenant read path: +not in matching, not in sync, not in the KGE vector index +(see [KGE_EMBEDDINGS.md](KGE_EMBEDDINGS.md)). + +```python +# ✅ tenant arrives as an argument +async def handle_match(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: ... + +# ❌ engine resolving tenant itself +tenant = request.headers["X-Tenant-Id"] # BANNED — chassis concern +class TenantContext(BaseModel): ... # BANNED — SHARED-002 +``` diff --git a/docs/contracts/TEST_PATTERNS.md b/docs/contracts/TEST_PATTERNS.md index 0397a057..e0decb99 100644 --- a/docs/contracts/TEST_PATTERNS.md +++ b/docs/contracts/TEST_PATTERNS.md @@ -65,3 +65,17 @@ from engine.api.routers.match import match_endpoint # BANNED ``` ``` + +## Required Coverage (CONTRACT-17) + +| Test type | Location | Must cover | +|---|---|---| +| Unit | `tests/unit/` | Gate compilation, scoring math, parameter resolution, null semantics **per gate type** | +| Integration | `tests/integration/` | Full pipeline against `testcontainers-neo4j` — the driver is never mocked | +| Compliance | `tests/compliance/` | Prohibited factors blocked at **compile** time | +| Performance | `tests/performance/` | p95 match latency < 200 ms | +| Validation | `tests/` | Compiled Cypher for the reference spec matches the hand-verified baseline | + +Every new function needs at least one test. The hand-verified Cypher baseline is the +regression anchor for the compiler — if it changes, the change is intentional and +reviewed, not incidental. diff --git a/docs/contracts/api/openapi.yaml b/docs/contracts/api/openapi.yaml index 68ae2893..a683268a 100644 --- a/docs/contracts/api/openapi.yaml +++ b/docs/contracts/api/openapi.yaml @@ -1,9 +1,9 @@ # ═══════════════════════════════════════════════════════════════ # Contract: L9 Cognitive Engine Graph — Unified API -# Source: chassis/app.py, chassis/chassis_app.py, chassis/actions.py +# Source: chassis/chassis_app.py, chassis/chassis_app.py, chassis/actions.py # Version: 1.0.0 # Updated: 2026-04-26 -# Verified: Against chassis/app.py lines 82-99, 330-361, handlers.py +# Verified: Against chassis/chassis_app.py lines 82-99, 330-361, handlers.py # ═══════════════════════════════════════════════════════════════ openapi: 3.1.0 @@ -15,6 +15,26 @@ info: accessed through the single universal ingress: POST /v1/execute. The `action` field routes to one of 8 named handlers in engine/handlers.py. Behavior is entirely driven by domain spec YAML files loaded at runtime. + + TWO CHASSIS, ONE ACTION SET. `chassis/entrypoint.py` dispatches on + `L9_CHASSIS`; both paths route the same `engine.handlers.ACTION_HANDLERS` + (CONTRACT-02). The request/response envelope differs: + + L9_CHASSIS=legacy (default) — chassis/chassis_app.py. The ExecuteRequest / + ExecuteResponse schemas below. Auth: `x-api-key`. `/v1/health` returns + 200 healthy / 503 degraded. + + L9_CHASSIS=sdk — chassis/node_app.py, built by constellation-node-sdk + `create_node_app()`. The body is a `TransportPacket`, not ExecuteRequest: + `action` and `payload` live under the packet, tenant arrives as + `tenant.org_id` (== CEG domain_id), and the response is a TransportPacket + with `header.packet_type` of `response` or `failure`. Auth is packet + signature verification (`L9_REQUIRE_SIGNATURE`), not `x-api-key`. + `/v1/health` is always HTTP 200 — readiness is the `ready` boolean, which + is why the container probes assert on `ready` rather than status alone. + Ingress is Gate-only: `/v1/execute` returns 403 unless the packet is + Gate-authored (see x-sdk-gate-only-ingress below). + Route surface is `/v1/execute`, `/v1/health`, `/metrics` — no `/v1/relay`. version: 1.0.0 contact: email: eng@l9.dev @@ -42,7 +62,7 @@ components: type: object description: | Universal execute request envelope — chassis contract. - Source: chassis/app.py:ExecuteRequest (lines 82-88) + Source: chassis/chassis_app.py:ExecuteRequest (lines 82-88) properties: action: type: string @@ -67,7 +87,7 @@ components: type: object description: | Universal execute response envelope — chassis contract. - Source: chassis/app.py:ExecuteResponse (lines 91-98) + Source: chassis/chassis_app.py:ExecuteResponse (lines 91-98) properties: status: type: string @@ -114,7 +134,7 @@ paths: Universal single-ingress endpoint for all engine operations. Routes to: match, sync, admin, outcomes, resolve, health, healthcheck, enrich. - HTTP status code mapping (source: chassis/app.py:_raise_for_failed_result): + HTTP status code mapping (source: chassis/chassis_app.py:_raise_for_failed_result): 200 — success, failed, or circuit_open (check body .status) 400 — unknown action (ValueError from engine) 422 — payload validation failure (Pydantic or engine ValidationError) @@ -122,7 +142,7 @@ paths: Each call is wrapped in a PacketEnvelope (inflate_ingress → handler → deflate_egress → PacketStore.persist). CONTRACT-06, CONTRACT-07, CONTRACT-08. - x-source-file: chassis/app.py:execute() + x-source-file: chassis/chassis_app.py:execute() tags: [engine] requestBody: required: true @@ -267,8 +287,8 @@ paths: 2. domain_loader.load_domain(tenant) spec validation Returns 200 when healthy, 503 when Neo4j unreachable. - Source: chassis/app.py:health(), engine/handlers.py:handle_health() - x-source-file: chassis/app.py:health() + Source: chassis/chassis_app.py:health(), engine/handlers.py:handle_health() + x-source-file: chassis/chassis_app.py:health() tags: [health] security: [] parameters: @@ -308,3 +328,15 @@ tags: description: Core engine action execution - name: health description: Liveness and readiness probes + +# Gate-only ingress is enforced by chassis/node_app.py middleware, not by the +# SDK: NodeRuntimeConfig carries no gate-only field. Rejection is HTTP 403 with +# `detail: "gate-only ingress: "`. +x-sdk-gate-only-ingress: + applies_to: L9_CHASSIS=sdk, POST /v1/execute + toggle: L9_ENFORCE_GATE_ONLY_INGRESS (default true) + gate_node: L9_GATE_NODE_NAME (default "gate") + required_packet_fields: + provenance.origin_kind: must equal "gate" + provenance.resolved_by_gate: must be true + address.source_node: must equal L9_GATE_NODE_NAME diff --git a/docs/contracts/config/env-contract.yaml b/docs/contracts/config/env-contract.yaml index 72ef8409..477d775c 100644 --- a/docs/contracts/config/env-contract.yaml +++ b/docs/contracts/config/env-contract.yaml @@ -1,7 +1,7 @@ # ═══════════════════════════════════════════════════════════════ # Contract: Engine + Chassis Environment Variables # Source: engine/config/settings.py:Settings -# chassis/app.py:ChassisSettings +# chassis/chassis_app.py:ChassisSettings # docker-compose.yml # Version: 1.0.0 # Updated: 2026-04-05 @@ -164,7 +164,7 @@ variables: Dotted module path to the engine's LifecycleHook class. Format: "module.path:ClassName" Example: "engine.boot:GraphLifecycle" - Source: chassis/app.py:ChassisSettings.l9_lifecycle_hook + Source: chassis/chassis_app.py:ChassisSettings.l9_lifecycle_hook source: env sensitive: false diff --git a/docs/github-ruleset-diagnostics.md b/docs/github-ruleset-diagnostics.md new file mode 100644 index 00000000..d54c31ec --- /dev/null +++ b/docs/github-ruleset-diagnostics.md @@ -0,0 +1,144 @@ +# GitHub Org-Level Ruleset Enforcement — Diagnostic Commands + +Diagnostic commands run while investigating whether/how GitHub org-level ruleset +enforcement was configured for `Quantum-L9`, before the org upgraded to +GitHub Enterprise. Extracted from the session transcript. + +Context: the user clarified that an earlier "checking rules and rule sets" +comment referred to **GitHub repository rulesets** (branch protection / merge +gating), not the Cursor `.mdc` agent-governance rules. These commands +established: (a) what ruleset state currently exists across the org, (b) why +org-level rulesets were inaccessible (GitHub Free plan), and (c) what a +rollout would need to account for (CodeQL coverage, malformed ref-name +conditions, archived/fork/visibility status). + +--- + +## 1. List rulesets on the current repo + +```bash +cd "/Users/ib-mac/Dropbox/Repo_Dropbox_IB/Cognitive.Engine.Graphs" +echo "=== repo-level rulesets ===" && gh api repos/Quantum-L9/Cognitive.Engine.Graphs/rulesets +``` + +## 2. List org-level rulesets for Quantum-L9 + +```bash +echo "=== org-level rulesets (Quantum-L9 org) ===" && gh api orgs/Quantum-L9/rulesets +``` + +Result: 403 — org-level rulesets require GitHub Team/Enterprise; org was on +Free at the time. + +## 3. Check org plan/billing tier and ruleset presence across repos + +```bash +echo "=== org plan / billing tier ===" && gh api orgs/Quantum-L9 | \ + python3 -c "import json,sys; d=json.load(sys.stdin); print('plan:', d.get('plan')); print('name:', d.get('login'))" + +echo +echo "=== list a few other Quantum-L9 repos and their rulesets ===" +for repo in $(gh repo list Quantum-L9 --limit 15 --json name -q '.[].name'); do + echo "--- $repo ---" + gh api repos/Quantum-L9/$repo/rulesets | \ + python3 -c "import json,sys; d=json.load(sys.stdin); print([r['name'] for r in d]) if isinstance(d,list) else print(d)" +done +``` + +## 4. Inspect full ruleset configs on this repo + +```bash +echo "=== CI Gate (this repo) full detail ===" && \ + gh api repos/Quantum-L9/Cognitive.Engine.Graphs/rulesets/13800421 | python3 -m json.tool + +echo +echo "=== Code Quality Copilot review ruleset detail (this repo) ===" && \ + gh api repos/Quantum-L9/Cognitive.Engine.Graphs/rulesets/19360468 | python3 -m json.tool +``` + +Finding: the "CI Gate" ruleset had `conditions.ref_name.include: []` / +`exclude: []` — an empty include array, which GitHub's create API rejects +(you must use `~ALL` or `~DEFAULT_BRANCH`), yet it existed via GET. Flagged as +a landmine to fix (`~DEFAULT_BRANCH`) before propagating this ruleset as a +template. + +## 5. Inspect the org's special `.github` repo for existing propagation tooling + +```bash +echo "=== quantum-l9-dotgithub structure ===" && \ + find "$HOME/quantum-l9-dotgithub" -maxdepth 3 -type f | sort +``` + +Found `ops/branch-protect.sh` — applies branch protection to that repo itself +only, not org-wide (rulesets are per-repo API objects; they are not +distributed automatically the way community-health files are via `.github`). + +## 6. List all Quantum-L9 repos with archive/fork/visibility status + +```bash +gh repo list Quantum-L9 --limit 200 --json name,isArchived,isFork,isPrivate,visibility | \ + python3 -m json.tool +``` + +## 7. Check CodeQL coverage and existing rulesets across every repo + +```bash +for repo in Cognitive.Engine.Graphs l9-meta-injector Constellation.Gate LLM-Router SEO-Bot \ + Gate_SDK Website-Bot Enrichment.Inference.Engine l9-ci-core Cursor-Governance \ + l9-assurance l9-harness l9-ci-sdk igorbot l9-graphiti-memory l9-deploy \ + L9-Node-Template l9-constellation-topology .github l9-ci-debt-lsp \ + l9-ci-debt-intelligence l9-ci-debt-resolver L9-Graphite-Memory L9-Ops-MCP \ + l9-cognitive-runtime PR_Repair infisical-config l9-infra l9-tools \ + l9-repo-template Constellation.PackageTemplate DeckhouseOdoo Governance-Active \ + SustainabilitySolutions1 ai-agency-genesis-portal Quantum-Website-Cursor \ + quantum-dashboard SplitWisely.ai timeAutomation_v1.0; do + has_codeql=$(gh api "repos/Quantum-L9/$repo/code-scanning/analyses" 2>&1 | \ + python3 -c "import json,sys; d=sys.stdin.read(); print('yes' if d.strip().startswith('[') and len(json.loads(d))>0 else 'no')" 2>/dev/null || echo "no/err") + rulesets=$(gh api "repos/Quantum-L9/$repo/rulesets" 2>&1 | \ + python3 -c "import json,sys; d=json.load(sys.stdin); print(','.join([r['name'] for r in d]) if isinstance(d,list) else 'ERR')" 2>/dev/null || echo "ERR") + echo "$repo | codeql_analyses=$has_codeql | rulesets=$rulesets" +done +``` + +Finding: only 7 of 29 public repos had CodeQL analyses running; most repos had +only the auto-generated "Code Quality Copilot review" ruleset; 3 repos +(`l9-meta-injector`, `l9-assurance`, `l9-graphiti-memory`) had zero rulesets. +This fed directly into the `scope` decision question (tiered rollout vs. full +CodeQL-required rollout vs. dry-run only). + +## 8. Check raw error shape on a private-repo ruleset query + +```bash +gh api repos/Quantum-L9/l9-harness/rulesets +``` + +## 9. Verify org plan upgrade and re-check org-level ruleset API access + +```bash +echo "=== org plan now ===" && gh api orgs/Quantum-L9 | \ + python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('plan'))" + +echo +echo "=== org rulesets API now unlocked? ===" && gh api orgs/Quantum-L9/rulesets +``` + +Run again after the user upgraded the org to GitHub Enterprise — confirmed the +`orgs/Quantum-L9/rulesets` endpoint was accessible, unblocking true org-level +ruleset enforcement (deferred until after the CI rollout is verified green, +per the `org_ruleset` decision: *"After rollout, once I've verified checks are +running green everywhere"*). + +--- + +## Outcome + +These diagnostics directly shaped three decisions surfaced via `AskQuestion`: + +1. **Path**: write a rollout script vs. upgrade to GitHub Team — superseded by + the user upgrading straight to Enterprise. +2. **Scope**: tiered ruleset rollout (baseline on all 29 public repos; full + CodeQL gate only on the 7 with CodeQL already running) vs. full/dry-run. +3. **Org-ruleset timing**: apply org-level enforcement *after* rollout is + verified green — not yet actioned; tracked as a follow-up once the CI + preset rollout (see `l9-ci-core` / `l9-ci-sdk` work) is fully green across + all 24 activated repos. diff --git a/engine/handlers.py b/engine/handlers.py index 8a27df77..10e1163a 100644 --- a/engine/handlers.py +++ b/engine/handlers.py @@ -19,6 +19,7 @@ import re import time import uuid +from collections.abc import Awaitable, Callable from typing import Any # T5-03: handlers.py is the sanctioned chassis bridge. Re-export chassis @@ -2000,19 +2001,29 @@ async def handle_enrich(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: return {"enriched_count": count, "entity_type": entity_type, "tenant": tenant} +# CONTRACT-02: single source of truth for the engine's action surface. +# Both chassis implementations (legacy chassis/actions.py and the SDK-native +# chassis/handler_registration.py) route off this dict rather than each +# maintaining their own action list, so the two chassis can't drift apart. +ACTION_HANDLERS: dict[str, Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]]] = { + "match": handle_match, + "sync": handle_sync, + "admin": handle_admin, + "outcomes": handle_outcomes, + "resolve": handle_resolve, + "health": handle_health, + "healthcheck": handle_healthcheck, + "enrich": handle_enrich, +} + + def register_all(chassis_router: Any) -> None: - """Register all 8 action handlers with a legacy chassis router interface. + """Register all action handlers with a legacy chassis router interface. The primary registration path is via chassis.actions._init_engine() - which builds the handler dict directly. This function exists for + which consumes ACTION_HANDLERS directly. This function exists for chassis implementations that use a router.register_handler() pattern. """ - chassis_router.register_handler("match", handle_match) - chassis_router.register_handler("sync", handle_sync) - chassis_router.register_handler("admin", handle_admin) - chassis_router.register_handler("outcomes", handle_outcomes) - chassis_router.register_handler("resolve", handle_resolve) - chassis_router.register_handler("health", handle_health) - chassis_router.register_handler("healthcheck", handle_healthcheck) - chassis_router.register_handler("enrich", handle_enrich) - logger.info("Registered 8 action handlers: match, sync, admin, outcomes, resolve, health, healthcheck, enrich") + for action, handler in ACTION_HANDLERS.items(): + chassis_router.register_handler(action, handler) + logger.info("Registered %d action handlers: %s", len(ACTION_HANDLERS), ", ".join(ACTION_HANDLERS)) diff --git a/engine/security/P2_9_llm_schemas.py b/engine/security/P2_9_llm_schemas.py index b0ab9792..e8e49b17 100644 --- a/engine/security/P2_9_llm_schemas.py +++ b/engine/security/P2_9_llm_schemas.py @@ -132,11 +132,8 @@ def _ensure_client(self, model: str) -> Any: if self._client is not None: return self._client - try: - from openai import OpenAI - except ImportError as exc: - raise RuntimeError("openai package is required for LLM features. Install with: pip install openai") from exc - + # Prefer a clear missing-key error over a missing-package error when + # both are absent (CI installs requirements-ci.txt without openai). api_key = os.environ.get("OPENAI_API_KEY") if not api_key: raise RuntimeError( @@ -144,6 +141,11 @@ def _ensure_client(self, model: str) -> Any: "Set it to your OpenAI (or compatible provider) API key." ) + try: + from openai import OpenAI + except ImportError as exc: + raise RuntimeError("openai package is required for LLM features. Install with: pip install openai") from exc + kwargs: dict[str, Any] = {"api_key": api_key} base_url = os.environ.get("OPENAI_BASE_URL") if base_url: diff --git a/poetry.lock b/poetry.lock index 4a751aa7..0aacb79f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-doc" @@ -112,6 +112,76 @@ files = [ {file = "ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b"}, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.9.0" +groups = ["main"] +files = [ + {file = "asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61"}, + {file = "asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be"}, + {file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8"}, + {file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1"}, + {file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3"}, + {file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8"}, + {file = "asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095"}, + {file = "asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540"}, + {file = "asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d"}, + {file = "asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab"}, + {file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c"}, + {file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109"}, + {file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da"}, + {file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9"}, + {file = "asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24"}, + {file = "asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047"}, + {file = "asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad"}, + {file = "asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d"}, + {file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a"}, + {file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671"}, + {file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec"}, + {file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20"}, + {file = "asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8"}, + {file = "asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186"}, + {file = "asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b"}, + {file = "asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e"}, + {file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403"}, + {file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4"}, + {file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2"}, + {file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602"}, + {file = "asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696"}, + {file = "asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab"}, + {file = "asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44"}, + {file = "asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5"}, + {file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2"}, + {file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2"}, + {file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218"}, + {file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d"}, + {file = "asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b"}, + {file = "asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be"}, + {file = "asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2"}, + {file = "asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31"}, + {file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7"}, + {file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e"}, + {file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c"}, + {file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a"}, + {file = "asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d"}, + {file = "asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3"}, + {file = "asyncpg-0.31.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb3cde58321a1f89ce41812be3f2a98dddedc1e76d0838aba1d724f1e4e1a95"}, + {file = "asyncpg-0.31.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6974f36eb9a224d8fb428bcf66bd411aa12cf57c2967463178149e73d4de366"}, + {file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2b685f400ceae428f79f78b58110470d7b4466929a7f78d455964b17ad1008"}, + {file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb223567dea5f47c45d347f2bde5486be8d9f40339f27217adb3fb1c3be51298"}, + {file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22be6e02381bab3101cd502d9297ac71e2f966c86e20e78caead9934c98a8af6"}, + {file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37a58919cfef2448a920df00d1b2f821762d17194d0dbf355d6dde8d952c04f9"}, + {file = "asyncpg-0.31.0-cp39-cp39-win32.whl", hash = "sha256:c1a9c5b71d2371a2290bc93336cd05ba4ec781683cab292adbddc084f89443c6"}, + {file = "asyncpg-0.31.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1e1ab5bc65373d92dd749d7308c5b26fb2dc0fbe5d3bf68a32b676aa3bcd24a"}, + {file = "asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735"}, +] + +[package.extras] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] + [[package]] name = "certifi" version = "2026.2.25" @@ -602,6 +672,18 @@ cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and pla [package.extras] ssh = ["bcrypt (>=3.1.5)"] +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + [[package]] name = "docker" version = "7.1.0" @@ -839,6 +921,121 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "jiter" +version = "0.16.0" +description = "Fast iterable JSON parser." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c"}, + {file = "jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f"}, + {file = "jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131"}, + {file = "jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b"}, + {file = "jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9"}, + {file = "jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26"}, + {file = "jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3"}, + {file = "jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea"}, + {file = "jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91"}, + {file = "jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3"}, + {file = "jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7"}, + {file = "jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1"}, + {file = "jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056"}, + {file = "jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a"}, + {file = "jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5"}, + {file = "jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730"}, + {file = "jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f"}, + {file = "jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274"}, + {file = "jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7"}, + {file = "jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331"}, + {file = "jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195"}, + {file = "jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e"}, + {file = "jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb"}, + {file = "jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84"}, + {file = "jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e"}, + {file = "jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd"}, + {file = "jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a"}, + {file = "jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee"}, + {file = "jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93"}, + {file = "jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a"}, + {file = "jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00"}, + {file = "jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe"}, + {file = "jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106"}, + {file = "jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8"}, + {file = "jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585"}, + {file = "jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e"}, + {file = "jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077"}, + {file = "jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734"}, + {file = "jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf"}, + {file = "jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db"}, + {file = "jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce"}, + {file = "jiter-0.16.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d8f80521644426d451e70f00c7974240cab8f6ee088aedaa9af2697153ab7805"}, + {file = "jiter-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b21b412b899fd8bd51a3046934b59a3bb068b79f70a5c6010053ac77cc53f0c"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0758ab7747a984797cf048e8eedea1d8ef39d7994b25611daf5b48fc903e8873"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ec553a99b0987efd7a3645a1a825cf29c224e494db267a83369fcc8da9aeda5"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26d122613ada2b708eb714695446f40fce5bdf2edb4b02116dec62faa62dfab3"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e03a5f21a5ce96a9441b8cb32719a8b88ed5388f53e0f339c5bcf54f1317f9d0"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:a5c54ef4ff776d9675837ef535b3308d6e31c208d43ebc44a0f7ab8a208c68f7"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1e7923093a376d93c6eb507c77045ae258d689ba577392846a1b3f10d0b09a9"}, + {file = "jiter-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2a0d46ef67cc58d906a6132dd3040ca70ae4f0b0d7c9c052fe432c658a69b3f6"}, + {file = "jiter-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70a490b55634dc0d2606ce8a8e01b1d62459011beb368d15d76e1eaf62460e3d"}, + {file = "jiter-0.16.0-cp39-cp39-win32.whl", hash = "sha256:9acf1b2faec82d998811ecce7ae84d9005e53410773e9d37d61cdc424ba4581b"}, + {file = "jiter-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:491e7d072a253b156fff46b78bceac4652a697aa8d7082c9c18c03d7b7917d24"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2"}, + {file = "jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c"}, +] + [[package]] name = "librt" version = "0.11.0" @@ -1125,6 +1322,34 @@ files = [ {file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"}, ] +[[package]] +name = "openai" +version = "1.109.1" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, + {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +jiter = ">=0.4.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.11,<5" + +[package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] +realtime = ["websockets (>=13,<16)"] +voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] + [[package]] name = "packaging" version = "26.0" @@ -1506,6 +1731,21 @@ files = [ [package.extras] dev = ["black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] +[[package]] +name = "python-multipart" +version = "0.0.9" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[package.extras] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + [[package]] name = "pytz" version = "2026.1.post1" @@ -1702,6 +1942,18 @@ files = [ {file = "ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1800,6 +2052,27 @@ test-module-import = ["httpx"] trino = ["trino"] weaviate = ["weaviate-client (>=4.5.4,<5.0.0)"] +[[package]] +name = "tqdm" +version = "4.69.0" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622"}, + {file = "tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +discord = ["envwrap", "requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] + [[package]] name = "types-pyyaml" version = "6.0.12.20250915" @@ -2278,4 +2551,4 @@ dev = ["pytest", "setuptools"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "0162098808d6f7ffbbdee5c39923ea5c62ae17b5e29539f1b748704fce4e03a4" +content-hash = "516375a9c7da87f69c1b153a0165d3bf84d3c6fe9ea5a2d764bd3c9a452675b0" diff --git a/pyproject.toml b/pyproject.toml index e46c7213..fe5f1a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "l9-engine" version = "0.1.0" description = "Domain-agnostic graph-native matching engine" authors = ["L9 "] +license = "LicenseRef-Proprietary" readme = "README.md" packages = [{include = "engine"}] @@ -14,12 +15,15 @@ neo4j = "^5.25.0" pydantic = "^2.10.0" pydantic-settings = "^2.6.0" pyyaml = "^6.0" +python-multipart = "^0.0.9" redis = "^7.4.0" apscheduler = "^3.10.0" httpx = "^0.28.0" structlog = "^25.5.0" prometheus-client = "^0.24.1" numpy = "^2.4.3" +openai = "^1.0.0" +asyncpg = "^0.31.0" constellation-node-sdk = {git = "https://github.com/cryptoxdog/Gate_SDK.git"} [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt index db0c18f1..82d4e333 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,12 +10,12 @@ pydantic-settings>=2.6.0,<3.0.0 pyyaml>=6.0,<7.0 types-PyYAML>=6.0 python-multipart>=0.0.9,<1.0.0 -redis>=5.2.0,<6.0.0 +redis>=7.4.0,<8.0.0 apscheduler>=3.10.0,<4.0.0 httpx>=0.28.0,<0.29.0 structlog>=25.5.0,<26.0.0 prometheus-client>=0.24.1,<1.0.0 -numpy>=1.26.0,<2.0.0 +numpy>=2.4.3,<3.0.0 openai>=1.0.0,<2.0.0 asyncpg>=0.31.0,<1.0.0 constellation-node-sdk @ git+https://github.com/cryptoxdog/Gate_SDK.git diff --git a/scripts/scripts-deploy.sh b/scripts/scripts-deploy.sh deleted file mode 100644 index 02500687..00000000 --- a/scripts/scripts-deploy.sh +++ /dev/null @@ -1,86 +0,0 @@ -# --- L9_META --- -# l9_schema: 1 -# origin: l9-template -# engine: graph -# layer: [scripts] -# tags: [L9_TEMPLATE, scripts, deploy] -# owner: platform -# status: active -# --- /L9_META --- -# -!/usr/bin/env bash ---- L9_META --- -l9_schema: 1 -origin: l9-template -engine: graph -layer: [scripts] -tags: [L9_TEMPLATE, scripts, deploy] -owner: platform -status: active ---- /L9_META --- -============================================================================ -deploy.sh — Deploy infrastructure via Terraform -============================================================================ -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(dirname "$SCRIPT_DIR")" -IAC_DIR="$ROOT_DIR/iac" - -ENV="${1:-dev}" -ACTION="${2:-apply}" - -echo "🚀 L9 Deploy — env=${ENV}, action=${ACTION}" - -if [ ! -d "$IAC_DIR" ]; then - echo "❌ iac/ directory not found" - exit 1 -fi - -cd "$IAC_DIR" - ---- Init --- -terraform init -backend-config="key=${L9_PROJECT:-l9-engine}/${ENV}/terraform.tfstate" - ---- Select workspace --- -terraform workspace select "$ENV" 2>/dev/null || terraform workspace new "$ENV" - ---- Plan or Apply --- -case "$ACTION" in - plan) - terraform plan -var="env=${ENV}" -var-file="${ENV}.tfvars" 2>/dev/null || terraform plan -var="env=${ENV}" - ;; - - apply) - terraform plan -var="env=${ENV}" -var-file="${ENV}.tfvars" -out=plan.out 2>/dev/null || terraform plan -var="env=${ENV}" -out=plan.out - - echo "" - read -p "Apply? [y/N] " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - terraform apply plan.out - rm -f plan.out - echo "✅ Deployed to ${ENV}" - - echo "" - echo "Outputs:" - terraform output - fi - ;; - - destroy) - echo "⚠️ DESTROYING ${ENV} environment" - read -p "Are you sure? Type env name to confirm: " CONFIRM - if [ "$CONFIRM" = "$ENV" ]; then - terraform destroy -var="env=${ENV}" -auto-approve - echo "✅ Destroyed ${ENV}" - else - echo "❌ Aborted" - fi - ;; - - *) - echo "Usage: deploy.sh [plan|apply|destroy]" - exit 1 - ;; -esac diff --git a/tests/contracts/test_chassis_parity.py b/tests/contracts/test_chassis_parity.py new file mode 100644 index 00000000..8506df55 --- /dev/null +++ b/tests/contracts/test_chassis_parity.py @@ -0,0 +1,80 @@ +""" +--- L9_META --- +l9_schema: 2 +origin: engine-specific +engine: graph +layer: [test] +tags: [platform, chassis] +status: active +--- /L9_META --- + +Contract test: the legacy chassis and the SDK chassis route the same action set. + +engine.handlers.ACTION_HANDLERS is the single source of truth (CONTRACT-02). +These assertions are cheap tautologies today; they exist so that a future edit +which reintroduces a hand-maintained action list in either chassis fails here +rather than in production. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +from constellation_node_sdk import clear_handlers, registered_actions + +from chassis.handler_registration import register_engine_handlers +from engine.handlers import ACTION_HANDLERS + +if TYPE_CHECKING: + from collections.abc import Iterator + +EXPECTED_ACTIONS = { + "match", + "sync", + "admin", + "outcomes", + "resolve", + "health", + "healthcheck", + "enrich", +} + + +@pytest.fixture(autouse=True) +def _clean_registry() -> Iterator[None]: + clear_handlers() + yield + clear_handlers() + + +def test_action_handlers_is_the_declared_action_set() -> None: + assert set(ACTION_HANDLERS) == EXPECTED_ACTIONS + + +def test_sdk_registry_matches_action_handlers() -> None: + register_engine_handlers() + assert set(registered_actions()) == set(ACTION_HANDLERS) + + +def test_legacy_chassis_routes_action_handlers() -> None: + """chassis/actions.py consumes ACTION_HANDLERS rather than rebuilding it.""" + from chassis import actions + + actions._init_engine() + assert set(actions._engine_handlers) == set(ACTION_HANDLERS) + + +def test_entrypoint_dispatch_targets_both_chassis() -> None: + from chassis.entrypoint import LEGACY, SDK, resolve_chassis + + assert (LEGACY, SDK) == ("legacy", "sdk") + assert resolve_chassis() in (LEGACY, SDK) + + +def test_entrypoint_rejects_unknown_chassis(monkeypatch: pytest.MonkeyPatch) -> None: + from chassis.entrypoint import resolve_chassis + + monkeypatch.setenv("L9_CHASSIS", "nope") + with pytest.raises(ValueError, match="L9_CHASSIS"): + resolve_chassis() diff --git a/tests/contracts/test_contract_registry.py b/tests/contracts/test_contract_registry.py new file mode 100644 index 00000000..dbf9383f --- /dev/null +++ b/tests/contracts/test_contract_registry.py @@ -0,0 +1,147 @@ +""" +Contract registry drift gate. + +Asserts that the three halves of the contract system agree: + +- ``contracts/contract_NN.yaml`` — identity and wiring (SSOT) +- ``docs/contracts/*.md`` — prose (SSOT) +- ``tools/contract_scanner.py`` — grep-able rule IDs +- ``tests/contracts/test_contracts.py`` — behavioral assertions + +Any pointer that goes stale in one half without the other is caught here +rather than silently rotting. +""" + +from __future__ import annotations + +import ast +import re +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parent.parent.parent +CONTRACTS_DIR = ROOT / "contracts" +SCANNER = ROOT / "tools" / "contract_scanner.py" +VERIFIER = ROOT / "tools" / "verify_contracts.py" + +EXPECTED_COUNT = 24 +REQUIRED_KEYS = {"id", "name", "layer", "level", "scope", "preconditions", "postconditions", "verification", "docs"} +VALID_LEVELS = {"MUST", "SHOULD", "MAY"} + + +def _load_contracts() -> list[dict]: + specs = [] + for f in sorted(CONTRACTS_DIR.glob("contract_*.yaml")): + specs.append((f.name, yaml.safe_load(f.read_text(encoding="utf-8")))) + return specs + + +CONTRACTS = _load_contracts() + + +def _scanner_rule_ids() -> set[str]: + """Extract every rule ID registered in contract_scanner.py via AST.""" + tree = ast.parse(SCANNER.read_text(encoding="utf-8"), filename=str(SCANNER)) + ids: set[str] = set() + for node in ast.walk(tree): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_rule" + and node.args + and isinstance(node.args[0], ast.Constant) + ): + ids.add(node.args[0].value) + return ids + + +def _test_class_names() -> set[str]: + """Class names defined in the monolithic contract test module.""" + path = ROOT / "tests" / "contracts" / "test_contracts.py" + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + return {n.name for n in tree.body if isinstance(n, ast.ClassDef)} + + +def _required_contract_docs() -> list[str]: + """REQUIRED_CONTRACTS list from verify_contracts.py, read without importing.""" + tree = ast.parse(VERIFIER.read_text(encoding="utf-8"), filename=str(VERIFIER)) + for node in tree.body: + if isinstance(node, ast.Assign) and any( + isinstance(t, ast.Name) and t.id == "REQUIRED_CONTRACTS" for t in node.targets + ): + return [e.value for e in node.value.elts if isinstance(e, ast.Constant)] + msg = "REQUIRED_CONTRACTS not found in tools/verify_contracts.py" + raise AssertionError(msg) + + +@pytest.mark.contract +def test_ids_are_contiguous_and_unique(): + ids = [spec["id"] for _f, spec in CONTRACTS] + assert len(ids) == len(set(ids)), f"Duplicate contract IDs: {ids}" + expected = [f"CONTRACT-{n:02d}" for n in range(1, EXPECTED_COUNT + 1)] + assert sorted(ids) == expected, f"Contract IDs must be {expected[0]}..{expected[-1]} with no gaps" + + +@pytest.mark.contract +def test_filename_matches_id(): + for filename, spec in CONTRACTS: + number = re.fullmatch(r"contract_(\d{2})\.yaml", filename) + assert number, f"Unexpected contract filename: {filename}" + assert spec["id"] == f"CONTRACT-{number.group(1)}", f"{filename} declares {spec['id']}" + + +@pytest.mark.contract +@pytest.mark.parametrize(("filename", "spec"), CONTRACTS, ids=[f for f, _ in CONTRACTS]) +def test_schema_is_uniform(filename, spec): + missing = REQUIRED_KEYS - set(spec) + assert not missing, f"{filename} missing keys: {sorted(missing)}" + assert spec["level"] in VALID_LEVELS, f"{filename} has invalid level {spec['level']!r}" + assert isinstance(spec["scope"].get("paths"), list), f"{filename} scope.paths must be a list" + assert isinstance(spec["docs"], list), f"{filename} docs must be a list" + assert spec["docs"], f"{filename} docs must be non-empty" + verification = spec["verification"] + assert isinstance(verification.get("scanner_rules"), list), f"{filename} scanner_rules must be a list" + assert isinstance(verification.get("test"), str), f"{filename} verification.test must be a string" + + +@pytest.mark.contract +@pytest.mark.parametrize(("filename", "spec"), CONTRACTS, ids=[f for f, _ in CONTRACTS]) +def test_docs_paths_exist(filename, spec): + missing = [d for d in spec["docs"] if not (ROOT / d).is_file()] + assert not missing, f"{filename} points at non-existent docs: {missing}" + + +@pytest.mark.contract +@pytest.mark.parametrize(("filename", "spec"), CONTRACTS, ids=[f for f, _ in CONTRACTS]) +def test_scanner_rules_are_registered(filename, spec): + known = _scanner_rule_ids() + declared = spec["verification"]["scanner_rules"] + unknown = [r for r in declared if r not in known] + assert not unknown, f"{filename} references unregistered scanner rules: {unknown}" + + +@pytest.mark.contract +@pytest.mark.parametrize(("filename", "spec"), CONTRACTS, ids=[f for f, _ in CONTRACTS]) +def test_verification_test_resolves(filename, spec): + node_id = spec["verification"]["test"] + file_part, _, class_part = node_id.partition("::") + assert (ROOT / file_part).exists(), f"{filename} test path does not exist: {file_part}" + assert class_part, f"{filename} verification.test must be a pytest node ID, got {node_id!r}" + assert class_part in _test_class_names(), f"{filename} references missing test class: {class_part}" + + +@pytest.mark.contract +def test_every_required_doc_is_claimed_by_a_contract(): + """Reverse direction: no orphaned markdown in the required set.""" + claimed = {d for _f, spec in CONTRACTS for d in spec["docs"]} + orphaned = [d for d in _required_contract_docs() if d not in claimed] + assert not orphaned, f"Required contract docs not referenced by any YAML docs: field: {orphaned}" + + +@pytest.mark.contract +def test_every_scanner_rule_is_claimed_by_a_contract(): + claimed = {r for _f, spec in CONTRACTS for r in spec["verification"]["scanner_rules"]} + orphaned = sorted(_scanner_rule_ids() - claimed) + assert not orphaned, f"Scanner rules not claimed by any contract YAML: {orphaned}" diff --git a/tests/contracts/test_contracts.py b/tests/contracts/test_contracts.py index 019ecbf5..207fef50 100644 --- a/tests/contracts/test_contracts.py +++ b/tests/contracts/test_contracts.py @@ -1,7 +1,16 @@ """ +--- L9_META --- +l9_schema: 2 +origin: l9-template +engine: graph +layer: [test] +tags: [governance, compliance] +status: active +--- /L9_META --- + Contract verification test suite. -One test per CEG contract (20 contracts defined in .cursorrules). +One test class per CEG contract (24 invariants defined in contracts/*.yaml). Verifies that architectural invariants hold across the codebase without requiring a running Neo4j instance. """ @@ -664,6 +673,127 @@ def test_kge_scoring_dimension_exists(self): assert ComputationType.KGE.value == "kge" +# ============================================================================ +# LAYER 7 - HARDENING (contracts 21-24) +# ============================================================================ + + +class TestContract21FeatureFlagDiscipline: + """CONTRACT 21: behavioral changes are gated by bool flags in settings.py.""" + + @pytest.mark.contract + def test_feature_flags_are_declared_bool(self): + from engine.config.settings import Settings + + flags = [name for name in Settings.model_fields if name.endswith("_enabled")] + assert flags, "No *_enabled feature flags found in Settings" + non_bool = [n for n in flags if Settings.model_fields[n].annotation is not bool] + assert not non_bool, f"Feature flags must be bool: {non_bool}" + + @pytest.mark.contract + def test_feature_gates_doc_exists(self): + doc = ROOT / "docs" / "FEATURE_GATES.md" + assert doc.exists(), "docs/FEATURE_GATES.md is required by CONTRACT 21" + + @pytest.mark.contract + def test_every_flag_is_documented(self): + from engine.config.settings import Settings + + doc_text = (ROOT / "docs" / "FEATURE_GATES.md").read_text(encoding="utf-8") + flags = [name for name in Settings.model_fields if name.endswith("_enabled")] + undocumented = [f for f in flags if f not in doc_text and f.upper() not in doc_text] + assert not undocumented, f"Feature flags missing from FEATURE_GATES.md: {undocumented}" + + +class TestContract22ScoringWeightCeiling: + """CONTRACT 22: default scoring weights sum to <= 1.0, asserted at startup.""" + + @pytest.mark.contract + def test_weight_sum_assertion_exists(self): + from engine import boot + + assert callable(boot._assert_default_weight_sum) + assert boot._WEIGHT_CEILING == 1.0 + + @pytest.mark.contract + def test_default_weights_within_ceiling(self): + from engine.boot import _WEIGHT_CEILING + from engine.config.settings import settings + + total = settings.w_structural + settings.w_geo + settings.w_reinforcement + settings.w_freshness + assert total <= _WEIGHT_CEILING + 1e-9, f"Default weight sum {total} exceeds {_WEIGHT_CEILING}" + + @pytest.mark.contract + def test_assertion_raises_when_exceeded(self, monkeypatch): + from engine import boot + + monkeypatch.setattr(boot.settings, "w_structural", 0.9) + monkeypatch.setattr(boot.settings, "w_geo", 0.9) + with pytest.raises(ValueError, match="exceeding"): + boot._assert_default_weight_sum() + + +class TestContract23AdminSubactionRegistration: + """CONTRACT 23: admin subactions are snake_case and validate inputs.""" + + @pytest.mark.contract + def test_subaction_names_are_snake_case(self): + source = (ROOT / "engine" / "handlers.py").read_text(encoding="utf-8") + names = set(re.findall(r'subaction\s*==\s*"([^"]+)"', source)) + assert names, "No admin subactions found in engine/handlers.py" + bad = [n for n in names if not re.fullmatch(r"[a-z][a-z0-9_]*", n)] + assert not bad, f"Admin subactions must be snake_case: {bad}" + + @pytest.mark.contract + def test_admin_handler_validates_subaction_key(self): + source = (ROOT / "engine" / "handlers.py").read_text(encoding="utf-8") + assert '_require_key(payload, "subaction", "admin"' in source, ( + "handle_admin must resolve 'subaction' via _require_key()" + ) + + @pytest.mark.contract + def test_require_key_raises_on_missing(self): + from engine.handlers import _require_key + + with pytest.raises(Exception, match="(?i)subaction|missing|required"): + _require_key({}, "subaction", "admin", "t1") + + +class TestContract24ResiliencePatterns: + """CONTRACT 24: queries go through GraphDriver; caches are bounded.""" + + @pytest.mark.contract + def test_driver_exposes_circuit_protected_execute(self): + from engine.graph.driver import GraphDriver + + assert hasattr(GraphDriver, "execute_query") + assert hasattr(GraphDriver, "circuit_breaker") + + @pytest.mark.contract + def test_circuit_breaker_settings_are_configurable(self): + from engine.config.settings import settings + + assert settings.neo4j_circuit_threshold > 0 + assert settings.neo4j_circuit_cooldown > 0 + assert settings.neo4j_circuit_half_open_max > 0 + + @pytest.mark.contract + def test_domain_cache_is_bounded(self): + from engine.config.settings import settings + + assert settings.domain_cache_maxsize > 0, "Domain pack cache must be bounded" + + @pytest.mark.contract + def test_engine_has_no_raw_neo4j_sessions(self): + violations = [] + for f in _engine_py_files(): + if f.name in {"driver.py", "circuit_breaker.py"}: + continue + if ".session(" in f.read_text(encoding="utf-8"): + violations.append(str(f.relative_to(ROOT))) + assert not violations, f"Raw Neo4j sessions outside GraphDriver: {violations}" + + # ============================================================================ # Helpers for building minimal DomainSpec instances # ============================================================================ diff --git a/tests/unit/test_node_app.py b/tests/unit/test_node_app.py new file mode 100644 index 00000000..edc018b6 --- /dev/null +++ b/tests/unit/test_node_app.py @@ -0,0 +1,324 @@ +""" +--- L9_META --- +l9_schema: 2 +origin: engine-specific +engine: graph +layer: [test] +tags: [platform, chassis] +status: active +--- /L9_META --- + +Unit tests for chassis/node_app.py and chassis/handler_registration.py — +the SDK-native chassis selected by L9_CHASSIS=sdk. + +NodeRuntimeConfig is built explicitly and passed to create_node_app(config=...) +rather than read from the environment: get_runtime_config() is lru_cached and +would leak across tests. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest +from constellation_node_sdk import ( + NodeRuntimeConfig, + clear_handlers, + create_node_app, + register_handler, + registered_actions, +) +from constellation_node_sdk.transport.packet import create_transport_packet +from constellation_node_sdk.transport.provenance import RoutingProvenance +from fastapi.testclient import TestClient +from pydantic import ValidationError + +from chassis import handler_registration +from chassis.handler_registration import _with_packet_audit, register_engine_handlers +from chassis.node_app import _gate_ingress_violation, _install_gate_only_ingress +from engine.handlers import ACTION_HANDLERS + +if TYPE_CHECKING: + from collections.abc import Iterator + +NODE_NAME = "graph-engine" +GATE_NODE = "gate" + + +# ── Fixtures ──────────────────────────────────────────────────────────────── + + +@pytest.fixture(autouse=True) +def _clean_registry() -> Iterator[None]: + """The SDK handler registry is module-global; isolate every test.""" + clear_handlers() + yield + clear_handlers() + + +@pytest.fixture(autouse=True) +def persisted_packets(monkeypatch: pytest.MonkeyPatch) -> list[tuple[Any, Any]]: + """Replace PacketStore.persist with an in-memory recorder.""" + persisted: list[tuple[Any, Any]] = [] + + class _Recorder: + async def persist(self, request: Any, response: Any) -> None: + persisted.append((request, response)) + + monkeypatch.setattr(handler_registration, "get_packet_store", _Recorder) + return persisted + + +def _config(**overrides: Any) -> NodeRuntimeConfig: + base: dict[str, Any] = { + "environment": "test", + "node_name": NODE_NAME, + "service_name": NODE_NAME, + "service_version": "1.1.0", + "require_signature": False, + "allowed_actions": tuple(ACTION_HANDLERS), + "max_attachments": 0, + # The SDK's own defaults are mutually invalid (10MB attachment cap vs a + # 256KB packet cap), so this must be set explicitly for any valid config. + "max_attachment_size_bytes": 0, + } + base.update(overrides) + fields = getattr(NodeRuntimeConfig, "model_fields", {}) + if "enforce_gate_only_ingress" in fields and "enforce_gate_only_ingress" not in base: + base["enforce_gate_only_ingress"] = False + return NodeRuntimeConfig(**base) + + +def _gate_packet(action: str = "match", tenant: str = "plasticos", **payload: Any) -> dict[str, Any]: + """Build a Gate-authored request packet as a JSON-safe dict.""" + packet = create_transport_packet( + action=action, + payload=payload or {"query": {}}, + tenant=tenant, + source_node=GATE_NODE, + destination_node=NODE_NAME, + reply_to=GATE_NODE, + provenance=RoutingProvenance( + origin_kind="gate", + requested_action=action, + resolved_by_gate=True, + ), + ) + return packet.model_dump_json_dict() + + +def _app(config: NodeRuntimeConfig | None = None, *, gate_only: bool = True): + app = create_node_app(config=config or _config(), auto_register_with_gate=False) + if gate_only: + _install_gate_only_ingress(app, gate_node=GATE_NODE) + return app + + +# ── Handler registry ──────────────────────────────────────────────────────── + + +def test_registered_actions_match_action_handlers() -> None: + register_engine_handlers() + assert set(registered_actions()) == set(ACTION_HANDLERS) + + +def test_audit_wrapper_keeps_two_parameter_signature() -> None: + """SDK _invoke_handler dispatches on parameter count — *args would misroute.""" + import inspect + + async def handler(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + return {} + + wrapped = _with_packet_audit("match", handler) + assert len(inspect.signature(wrapped).parameters) == 2 + + +# ── Routing and tenant invariant ──────────────────────────────────────────── + + +def test_gate_packet_routes_to_handler_and_returns_response() -> None: + seen: list[tuple[str, dict[str, Any]]] = [] + + async def spy(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + seen.append((tenant, payload)) + return {"matches": []} + + register_handler("match", _with_packet_audit("match", spy)) + + with TestClient(_app()) as client: + response = client.post("/v1/execute", json=_gate_packet(query={"a": 1})) + + assert response.status_code == 200 + assert response.json()["header"]["packet_type"] == "response" + assert len(seen) == 1 + + +def test_tenant_org_id_passthrough() -> None: + """packet.tenant.org_id is the CEG domain_id — the DomainPackLoader key.""" + seen: list[str] = [] + + async def spy(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + seen.append(tenant) + return {} + + register_handler("match", _with_packet_audit("match", spy)) + + with TestClient(_app()) as client: + client.post("/v1/execute", json=_gate_packet(tenant="plasticos")) + + assert seen == ["plasticos"] + + +def test_packet_store_persists_once_per_execute( + persisted_packets: list[tuple[Any, Any]], +) -> None: + async def spy(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + return {"ok": True} + + register_handler("match", _with_packet_audit("match", spy)) + + with TestClient(_app()) as client: + client.post("/v1/execute", json=_gate_packet()) + + assert len(persisted_packets) == 1 + + +def test_failing_handler_persists_pair_and_returns_failure_packet( + persisted_packets: list[tuple[Any, Any]], +) -> None: + async def boom(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + msg = "handler exploded" + raise RuntimeError(msg) + + register_handler("match", _with_packet_audit("match", boom)) + + with TestClient(_app()) as client: + response = client.post("/v1/execute", json=_gate_packet()) + + # return_transport_errors=true: a transport failure packet, not HTTP 500. + assert response.status_code == 200 + assert response.json()["header"]["packet_type"] == "failure" + assert len(persisted_packets) == 1 + + +# ── Gate-only ingress ─────────────────────────────────────────────────────── + + +@pytest.mark.parametrize( + ("provenance", "address", "fragment"), + [ + ( + {"origin_kind": "node", "requested_action": "match", "resolved_by_gate": True}, + {"source_node": "gate", "destination_node": NODE_NAME, "reply_to": "gate"}, + "origin_kind", + ), + ( + {"origin_kind": "gate", "requested_action": "match", "resolved_by_gate": False}, + {"source_node": "gate", "destination_node": NODE_NAME, "reply_to": "gate"}, + "resolved_by_gate", + ), + ( + {"origin_kind": "gate", "requested_action": "match", "resolved_by_gate": True}, + {"source_node": "client", "destination_node": NODE_NAME, "reply_to": "client"}, + "source_node", + ), + ], +) +def test_gate_ingress_violation_detects( + provenance: dict[str, Any], + address: dict[str, Any], + fragment: str, +) -> None: + reason = _gate_ingress_violation({"provenance": provenance, "address": address}, GATE_NODE) + assert reason is not None + assert fragment in reason + + +def test_gate_authored_packet_passes_violation_check() -> None: + body = _gate_packet() + assert _gate_ingress_violation(body, GATE_NODE) is None + + +def test_non_gate_packet_rejected_with_403() -> None: + async def spy(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + return {} + + register_handler("match", _with_packet_audit("match", spy)) + + client_packet = create_transport_packet( + action="match", + payload={"query": {}}, + tenant="plasticos", + source_node="client", + destination_node=NODE_NAME, + ).model_dump_json_dict() + + with TestClient(_app()) as client: + response = client.post("/v1/execute", json=client_packet) + + assert response.status_code == 403 + assert "gate-only ingress" in response.json()["detail"] + + +def test_gate_only_disabled_accepts_client_packet() -> None: + async def spy(tenant: str, payload: dict[str, Any]) -> dict[str, Any]: + return {} + + register_handler("match", _with_packet_audit("match", spy)) + + client_packet = create_transport_packet( + action="match", + payload={"query": {}}, + tenant="plasticos", + source_node="client", + destination_node=NODE_NAME, + ).model_dump_json_dict() + + with TestClient(_app(gate_only=False)) as client: + response = client.post("/v1/execute", json=client_packet) + + assert response.status_code == 200 + + +# ── Ingress surface (CONTRACT-01: single ingress) ──────────────────────────── + + +def test_core_ingress_routes_present() -> None: + """SDK chassis always exposes execute/health/metrics; newer SDKs may also mount relay.""" + paths = {route.path for route in _app().routes if hasattr(route, "path")} + assert {"/v1/execute", "/v1/health", "/metrics"} <= paths + + +# ── Readiness ─────────────────────────────────────────────────────────────── + + +def test_health_reports_not_ready_before_lifespan() -> None: + """The SDK health route is always HTTP 200 — readiness lives in `ready`.""" + response = TestClient(_app()).get("/v1/health") + assert response.status_code == 200 + assert response.json()["ready"] is False + + +# ── Preflight fails closed ────────────────────────────────────────────────── + + +def test_require_signature_without_key_is_rejected() -> None: + with pytest.raises(ValidationError, match="signing_key"): + _config(require_signature=True, signing_key=None) + + +def test_max_attachments_without_schemes_is_rejected() -> None: + with pytest.raises(ValidationError, match="attachment_allowed_schemes"): + _config(max_attachments=4, max_attachment_size_bytes=1024) + + +def test_sdk_default_attachment_caps_construct() -> None: + """Current constellation-node-sdk accepts default attachment/packet caps.""" + cfg = NodeRuntimeConfig( + environment="test", + node_name=NODE_NAME, + service_name=NODE_NAME, + service_version="1.1.0", + ) + assert cfg.max_packet_bytes > 0 + assert cfg.max_attachment_size_bytes >= 0 diff --git a/tools/auditors/api_regression.py b/tools/auditors/api_regression.py index fb09aeaa..52c35a63 100644 --- a/tools/auditors/api_regression.py +++ b/tools/auditors/api_regression.py @@ -150,7 +150,7 @@ def scan(self, files, repo_root, index=None, dep_indexes=None): message=f"Signature changed: {cn}.{mn}({', '.join(bm['args'])}) -> ({', '.join(cm['args'])})", file=rp, line=0, - fix_hint="Update METHODSIGNATURES.md + all callers", + fix_hint="Update METHOD_SIGNATURES.md + all callers", suggestions=[f"Old: {bm['args']}"], ) if bm["returns"] and cm["returns"] and bm["returns"] != cm["returns"]: diff --git a/tools/auditors/base.py b/tools/auditors/base.py index 585b12d7..f1e915f4 100644 --- a/tools/auditors/base.py +++ b/tools/auditors/base.py @@ -1,15 +1,15 @@ """ --- L9_META --- -l9_schema: 1 +l9_schema: 2 origin: l9-template engine: graph layer: [audit] -tags: [L9_TEMPLATE, auditors, base] -owner: platform +tags: [delivery, harness] status: active --- /L9_META --- -L9 BaseAuditor Protocol v2 — tiered execution, allowlists, two-phase scanning.""" +L9 BaseAuditor Protocol v2 — tiered execution, allowlists, two-phase scanning. +""" from __future__ import annotations diff --git a/tools/contract_report.py b/tools/contract_report.py index 54850d27..80fde4f8 100644 --- a/tools/contract_report.py +++ b/tools/contract_report.py @@ -61,8 +61,10 @@ def check_test_exists(contract: dict, repo_root: Path) -> dict[str, bool]: if not test_path: return result - # Check if the specific test file/dir exists - full_path = repo_root / test_path + # verification.test may be a pytest node ID ("file.py::TestClass"); the + # filesystem check only applies to the path portion. + file_part = test_path.split("::", 1)[0] + full_path = repo_root / file_part if full_path.exists(): if "contracts" in test_path: result["contract"] = True diff --git a/tools/contract_scanner.py b/tools/contract_scanner.py index f236edfd..59692959 100644 --- a/tools/contract_scanner.py +++ b/tools/contract_scanner.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 """ --- L9_META --- -l9_schema: 1 +l9_schema: 2 origin: l9-template engine: graph layer: [audit] -tags: [L9_TEMPLATE, audit, contracts] -owner: platform +tags: [delivery, harness] status: active --- /L9_META --- L9 Contract Violation Scanner -Encodes all 20 contracts as grep-able rules. +Encodes the grep-able subset of the 24 contracts as regex rules. Contracts without a +mechanical signature are verified by tests/contracts/, not here — see contracts/*.yaml +`verification.scanner_rules` for the authoritative rule-to-contract mapping. Exit code 1 = violations found = commit/merge blocked. """ @@ -190,7 +191,7 @@ def _rule( "CRITICAL", r"httpx\.(post|get|put|delete|patch)\s*\(", "Raw HTTP to another node - use delegate_to_node()", - "from l9.core.delegation import delegate_to_node", + "from engine.packet.chassis_contract import delegate_to_node", include_dirs=["engine/"], ), _rule( @@ -199,7 +200,7 @@ def _rule( "CRITICAL", r"requests\.(post|get|put|delete|patch)\s*\(", "Raw HTTP via requests - use delegate_to_node()", - "from l9.core.delegation import delegate_to_node", + "from engine.packet.chassis_contract import delegate_to_node", include_dirs=["engine/"], ), # -- CONTRACT 19: MEMORY_SUBSTRATE_ACCESS.md -- @@ -209,7 +210,7 @@ def _rule( "CRITICAL", r"INSERT\s+INTO\s+packetstore", "Direct write to packetstore - use ingest_packet()", - "from l9.memory.ingestion import ingest_packet", + "Persist via engine.packet.packet_store, or delegate to the memory substrate node", include_dirs=["engine/"], ), _rule( @@ -227,8 +228,8 @@ def _rule( "SHARED_MODELS.md", "HIGH", r"class\s+PacketEnvelope\s*\(", - "Redefining PacketEnvelope - import from l9.core", - "from l9.core.envelope import PacketEnvelope", + "Redefining PacketEnvelope - import the shared model", + "from engine.packet.packet_envelope import PacketEnvelope", include_dirs=["engine/"], exclude_dirs=["engine/packet/packet_envelope.py"], # canonical envelope in this repo ), @@ -237,8 +238,8 @@ def _rule( "SHARED_MODELS.md", "HIGH", r"class\s+TenantContext\s*\(", - "Redefining TenantContext - import from l9.core", - "from l9.core.envelope import TenantContext", + "Redefining TenantContext - import the shared model", + "from engine.packet.packet_envelope import TenantContext", include_dirs=["engine/"], exclude_dirs=["engine/packet/packet_envelope.py"], # canonical envelope in this repo ), @@ -247,8 +248,8 @@ def _rule( "SHARED_MODELS.md", "HIGH", r"class\s+ExecuteRequest\s*\(", - "Redefining ExecuteRequest - import from l9.core", - "from l9.core.contract import ExecuteRequest", + "Redefining ExecuteRequest - the chassis owns this model", + "from chassis.chassis_app import ExecuteRequest", include_dirs=["engine/"], ), # -- CONTRACT 18: OBSERVABILITY.md -- @@ -290,7 +291,37 @@ def _rule( "Check PACKET_TYPE_REGISTRY.md", exclude_dirs=["agents/cursor/"], ), - # -- CONTRACT 17: ENV_VARS.md -- + # -- CONTRACT 17: zero-stub protocol (TEST_PATTERNS.md / BANNED_PATTERNS.md) -- + # Scoped to engine/: abstract-base-class methods in chassis/ legitimately raise + # NotImplementedError as their contract. + _rule( + "STUB-001", + "BANNED_PATTERNS.md", + "CRITICAL", + r"raise\s+NotImplementedError", + "Stub in engine/ - unimplemented code path", + "Ship the implementation or record the gap in DEFERRED.md", + include_dirs=["engine/"], + ), + _rule( + "STUB-002", + "BANNED_PATTERNS.md", + "HIGH", + r"#\s*TODO\b", + "TODO comment in engine/ - deferred work must be tracked, not inlined", + "Implement it now or add an entry to DEFERRED.md and drop the comment", + include_dirs=["engine/"], + ), + _rule( + "STUB-003", + "BANNED_PATTERNS.md", + "HIGH", + r"#\s*(?:PLACEHOLDER|FIXME|XXX)\b", + "PLACEHOLDER/FIXME comment in engine/ - deferred work must be tracked, not inlined", + "Implement it now or add an entry to DEFERRED.md and drop the comment", + include_dirs=["engine/"], + ), + # -- CONTRACT 05: ENV_VARS.md -- _rule( "ENV-001", "ENV_VARS.md", diff --git a/tools/deploy/deploy.sh b/tools/deploy/deploy.sh deleted file mode 100755 index 35bd23ac..00000000 --- a/tools/deploy/deploy.sh +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env bash -# --- L9_META --- -# l9_schema: 1 -# origin: l9-template -# engine: graph -# layer: [scripts, deploy] -# tags: [L9_TEMPLATE, deploy] -# owner: platform -# status: active -# --- /L9_META --- -# ============================================================================= -# VPS Deploy (Repo-Agnostic) -# -# GitHub SSOT + Env Sync + Selective Rebuild + Health Validation -# -# Behavior: -# LOCAL: stages + commits + pushes current branch to origin -# VPS: git hard reset to origin/$BRANCH, env sync, docker rebuild -# HEALTH: runs deep_mri.sh; optionally runs e2e smoke test -# -# ALL configuration pulled from .env.vps (or environment variables). -# You never edit this script — you edit your .env.vps. -# -# Required .env.vps vars: -# VPS_HOST — SSH hostname of your VPS -# VPS_REPO — Remote repo path (e.g. /opt/myapp) -# COMPOSE_PROD — Production compose overlay (e.g. docker-compose.prod.yml) -# CORE_SERVICES — Space-separated core services for --core flag -# APP_API_KEY — API key (used by health scripts on VPS) -# -# Optional .env.vps vars: -# DEPLOY_BRANCH — Required branch (default: main) -# ALLOW_NON_MAIN — Allow deploy from non-main (default: false) -# ALLOW_DOCKER_PRUNE — Allow docker prune (default: false) -# HEALTH_SCRIPT — Path to health script (default: scripts/deployment/deep_mri.sh) -# E2E_SCRIPT — Path to E2E script (default: scripts/e2e_test.sh) -# APP_NAME — Project name for display (default: APP) -# -# Usage: ./tools/deploy/deploy.sh [flags] -# ============================================================================= - -set -euo pipefail - -# --------------------------------------------------------------------------- -# Load config from .env.vps -# --------------------------------------------------------------------------- - -MAC_REPO="$(git rev-parse --show-toplevel 2>/dev/null || true)" -[[ -n "$MAC_REPO" ]] || { echo "❌ Run this from inside a git repo."; exit 1; } -cd "$MAC_REPO" - -ENV_VPS="$MAC_REPO/.env.vps" -if [[ -f "$ENV_VPS" ]]; then - set -a - # shellcheck source=/dev/null - source "$ENV_VPS" - set +a -fi - -# --------------------------------------------------------------------------- -# All config from env vars — fail fast on missing required ones -# --------------------------------------------------------------------------- - -VPS_HOST="${VPS_HOST:?Set VPS_HOST in .env.vps}" -VPS_REPO="${VPS_REPO:?Set VPS_REPO in .env.vps}" -COMPOSE_BASE="${COMPOSE_BASE:-docker-compose.yml}" -COMPOSE_PROD="${COMPOSE_PROD:?Set COMPOSE_PROD in .env.vps}" -CORE_SERVICES="${CORE_SERVICES:?Set CORE_SERVICES in .env.vps (space-separated)}" -DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}" -ALLOW_NON_MAIN="${ALLOW_NON_MAIN:-false}" -ALLOW_DOCKER_PRUNE="${ALLOW_DOCKER_PRUNE:-false}" -HEALTH_SCRIPT="${HEALTH_SCRIPT:-scripts/deployment/deep_mri.sh}" -E2E_SCRIPT="${E2E_SCRIPT:-scripts/e2e_test.sh}" -APP_NAME="${APP_NAME:-APP}" -ENV_EXAMPLE="${ENV_EXAMPLE:-.env.example}" -ENV_VPS_TEMPLATE="${ENV_VPS_TEMPLATE:-.env.vps.template}" -REMOTE_ENV_FILE="${REMOTE_ENV_FILE:-.env}" - -SSH_OPTS="-o BatchMode=yes -o StrictHostKeyChecking=accept-new" - -# Runtime flags -BRANCH="${BRANCH:-$DEPLOY_BRANCH}" -NO_CACHE=false -PRUNE_DOCKER=false -SYNC_ENV=true -DRY_RUN=false -NO_REBUILD=false -RUN_GODMODE=false -SERVICES="" -COMMIT_MSG="deploy: $(date +'%Y-%m-%d %H:%M:%S')" - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -run() { - if $DRY_RUN; then echo "DRY: $*"; else eval "$@"; fi - return 0 -} -die() { echo "❌ $*" 1>&2; exit 1; } - -usage() { -cat << EOF -Usage: ./tools/deploy/deploy.sh [flags] - -Flags: - --msg "" Commit message - --no-cache Rebuild images without cache - --prune-docker docker system prune -af (NO volumes, gated by ALLOW_DOCKER_PRUNE) - --no-sync-env Do not sync .env.vps → VPS - --no-rebuild Skip container rebuild (git pull + env sync only) - --services "a b" Rebuild ONLY specified services - --core Rebuild ONLY core services (\$CORE_SERVICES) - --godmode Run E2E smoke test after deployment - --dry-run Print commands without executing - -h, --help Help - -Examples: - ./tools/deploy/deploy.sh --msg "full deploy" - ./tools/deploy/deploy.sh --no-rebuild - ./tools/deploy/deploy.sh --services "api postgres" - ./tools/deploy/deploy.sh --core - ./tools/deploy/deploy.sh --msg "critical hotfix" --godmode -EOF - return 0 -} - -# --------------------------------------------------------------------------- -# .env.vps.template management -# --------------------------------------------------------------------------- - -ensure_gitignore_allows_env_template() { - [[ -f ".gitignore" ]] || return 0 - if grep -qE '^\s*\.env\.\*' .gitignore && ! grep -qE "^\s*!${ENV_VPS_TEMPLATE}" .gitignore; then - echo " + Patching .gitignore to allow tracking ${ENV_VPS_TEMPLATE}" - printf '\n# Allow committing env template (placeholders only)\n!%s\n' "$ENV_VPS_TEMPLATE" >> .gitignore - fi - return 0 -} - -patch_env_template_from_example() { - local example_path="$MAC_REPO/$ENV_EXAMPLE" - local template_path="$MAC_REPO/$ENV_VPS_TEMPLATE" - [[ -f "$example_path" ]] || { echo " = No $ENV_EXAMPLE found, skipping template patch"; return 0; } - - local tmp_out - tmp_out="$(mktemp)" - - while IFS= read -r line; do - if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then - echo "$line" >> "$tmp_out"; continue - fi - if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)= ]]; then - local key="${BASH_REMATCH[1]}" - if [[ -f "$template_path" ]]; then - local existing - existing="$(grep -E "^${key}=" "$template_path" 2>/dev/null | head -1 || true)" - echo "${existing:-${key}=}" >> "$tmp_out" - else - echo "${key}=" >> "$tmp_out" - fi - else - echo "$line" >> "$tmp_out" - fi - done < "$example_path" - - if [[ ! -f "$template_path" ]] || ! cmp -s "$tmp_out" "$template_path"; then - mv "$tmp_out" "$template_path" - echo " + Patched $ENV_VPS_TEMPLATE from $ENV_EXAMPLE" - else - rm -f "$tmp_out" - echo " = $ENV_VPS_TEMPLATE already up-to-date" - fi -} - -# --------------------------------------------------------------------------- -# Deployment functions -# --------------------------------------------------------------------------- - -sync_env_to_server() { - $SYNC_ENV || { echo " = Env sync disabled"; return 0; } - [[ -f "$ENV_VPS" ]] || die "Missing .env.vps — create it with real values." - - local remote_env="$VPS_REPO/$REMOTE_ENV_FILE" - echo "[ENV] Syncing .env.vps → $VPS_HOST:$remote_env" - - local stamp - stamp="$(date +%Y%m%d_%H%M%S)" - - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && (test -f '$remote_env' && cp -a '$remote_env' '${remote_env}.bak.${stamp}' || true)" - - if $DRY_RUN; then - echo "DRY: streaming .env.vps → $VPS_HOST:$remote_env" - else - ssh $SSH_OPTS "$VPS_HOST" "cat > '$remote_env' && chmod 600 '$remote_env'" < "$ENV_VPS" - fi - - if ! $DRY_RUN; then - local local_hash remote_hash - local_hash="$(shasum -a 256 "$ENV_VPS" | awk '{print $1}')" - remote_hash="$(ssh $SSH_OPTS "$VPS_HOST" "shasum -a 256 '$remote_env'" | awk '{print $1}')" - [[ "$local_hash" == "$remote_hash" ]] || die "Env sync mismatch (local $local_hash != remote $remote_hash)" - echo " ✅ Env synced (sha256 match)" - fi -} - -remote_git_hard_reset() { - echo "[VPS] Hard reset to origin/$BRANCH (SSOT)" - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && git fetch origin '$BRANCH' && git reset --hard 'origin/$BRANCH' && git clean -fd" - return 0 -} - -remote_rebuild_stack() { - local build_opts="" - $NO_CACHE && build_opts="--no-cache" - - if [[ -n "$SERVICES" ]]; then - echo "[VPS] Selective rebuild: $SERVICES (no-cache=$NO_CACHE)" - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && docker compose -f $COMPOSE_BASE -f $COMPOSE_PROD stop $SERVICES" - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && docker compose -f $COMPOSE_BASE -f $COMPOSE_PROD build $build_opts $SERVICES" - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && docker compose -f $COMPOSE_BASE -f $COMPOSE_PROD up -d --force-recreate $SERVICES" - else - echo "[VPS] Full rebuild (all services) no-cache=$NO_CACHE" - [[ "$RUN_GODMODE" == "false" ]] && { echo "[VPS] Full rebuild → GOD MODE enabled automatically"; RUN_GODMODE=true; } - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && docker compose -f $COMPOSE_BASE -f $COMPOSE_PROD down --remove-orphans" - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && docker compose -f $COMPOSE_BASE -f $COMPOSE_PROD build $build_opts" - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && docker compose -f $COMPOSE_BASE -f $COMPOSE_PROD up -d --force-recreate --remove-orphans" - fi - - if $PRUNE_DOCKER; then - if [[ "$ALLOW_DOCKER_PRUNE" == "true" ]]; then - echo "[VPS] Prune docker (no volumes)" - ssh $SSH_OPTS "$VPS_HOST" "docker system prune -af" - else - echo "[VPS] --prune-docker requested but ALLOW_DOCKER_PRUNE!=true, skipping." - fi - fi - return 0 -} - -remote_health() { - echo "" - echo "┌─────────────────────────────────────────────────────────────┐" - echo "│ HEALTH VALIDATION │" - echo "└─────────────────────────────────────────────────────────────┘" - echo "" - - echo "⏳ Waiting for services to initialize (15s)..." - sleep 15 - - echo "" - echo "═══ PHASE 1: Deep MRI ($HEALTH_SCRIPT) ═══" - echo "" - - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && chmod +x '$HEALTH_SCRIPT' && './$HEALTH_SCRIPT'" - local mri_exit=$? - [[ $mri_exit -eq 0 ]] && echo "✅ Deep MRI passed" || echo "⚠️ Deep MRI completed with warnings (exit $mri_exit)" - - if $RUN_GODMODE; then - echo "" - echo "═══ PHASE 2: E2E Smoke ($E2E_SCRIPT) ═══" - echo "" - - ssh $SSH_OPTS "$VPS_HOST" "cd '$VPS_REPO' && chmod +x '$E2E_SCRIPT' && './$E2E_SCRIPT' smoke" - local e2e_exit=$? - [[ $e2e_exit -eq 0 ]] && echo "✅ E2E validation PASSED" || echo "❌ E2E validation FAILED (exit $e2e_exit)" - else - echo "" - echo "ℹ️ GOD MODE skipped (use --godmode for E2E validation)" - fi - return 0 -} - -# --------------------------------------------------------------------------- -# Parse flags -# --------------------------------------------------------------------------- - -while [[ $# -gt 0 ]]; do - case "$1" in - --msg) COMMIT_MSG="$2"; shift 2 ;; - --no-cache) NO_CACHE=true; shift ;; - --prune-docker) PRUNE_DOCKER=true; shift ;; - --no-sync-env) SYNC_ENV=false; shift ;; - --no-rebuild) NO_REBUILD=true; shift ;; - --services) SERVICES="$2"; shift 2 ;; - --core) SERVICES="$CORE_SERVICES"; shift ;; - --godmode) RUN_GODMODE=true; shift ;; - --dry-run) DRY_RUN=true; shift ;; - -h|--help) usage; exit 0 ;; - *) die "Unknown flag: $1" ;; - esac -done - -# --------------------------------------------------------------------------- -# MAIN -# --------------------------------------------------------------------------- - -echo "╔═══════════════════════════════════════════════════════════════╗" -echo "║ ${APP_NAME} Deploy (GitHub SSOT + Selective + Health)" -echo "╚═══════════════════════════════════════════════════════════════╝" -echo "" -echo "[LOCAL] Repo: $MAC_REPO" -echo "[LOCAL] Branch: $(git rev-parse --abbrev-ref HEAD)" -echo "[LOCAL] Commit: $(git rev-parse --short HEAD)" -echo "[VPS] Host: $VPS_HOST" -echo "[VPS] Repo: $VPS_REPO" - -if [[ -n "$SERVICES" ]]; then - echo "[MODE] Selective rebuild: $SERVICES" -elif $NO_REBUILD; then - echo "[MODE] No rebuild (git pull + env sync only)" -else - echo "[MODE] Full rebuild (all containers)" -fi - -$RUN_GODMODE && echo "[HEALTH] Deep MRI + E2E" || echo "[HEALTH] Deep MRI only" -echo "" - -# Branch safety -current_branch="$(git rev-parse --abbrev-ref HEAD)" -if [[ "$current_branch" != "$DEPLOY_BRANCH" && "$ALLOW_NON_MAIN" != "true" ]]; then - die "Refusing deploy from '$current_branch'. Expected '$DEPLOY_BRANCH' or ALLOW_NON_MAIN=true." -fi - -echo "[LOCAL] Git status:" -git status --porcelain || true -echo "" - -# 0) Template management -ensure_gitignore_allows_env_template -patch_env_template_from_example - -# 1) Stage + commit + push -git add -A -if git diff --cached --quiet; then - echo " = Nothing staged; skipping commit/push" -else - git commit --no-verify -m "${COMMIT_MSG}" - git push --no-verify origin HEAD -fi - -# 2) VPS: hard reset, sync env, rebuild -remote_git_hard_reset -sync_env_to_server - -if $NO_REBUILD; then - echo "[VPS] Skipping container rebuild (--no-rebuild)" -else - remote_rebuild_stack -fi - -# 3) Health validation -remote_health - -echo "" -echo "✅ Done." diff --git a/tools/l9_template_manifest.yaml b/tools/l9_template_manifest.yaml index 76c999a4..8ec47a7f 100644 --- a/tools/l9_template_manifest.yaml +++ b/tools/l9_template_manifest.yaml @@ -114,9 +114,6 @@ files: tags: ["L9_TEMPLATE", "pr-review"] # --- Config / bootstrap --- - - path: ".suite6-config.json" - required: false - tags: ["L9_TEMPLATE", "config", "suite6"] - path: "setup-new-workspace.yaml" required: false tags: ["L9_TEMPLATE", "bootstrap", "workspace"] diff --git a/tools/spec_extract.py b/tools/spec_extract.py index 977becec..383160d4 100644 --- a/tools/spec_extract.py +++ b/tools/spec_extract.py @@ -48,6 +48,8 @@ L9_TEMPLATE_TAG = "L9_TEMPLATE" +RESEARCH_DIR = "tools/research" +RESEARCH_PATTERNS_FILE = "top5_leverage_patterns_detailed.json" RESEARCH_DIR = "tools/research" RESEARCH_PATTERNS_FILE = "top5_leverage_patterns_detailed.json" @@ -390,7 +392,6 @@ def extract_research_features(root: Path) -> list[SpecFeature]: for key, pattern in data.items(): if key.startswith("_") or not isinstance(pattern, dict): continue - mapping = pattern.get("engine_mapping") tokens = mapping.get("search_tokens") if isinstance(mapping, dict) else None if not tokens: @@ -573,6 +574,7 @@ def main() -> int: features += extract_v11_additions(spec) features += extract_action_features(spec) features += extract_gds_features(spec) + features += extract_research_features(root) research = extract_research_features(root) features += research diff --git a/tools/verify_contracts.py b/tools/verify_contracts.py index 2dc6a490..d4ff2a5d 100644 --- a/tools/verify_contracts.py +++ b/tools/verify_contracts.py @@ -1,17 +1,24 @@ #!/usr/bin/env python3 """ --- L9_META --- -l9_schema: 1 +l9_schema: 2 origin: l9-template engine: graph layer: [audit] -tags: [L9_TEMPLATE, audit, verify] -owner: platform +tags: [delivery, harness] status: active --- /L9_META --- L9 Contract Files Existence + Wiring Check -Verifies all 20 contract files exist AND are referenced in .cursorrules and CLAUDE.md. + +Two layered passes: + +1. REQUIRED_CONTRACTS -- a literal ratchet floor. Docs on this list must exist and be + referenced from an agent file. This list only ever grows. +2. contracts/*.yaml `docs:` pointers -- every doc a machine-readable contract claims to + be described by must also exist and be wired. This catches docs added to the YAML + registry without being added to the floor. + Exit code 1 = missing file or unwired -> blocks CI/merge. """ @@ -20,6 +27,8 @@ import sys from pathlib import Path +import yaml + REQUIRED_CONTRACTS = [ "docs/contracts/FIELD_NAMES.md", "docs/contracts/METHOD_SIGNATURES.md", @@ -41,16 +50,44 @@ "docs/contracts/OBSERVABILITY.md", "docs/contracts/MEMORY_SUBSTRATE_ACCESS.md", "docs/contracts/SHARED_MODELS.md", + "docs/contracts/PROHIBITED_FACTORS.md", + "docs/contracts/PII_HANDLING.md", + "docs/contracts/BIDIRECTIONAL_MATCHING.md", + "docs/contracts/L9_META_HEADERS.md", + "docs/contracts/KGE_EMBEDDINGS.md", + "docs/contracts/FEATURE_FLAG_DISCIPLINE.md", + "docs/contracts/SCORING_WEIGHT_CEILING.md", ] -AGENT_FILES = [".cursorrules", "CLAUDE.md"] +AGENT_FILES = [".cursorrules", "CLAUDE.md", "AGENTS.md"] + +CONTRACTS_DIR = "contracts" + + +def yaml_declared_docs(root: Path, errors: list[str]) -> list[str]: + """Collect every `docs:` pointer declared across contracts/*.yaml.""" + declared: list[str] = [] + for path in sorted((root / CONTRACTS_DIR).glob("contract_*.yaml")): + try: + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError) as e: + errors.append(f"Cannot parse {path.name}: {e}") + continue + for doc in data.get("docs") or []: + if doc not in declared: + declared.append(doc) + return declared def main() -> int: root = Path.cwd() errors: list[str] = [] - for rel in REQUIRED_CONTRACTS: + declared = yaml_declared_docs(root, errors) + # The literal list is the ratchet floor; YAML pointers layer on top of it. + to_check = REQUIRED_CONTRACTS + [d for d in declared if d not in REQUIRED_CONTRACTS] + + for rel in to_check: path = root / rel if not path.is_file(): errors.append(f"Missing contract file: {rel}") @@ -67,13 +104,15 @@ def main() -> int: except OSError as e: errors.append(f"Cannot read {agent_file}: {e}") - for rel in REQUIRED_CONTRACTS: + for rel in to_check: name = Path(rel).name if not any(name in c or rel in c for _f, c in agent_contents): errors.append(f"No agent file references contract: {name}") if not errors: - print("L9 contract files: all 20 present and wired.") + extra = len(to_check) - len(REQUIRED_CONTRACTS) + suffix = f" (+{extra} from contracts/*.yaml)" if extra else "" + print(f"L9 contract files: all {len(to_check)} present and wired{suffix}.") return 0 print("L9 contract verification failed:\n", file=sys.stderr)