Skip to content

Engineering Principles (Standing Order)

Index and authority: .github/copilot-instructions.md, section Building: Engineering Principles.

SOLID Principles

  • (S) Single Responsibility: One reason to change per class. No god objects. Extract focused modules when classes exceed ~500 lines or ~15 methods.
  • (O) Open/Closed: Extend via public APIs, not private member patching. Never access obj._private_attr from outside the owning class. If you need to modify behavior, define a public method on the target.
  • (L) Liskov Substitution: Subtypes must honor base contracts. If BaseAgent defines act(), every subclass must implement it compatibly.
  • (I) Interface Segregation: Depend on narrow typing.Protocol interfaces, not entire classes. Consumers should only see the methods they use.
  • (D) Dependency Inversion: Constructor injection. Depend on abstractions (protocols), not concretions. New services should accept dependencies as parameters, not import and instantiate them internally.

Additional Engineering Principles

  • Law of Demeter: Don't reach through objects. No a.b._c chains. If wiring is needed, define a public API on the target.
  • Fail Fast: Default to log-and-degrade. Three tiers for exception handling:
Tier When Pattern
Swallow Non-critical, no user impact except: pass (rare, must justify)
Log-and-degrade Visible degradation acceptable except: logger.warning(...); return fallback
Propagate Security, data integrity, safety except: logger.error(...); raise
  • Defense in Depth: Validate at every boundary. Input sanitization at API AND service layers. Never assume the caller already checked.
  • DRY: Search for existing implementations before writing new ones. If same logic exists in 2+ places, extract it.
  • Cloud-Ready Storage: New database modules must use an abstract connection interface (typing.Protocol), not direct aiosqlite.connect(). This enables the commercial overlay to swap storage backends (SQLite → Postgres) without changing business logic.

Coding Standards

  • Follow existing patterns. Check how similar things are already done before inventing new approaches.
  • New agents must follow the perceive -> decide -> act -> report lifecycle. CognitiveAgent subclasses use instructions-first design.
  • Destructive intents must set requires_consensus=True in their IntentDescriptor.
  • Store raw trust parameters (alpha, beta), never derived mean scores. Derived scores lose the full Beta distribution information.
  • Restored designed agent code must pass CodeValidator validation before importlib loading. No exceptions on warm boot.

Testing Standards

  • Framework: pytest + pytest-asyncio. Prefer _Fake* stub classes over complex mock chains. Test files mirror source paths.
  • Test gates: The Builder runs focused tests for the changed slice and its immediate consumers after each milestone, with the focused command below; the orchestrator's gates are in .github/agents/orchestrator.agent.md (Gate, Batch and Push).
  • Mutation testing is targeted, not routine (Captain, 2026-08-25): use it when acceptance criteria demand it, when scoped review raises a test-validity concern ordinary regression tests cannot settle, or for Critical/High-risk behaviour. Do not run routine mutation campaigns.
  • Run the unmutated baseline FIRST and abort if it is already red, or every mutant looks killed.
  • Mutate in place with a .mutbak sibling and restore in finally. A copied tree is inert under an editable install.
  • Single-line anchors only — this is a CRLF tree and multi-line anchors silently match nothing. An anchor that is not found is an INERT mutant, not a killed one; say so.
  • Classify a timeout banner as INVALID, never SURVIVED: a run that never completed proves nothing.
  • A surviving mutant may mean the MUTANT is wrong, not the test. Measured twice this session: a mutant that dropped a <redacted>@ marker rather than the stripping itself, and one whose subclass was rejected by an earlier gate so the bound under test was never reached. Before concluding a test is weak, check that the mutant actually reaches the behaviour it claims to break.
  • One campaign per candidate. When a build contract pins byte-exact files and carries its author's executed mutation table for them, a Builder whose base tree and file hashes match does not repeat the campaign; it reruns only the mutants whose files differ, or those a review's test-validity concern names. Measured 2026-10-05 (#1135 slice 2a): repeating it on byte-identical files cost the Builder about 50 minutes across two rounds.
  • Run focused tests with: d:/ProbOS/.venv/Scripts/pytest.exe <test-files-or-node-ids> -q -n 0 -p no:randomly
  • Coverage rule: All new public methods and branches must have tests. Target 100% coverage on new code — gaps require justification.
  • Test structure: Follow Arrange-Act-Assert. Each test should verify one behavior. Name tests descriptively: test_{method}_{scenario}_{expected} (e.g., test_get_template_missing_key_returns_none).
  • Boundary testing: Every public method must test at minimum: (1) happy path, (2) error/edge case, (3) empty/None input where applicable.
  • Test isolation: Tests must not depend on execution order. No shared mutable state between tests. Each test creates its own fixtures. If a test fails when run alone, it's broken.
  • No test pollution: Tests must clean up any resources they create (temp files, background tasks, DB entries). Use tmp_path fixture for files, try/finally or context managers for cleanup.
  • API test requirement: Every new API endpoint (api.py) must have at least 3 tests — happy path, error case, and input validation. Test in tests/test_distribution.py or tests/test_hxi_chat_integration.py.
  • UI test requirement: Every UI change (TypeScript/React) must include a Vitest component test. The HXI has broken from untested UI changes multiple times — tooltips, bloom position, chat rendering. No UI PR without tests.
  • UI tests run with: cd ui && npx vitest run (when Vitest is set up)

Type Annotation Standards

  • Public API rule: All public methods and properties must have full type annotations (parameters + return type). No exceptions.
  • Protocol compliance: When a class structurally implements a typing.Protocol, its method signatures must match exactly — annotate to prove it.
  • Use modern Python typing: X | None over Optional[X], list[str] over List[str] (Python 3.10+ syntax). Use from __future__ import annotations in modules with complex types.
  • Internal methods: Type annotations recommended but not required on private (_method) internals.

Logging Standards

  • Structured context: Every log message must include what failed, why it matters, and what happens next. Bad: logger.warning("error"). Good: logger.warning("Template %s not found in spawner; falling back to default", template_name).
  • Log levels:
Level When
debug Internal state useful during development only
info System lifecycle events (startup, shutdown, agent spawned, pool created)
warning Degraded operation — something failed but the system compensated
error Operation failed, user impact, requires investigation
exception Same as error but with traceback — use inside except blocks
  • No bare print(): Use logger for all operational output. print() is only for CLI/shell user-facing output.
  • No sensitive data in logs: Never log API keys, tokens, full file contents, or user credentials.

Async Discipline

  • Always use asyncio.get_running_loop(), never get_event_loop().
  • Task references: Always hold a reference to tasks created with asyncio.create_task(). Fire-and-forget tasks silently swallow exceptions and can be garbage collected. Store in a set or instance variable and remove on completion.
  • Never use asyncio.ensure_future() — use asyncio.create_task(). ensure_future is ambiguous about whether it receives a coroutine or future.
  • Cancellation handling: Long-running async methods should catch asyncio.CancelledError, perform cleanup, and re-raise. Never swallow cancellation.
  • Async context cleanup: Use async with for resources that need async teardown. If a class has async def start(), it must have async def stop() with corresponding cleanup.

Import & Module Standards

  • Layer discipline: Lower layers must not import from higher layers (Substrate cannot import from Cognitive). Cross-cutting modules (federation/, knowledge/, runtime.py) may import from any layer.
  • Circular import prevention: Use TYPE_CHECKING guard for type-only imports that would create cycles:
    from __future__ import annotations
    from typing import TYPE_CHECKING
    if TYPE_CHECKING:
        from probos.runtime import ProbOSRuntime
    
  • Import order: stdlib → third-party → local, separated by blank lines. Tools like isort conventions.
  • No wildcard imports: Never use from module import *. Always import specific names.

Configuration Standards

  • Pydantic models only: New configuration must be added to the existing Pydantic models in config.py. Never read config from raw dicts, environment variables, or ad-hoc YAML parsing.
  • Defaults required: Every config field must have a sensible default so ProbOS runs out of the box with zero configuration.
  • Validation at parse time: Use Pydantic validators. Invalid config should fail at startup, not at runtime when the feature is first used.