Metadata-Version: 2.4
Name: plm-engine-core
Version: 1.0.0
Summary: TracePulse PLM Engine Core — control_plane (governance, autonomy, HITL, run lifecycle, trace propagation) + agent_runtime (selector, dispatcher, retry, escalation). US-CR.1 scaffold; behaviour lands in US-CR.0+. Internal one-way import contract: control_plane MUST NOT import from agent_runtime (mechanical enforcement in US-CR.2).
Author: TracePulse
License: Proprietary
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: plm-shared<2.0,>=1.0.0
Requires-Dist: PyJWT<3,>=2.8
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: fastapi; extra == "test"
Requires-Dist: httpx; extra == "test"
Requires-Dist: import-linter<3,>=2.0; extra == "test"

# plm-engine-core

TracePulse PLM Engine Core package. Lives next to `plm-shared` in the
monorepo; shipped editable for Wave 1 (`pip install -e ../plm-engine-core`),
internal PyPI publication deferred to Wave 5.

US-CR.1 ships this package as **empty scaffolding**. Every other
US-CR.* story in FTR-607 lands its implementation inside the two
sub-packages declared here, on top of contracts published by
`plm-shared` (US-W1.0).

## Internal layout

| Sub-package      | Owns                                                           | Stories landing here                          |
|------------------|----------------------------------------------------------------|-----------------------------------------------|
| `control_plane/` | governance, autonomy, HITL, run lifecycle, trace propagation   | CR.0, CR.3, CR.4, CR.5, CR.6, CR.10, CR.13    |
| `agent_runtime/` | selector, dispatcher, retry, escalation                        | CR.10, CR.11                                  |

The split implements target architecture invariant **#5** — *execution
separated from expertise* — and decision **D-LOCKED-13**. The physical
repo split is deferred to V1.1; Wave 1 delivers the **logical**
boundary.

## One-way import contract

```
control_plane  ─────►  plm_shared.*       (frozen contracts)
agent_runtime  ─────►  plm_shared.*       (frozen contracts)
agent_runtime  ─────►  control_plane      (asks for verdicts)
control_plane  ──╳──►  agent_runtime      (FORBIDDEN)
plm_engine_core ─╳──►  plm_accelerators   (FORBIDDEN)
```

`control_plane` decides *what is allowed*; `agent_runtime` asks
*how do I run this*. Reversing the dependency (control_plane reading
runtime state) couples policy to execution and breaks the V1.1 repo
split. **plm-engine-core MUST NOT import from the Workbench /
Accelerators** package; reversing this absorption would dissolve
the platform / product-line boundary (anti-pattern #8).

**Mechanical enforcement (US-CR.2 / Conv F sub-story 3):** two
`import-linter` contracts in `pyproject.toml` fail CI on any
violation:

| Contract                                                  | Source                       | Forbidden                       |
|-----------------------------------------------------------|------------------------------|---------------------------------|
| `control_plane-must-not-depend-on-agent_runtime`          | `plm_engine_core.control_plane` | `plm_engine_core.agent_runtime` |
| `plm_engine_core-must-not-depend-on-workbench`            | `plm_engine_core`            | `plm_accelerators`              |

`if TYPE_CHECKING:` imports across the boundary are **blocked by
default** (CR.2 AC-6) — TYPE_CHECKING leakage is the most common
way the boundary erodes silently. `tests/` lives outside the
package and is naturally excluded.

The CR.1-era by-convention guard in `tests/test_imports.py` STAYS
alongside the mechanical contract. Both fire if either rule trips
— belt-and-braces protection if the linter contract has to be
relaxed for a transient reason.

### Negative-fixture toggling test

A permanent sandbox lives at
`plm_engine_core/control_plane/_lint_fixtures/violation_demo.py`.
The `tests/test_import_linter.py::test_negative_fixture_trips_*`
tests (gated by `RUN_LINTER_NEGATIVE=1`) materialise a temporary
sibling file with an offending import, run `lint-imports`, assert
the contract trips with the right name + file:line, then delete
the temp file and verify the clean state passes again. Run with:

```bash
RUN_LINTER_NEGATIVE=1 pytest tests/test_import_linter.py -v
```

### Exemption process

Genuine exceptions go through an ADR signed off by the
architecture team. ADRs live under
[`02_App/plm-engine-core/docs/adr/`](docs/adr/). Each ADR records
the contract relaxed, the scope of the relaxation
(typically a specific `ignore_imports` entry on one contract),
the rationale, and the cross-link to D-LOCKED-13 + invariant #5.
The contract / `ignore_imports` edit MUST cite the ADR file path
in a comment so a future reader finds the authority for the
exemption.

### Known limitations

- **Transitive imports via `plm-shared` are not caught** (CR.2 §9 +
  Edge cases). A control_plane file importing a plm-shared helper
  that itself transitively imports agent_runtime is invisible to
  the contract. Intentional — limits blast radius of a single
  PR's contract scope.
- **Dynamic imports (`importlib`, string-based) are not caught**.
  Static analysis only.
- **The contracts are scoped to plm-engine-core**. Cross-product-
  line contracts (e.g. `agent_runtime → connectors`,
  `agent_runtime → workbench`) land with Epic 6 / Epic 8.

## `with_system_identity` allowlist (CR.2 Decision #24)

`plm_engine_core.control_plane.identity.with_system_identity` is a
context manager that binds an `actor_kind="system"` identity for
in-process callers without an inbound JWT (cron, BackgroundTask,
GC). It bypasses JWT validation by design and MUST NOT be invoked
from arbitrary call sites.

The path-based BL6 guardrail in
[`02_App/backend/scripts/architecture_guardrails.py`](../backend/scripts/architecture_guardrails.py)
restricts the importer set. Initial allowlist:

- `plm_engine_core/control_plane/identity/system_identity.py` — defining module.
- `plm_engine_core/control_plane/identity/__init__.py` — re-export site.
- `plm_engine_core/cli/` — `plm-cli` future subcommands may bind a
  system identity for offline operations.
- `tests/` — fixtures may import freely.

Adding a new module to the allowlist requires an ADR.

## Install (developer)

```bash
cd 02_App/backend
pip install -e ../plm-engine-core
```

The editable install puts `plm_engine_core.*` on the sys.path of the
backend venv. Like `plm-shared`, **`pip install` MUST run from
`02_App/backend/`** because pip resolves `-e ../plm-engine-core`
relative to the invocation CWD.

## Tests

```bash
cd 02_App/plm-engine-core
python -m pytest tests/ -v
```

The smoke test verifies:
- Both sub-packages import cleanly.
- `__all__` placeholders match the documented public surface.
- No `control_plane` module imports from `agent_runtime` (one-way
  contract guard, AC-10).
- No business logic has snuck in beyond `__init__.py` files (AC-9
  scope-creep guard).

## CI

`.github/workflows/test.yml` runs a `plm-engine-core-tests` job
mirroring `plm-shared-tests` — editable-install + pytest on every
push to `main` / `FTR575-Codebase-Split` and on every PR touching
`02_App/**`.

## Status

- **US-CR.1**: scaffold (Conv E close `822c415`). Ships placeholders only.
- **US-CR.0** (Wave 1 Conv E + Conv F): `IdentityMiddleware`,
  `Hs256JwtIdentityProvider`, `with_system_identity`, `audit_log`
  migration 0013, and `plm-cli auth issue-token` all land. 16/16 ACs
  covered; story closed Conv F (3/3 PRs).
- **US-CR.2** (Wave 1 Conv F): two import-linter Forbidden contracts +
  permanent negative-fixture toggling test + BL6 path-based guardrail
  for `with_system_identity` + ADR exemption process.
