PACK_INTERFACE - the DomainPack contract¶
SEC-M0 deliverable. A proposed DomainPack Protocol fitted to the findings in
AV_ASSUMPTIONS.md. This is a design document, not code; it is the contract SEC-M1+ builds
against. Every surface below exists because a specific AV assumption was found baked into engine code, and the
surface is where that assumption becomes pack-supplied data.
Design rules (from what the audit found)¶
- The engine core imports nothing from packs. Enforced in CI by import-linter (SEC-M1). The core keeps the kernels the audit found already neutral (SE(3), slice math, storage, fusion state machine, gate control flow) and calls into a pack only through the Protocol below.
- A pack is data + a small amount of code. The bulk of AV-specificity the audit found is data wearing a
code costume:
STUFF_NAMESfrozensets, the{"vru","animal"}literal,CROSS_ANCHORS,supported_core, the target registries. Those move to pack config (pack.yaml+ governed ontology YAML). Only the genuinely behavioural forks (theSceneModel, the privacy detectors) are pack code. - Reuse the existing config system. Pack settings are a namespaced extension of
core/config.py(get_settings().pack(<id>)), not a parallel mechanism. AV's current defaults (OntologySettings,RigSettings,SpatialSettings,SanyxSettings, ...) become the AV pack's config block verbatim, proving parity. - AV moves behind the interface unchanged. The AV pack is authored first and must reproduce every current metric (the golden regression harness, SEC-M1, is the gate). If a surface can't express what AV does today, the surface is wrong.
The Protocol¶
# packs/base.py (engine core; a pack implements this)
from typing import Protocol, runtime_checkable
@runtime_checkable
class DomainPack(Protocol):
manifest: PackManifest # identity, version, capabilities, feature flags
ontology: OntologySpec # the taxonomy + the facts currently baked in code
scene_model: SceneModelFactory # moving-camera vs static-camera pose/geometry source
autolabel_profile: AutoLabelProfile # prompt, anchors, maps, gate policy for the 3 paths
safety_policy: SafetyPolicy # replaces the copy-pasted {"vru","animal"}
eval_strata: EvalStrataSpec # slice dimensions + critical classes + protected slices
ingestion_adapters: list[IngestionAdapter] # how raw captures become Session/Frame
quality_profile: QualityProfile # SANYX checks + rootcause table for this domain
privacy_plane: PrivacyPlane # redaction targets + legal regime (never None; see note)
forge_targets: list[ForgeTarget] # edge hardware profiles (replaces the AV silicon registries)
The names track repo conventions: *Settings/*Spec for config-shaped data, *Factory where the pack
returns a constructed object, *Profile for a bundle of policy. Each is detailed below with the exact finding
it dissolves.
manifest: PackManifest¶
Pack identity and the on/off switches for optional planes.
@dataclass(frozen=True)
class PackManifest:
id: str # "av", "sec"
version: str # semver; travels into ModelRun.provenance
display_name: str
capabilities: frozenset[str] # {"moving_camera","dynamics","depth","inertial_qa","privacy_export",...}
default_scene_model: str # "moving_camera" | "static_camera"
capabilities is the honest feature gate the audit kept bumping into: the engine already degrades gracefully
when a signal is absent (SANYX with no inertial channels, CALYX with no overlapping cameras). capabilities
makes that explicit instead of implicit, so a plane can skip cleanly ("dynamics" not in pack.capabilities for
Sec) rather than running IPM math on a static camera and producing garbage.
Dissolves: the implicit "every pack has ego-motion" premise behind ORACLYX depth, SIEVYX maneuver, and
services/dynamics/compute.py.
ontology: OntologySpec¶
The single biggest finding: ontology facts live in code, and there is one global ontology per process.
@dataclass(frozen=True)
class OntologySpec:
yaml_path: str # the governed, versioned taxonomy (already exists)
stuff_names: frozenset[str] # was services/autolabel/ontology.py:31 STUFF_NAMES
supported_core: frozenset[str] # was core/config.py:396 OntologySettings.supported_core
superclass_map: dict[str, str] # l1/l0 semantics used by quality_reviewer road-plane rules
size_priors: dict[str, SizeBound] # was core/config.py:242 size_bounds
fallback_policy: FallbackPolicy # CUSTOM_ID_BASE, fallback_ids, is_fallback (ontology.py:22,108)
Two structural fixes ride along:
get_ontology()gains a pack dimension. The@lru_cache(maxsize=1)singleton (services/autolabel/ontology.py:225) becomesget_ontology(pack_id)(cache keyed by pack + version). This is the one change that touches many call sites (the finding listsfinetune.py:54,dataset_builder.py:56, and every plane) so it is done first under SEC-M1 with the golden harness proving AV is byte-identical.stuff_namesmoves from a code frozenset into the YAML (arole: thing|stuffper class), so the panoptic split is governed data.superclass_mapexternalises the_GROUND/_VEHICLE/_VRU/_OVERHEADsets the quality reviewer keys on.
Dissolves: ontology.py:31 STUFF_NAMES, core/config.py:396 supported_core, quality_reviewer.py:20
road-plane l1 sets, the single-ontology singleton. Also fixes latent bug #1/#2 (the id-based
CLASS_HEIGHT_M and CRITICAL_CLASSES become name-based lookups through the spec, so they can't drift from
the YAML).
scene_model: SceneModelFactory - the deep fork (SEC-M2)¶
The audit located the moving-vs-static boundary precisely at services/calibration/resolve.py. This factory is
that boundary.
class SceneModelFactory(Protocol):
def build(self, session: Session, calib: Calibration) -> SceneModel: ...
class SceneModel(Protocol):
def camera_pose(self, frame: Frame) -> SE3: ... # world<-camera for this frame
def R(self) -> np.ndarray: ... # camera rotation convention
def ground_plane(self) -> Plane | None: ... # for depth/IPM priors; None if not applicable
def is_static(self) -> bool: ...
MovingCameraSceneModel(AV) wraps today'sCalibration.R() = Rz(-yaw)@Ry@Rx@_R_OPT2EGO(resolve.py:49), the SLERP ego-pose composition (core/accel/propagate.py), and the road ground plane. It readsSession.vehicle_id/Frame.ego_speed.StaticCameraSceneModel(Sec) holds a fixed per-camera extrinsic (no ego-motion, no SLERP), keys state by a stablecamera_id, and derives a scene background prior instead of a road ground plane.camera_pose()is constant across frames;ground_plane()may be None.
Everything downstream of the pose source (CALYX drift, ORACLYX depth, dynamics/compute.py, the ego-hood mask
in autolabel/runner.py:317) asks the SceneModel instead of assuming ego-motion. Where a pack's SceneModel
reports is_static(), those planes either skip (guarded by manifest.capabilities) or use the static path.
Dissolves: resolve.py ego-frame convention, calyx/drift.py VO-vs-IMU estimation, mono_depth.py:28
road-plane prior, radar.py ego-motion, the ego-hood mask, and the Session.vehicle_id/Frame.ego_speed
hard dependency (they become inputs the moving-camera SceneModel consumes and the static one ignores).
autolabel_profile: AutoLabelProfile (SEC-M5)¶
Bundles the five swappable auto-label surfaces the audit named, over the reusable runner/fusion/gate plumbing.
@dataclass(frozen=True)
class AutoLabelProfile:
vlm_prompt_template: str # was hardcoded "Indian road scene" path_c_vlm.py:64
cross_anchors: list[str] # was CROSS_ANCHORS path_c_vlm.py:186
coco_map: dict[int, str] # was COCO_TO_ONTOLOGY path_a_detect.py:21
openvocab_synonyms: dict[str, list[str]] # was _OPENVOCAB_SYNONYMS path_b_openvocab.py:29
gate_policy: GatePolicy # safety_auto_accept + is_rare rules, gate.py:20
disable_ego_hood_mask: bool # static packs set True
vlm_prompt_template is the single highest-value seam (there is no config path for it today). It is a
template the pack fills, e.g. AV's "object cropped from an Indian road scene for an autonomous-driving dataset"
vs Sec's "object cropped from an Indian CCTV/security camera scene". The runner, VRAM guard, fusion voting, and
calibration/isotonic scaling are engine core and untouched.
Dissolves: path_c_vlm.py:64 prompt, :186 anchors, path_a_detect.py:21 COCO map,
path_b_openvocab.py:29 synonyms, autolabel/gate.py:20 policy.
safety_policy: SafetyPolicy¶
The {"vru","animal"} literal appears in six+ files (champion.py:26, recall/gate.py:12,
safe_miou.py:13, autolabel/gate.py:20, dynamics/compute.py:33, flywheel/signals.py:26). One pack-scoped
definition replaces all of them.
class SafetyPolicy(Protocol):
def is_safety_class(self, class_name: str) -> bool: ... # AV: l1 in {"vru","animal"}
def affinity_cost(self, a: str, b: str) -> float: ... # AV: the safe_miou.py:21 road cost tree
def critical_classes(self) -> frozenset[str]: ... # by NAME, not id (fixes CRITICAL_CLASSES bug)
The champion_gate control flow (services/govern/champion.py) stays exactly as-is; it calls
pack.safety_policy.is_safety_class(...) instead of testing a module-level set. AV's policy returns the current
behaviour verbatim, so the golden harness sees no change. Sec's policy defines its own safety set (e.g.
{"person","weapon"}) and its own affinity.
Dissolves: the six copies of {"vru","animal"} (finding #4), the Safe-mIoU affinity tree
(safe_miou.py:21), and CRITICAL_CLASSES (fixes latent bug #2 by making it name-based).
eval_strata: EvalStrataSpec (SEC-M3/M4)¶
Replaces the hardcoded scene axes and protected slices.
@dataclass(frozen=True)
class EvalStrataSpec:
dimensions: list[StratumDimension] # AV: weather,time_of_day,road_type,density (curation/slices.py:15)
protected_slices: list[str] # AV: ["pedestrian_night","autorickshaw_glare"] (verdyx/run.py:20)
Two fixes ride along: the missing config field (latent bug #3, GovernSettings.protected_slices doesn't
exist, so this spec becomes the real source), and the strata dimensions currently duplicated across
curation/slices.py:15, explore/facets.py:28, and consumed by SIEVYX ODD. VERDYX verdict/stats/shadow and
core/accel/slices.py stay generic (they already take opaque slice-id strings). Sec's dimensions are e.g.
camera_zone, time_of_day, occupancy, event_type.
Dissolves: verdyx/run.py:20 default + missing-field bug, curation/slices.py:15 / explore/facets.py:28
strata literals, SIEVYX odd.py cell schema, and sievyx/maneuver.py:13 (the maneuver taxonomy becomes one
StratumDimension / event vocabulary the pack supplies).
ingestion_adapters: list[IngestionAdapter] (SEC-M2)¶
How a domain's raw captures become Session/Frame. The dataset paths are already generic
(frames/{session}/{cam}/{ts}.jpg), so this is about populating the schema, not repathing it.
class IngestionAdapter(Protocol):
def can_handle(self, source: IngestSource) -> bool: ...
def sessions(self, source: IngestSource) -> Iterator[SessionDraft]: ... # sets vehicle_id XOR camera_id
AV's adapter fills vehicle_id + ego_speed from MCAP/CAN; Sec's adapter fills camera_id and leaves
ego-motion fields null. Session/Frame schema is unchanged (the AV-only fields become nullable, per
AV_ASSUMPTIONS §0).
quality_profile: QualityProfile (SEC-M2)¶
SANYX's reusable image checks stay in core; the domain-specific parts become pack data.
@dataclass(frozen=True)
class QualityProfile:
image_checks: list[str] # reused core checks: dropped/exposure/lens (checks.py:79)
sensor_checks: list[SensorCheck] # AV: imu/gps/time_sync with MTi-3/NEO-F9P limits (checks.py:167)
rootcause_signatures: list[Signature] # was rootcause.py:14 fault table
score_weights: ScoreWeights
AV supplies the inertial checks + the GMSL2/GPS/IMU fault signatures; Sec supplies CCTV signatures (stream dropout, IR-cut flip, tampering) and no inertial checks. The scoring/gate/streaming machinery is core.
Dissolves: sanyx/checks.py:167 hardcoded sensor limits, sanyx/inertial.py sensor vocab,
sanyx/rootcause.py:14 AV fault table, sanyx/lite.py.
privacy_plane: PrivacyPlane (SEC-M6)¶
The audit's correction to the mission stands: AV is not None here. The privacy organ already exists,
fail-closed, at two gates. The interface generalises its targets and legal regime.
class PrivacyPlane(Protocol):
redaction_targets: list[RedactionTarget] # AV: face, plate. Sec: face, plate, + ...
legal_regime: LegalRegime # DPDPA today; config not code
def ingest_gate(self, frame: Frame) -> RedactionResult: ... # Gate A: blur before storage
def export_gate(self, clip: Clip) -> ExportDecision: ... # refuse un-redacted export
AV instantiates it with face+plate detectors and the DPDPA regime (exactly today's behaviour, wiring at
ingest/run.py:120 and export/dataset.py:168 unchanged). Sec adds targets and can carry retention/consent
metadata as config, not new code paths. The unwired speech seam (anonymize/speech.py:14) becomes an
optional RedactionTarget a pack may enable.
Dissolves: the AV-typed hardcoding in services/anonymize/ (face/plate/DPDPA as the only shape), and the
mission's incorrect PrivacyPlane | None.
forge_targets: list[ForgeTarget] (SEC-M9)¶
Replaces the four AV silicon registries with a per-pack hardware profile list.
@dataclass(frozen=True)
class ForgeTarget:
name: str # opaque string FORGYX gate/packaging already accept
backend: str # was _BACKENDS capabilities.py:18
thermal_envelope: Thermal # was TARGET_THERMAL thermal.py:11
latency_budget_ms: float # was TARGET_BUDGET_MS cooptimize.py:11
export_format: str # was _FORMAT run.py:19
telemetry_parser: str # AV: tegrastats; Sec: NVR/Ambarella/ARTPEC
gate.py/packaging.py/rollout are already fully generic (opaque target strings), so only the registries move.
AV registers Jetson/Sentrix targets; Sec registers CCTV SoCs and x86.
Dissolves: capabilities.py:18 _BACKENDS, thermal.py:11, cooptimize.py:11, run.py:19 _FORMAT,
benchmark.py:44 tegrastats.
Registry and discovery¶
# packs/registry.py (engine core)
def load_packs() -> dict[str, DomainPack]: ... # discover packs/*/pack.yaml, validate manifest, register
def get_pack(pack_id: str) -> DomainPack: ...
Packs live in packs/<id>/ with a pack.yaml (manifest + config) and Python modules for the behavioural
surfaces (scene_model.py, privacy.py). Discovery mirrors the existing platform registry
(web/platforms/registry.ts has the front-end analogue; this is the backend twin). Multiple packs load in one
process; the ontology cache and every plane take a pack_id. A Session.pack_id column (nullable, defaulting
to "av" in the backfill) routes a session to its pack.
What stays in the engine core (unchanged)¶
Per AV_ASSUMPTIONS §7, these are already neutral and do not get a pack seam:
- The SE(3)/geometry kernels (
core/geometry.py,core/accel/projection.py, Kabsch, Sampson, consensus). - Slice/confusion aggregation (
core/accel/slices.py), parameterised byn_classes. - The content-addressed store and all dataset path conventions (
core/storage.py,ingest/run.py:40). - The auto-label runner/fusion/gate plumbing (only the five profile surfaces move).
- The
champion_gate/ recall-gate / VERDYX-verdict control flow (only the predicates move). - FORGYX gate/packaging/rollout (only the target registries move).
- The privacy mechanism and its two chokepoints (only targets + regime move).
- Every JSONB DB model (Evaluation, ModelRegistry, Deployment, PseudoLabel, ...) already domain-agnostic.
Build order this contract implies¶
- SEC-M1 - golden regression harness + import-linter;
get_ontology(pack_id); author the AV pack against this contract and prove byte-parity. No behaviour change. - SEC-M2 -
SceneModelFactory+StaticCameraSceneModel; ingestion adapter + quality profile for Sec. - SEC-M3/M4 -
OntologySpec/SafetyPolicy/EvalStrataSpecfor the Sec taxonomy; eval strata. - SEC-M5 -
AutoLabelProfilefor Sec (CCTV prompt, anchors, gate policy). - SEC-M6 -
PrivacyPlanegeneralisation (retention/consent/rights as config). - SEC-M7/M8/M9 - ANPR-India, the Sentigon webhook,
ForgeTargetCCTV profiles.
Each milestone announces its one-paragraph plan first. The AV pack (SEC-M1) is the parity proof: if it can't reproduce today's metrics through this contract, the contract is wrong and gets revised before Sec begins.