Skip to content

feat(filter_runtime): typed FilterConfigBase + emit_schema (FILTER-441)#86

Merged
stwilt merged 1 commit into
mainfrom
feat/filter-441-typed-filter-config-base
May 4, 2026
Merged

feat(filter_runtime): typed FilterConfigBase + emit_schema (FILTER-441)#86
stwilt merged 1 commit into
mainfrom
feat/filter-441-typed-filter-config-base

Conversation

@stwilt

@stwilt stwilt commented May 1, 2026

Copy link
Copy Markdown
Contributor

📦 Pull Request

📋 What does this PR do?

Introduces FilterConfigBase(BaseSettings) — the typed, introspectable successor to the legacy adict-based FilterConfig — and the surrounding machinery that lets a filter declare its config contract in Python and emit it as a JSON Schema (draft 2020-12) the platform can ingest without pulling the filter image.

Specifically:

  • New FilterConfigBase in openfilter/filter_runtime/config.py. Subclasses get pydantic-settings env-var sourcing (FILTER_* prefix, __ nested delimiter), declarative range/enum/pattern/format constraints, and a model_json_schema() export.
  • New openfilter/filter_runtime/formats.pyOpenfilterSource and VideoSource Annotated aliases plus their Python-side validators (FC-1.5 py).
  • FC-2 markers: x-openfilter-managed, x-openfilter-resolve, and x-openfilter-preflight JSON Schema extension keywords, plus Managed(...) and Resolve(...) field helpers. Managed fields are stripped from the operator-facing schema by default (filter exposes operator surface only; orchestrator handles managed fields).
  • New authoring guide at docs/declarative-config.md covering the typed-config contract end to end (Tier 1/2/3 migration, env-var sourcing, tagged unions, named formats, the Managed / Resolve boundary).
  • Tier-3 fallback intact: filters that don't subclass FilterConfigBase keep working unchanged.
  • Adds pydantic~=2.9 and pydantic-settings~=2.6 as hard deps; lock file refreshed.

The build-time CLI half (openfilter emit-schema) is stacked on top of this PR as
FILTER-442.


🔍 Why is this needed?

Filter authors need a way to declare their config contract once, in Python, and have the platform pick it up at deploy time without side-loading filter images. Today the platform infers config from hand-maintained env-var docs that drift; the platform-side ingest work (Declarative Configuration Validation) is blocked on having a real schema artifact to ingest.

This PR is the SDK-side scaffolding for that contract. It ships the typed base class, the named-format aliases, and the FC-2 boundary markers that distinguish operator-visible config from platform-managed / resolved / preflight fields. Once it lands, individual filters can opt in at their own cadence (Tier 1/2 per the design doc); unmigrated filters keep working through the openfilter 1.0 cut.

Design doc:
Declarative Filter Configuration Contract — Goldenrod.2.


🧪 How was it tested?

Unit tests (37, all passing)

tests/test_filter_config_base.py covers:

  • env-var sourcing with the FILTER_* prefix
  • tagged unions via env_nested_delimiter="__" (sam3 prompt-mode fitness test)
  • managed-field exclusion in emit_schema() and --include-managed parity
  • Managed / Resolve field helpers and parent/child override semantics
  • OpenfilterSource / VideoSource validators (accept/reject + JSON Schema format keyword)
$ uv run --with pytest pytest tests/test_filter_config_base.py -q
.....................................                                    [100%]
37 passed in 0.30s

Integration spike: sam3-shaped config

Validated FILTER-441's load-bearing acceptance criterion — sam3's three mutually-exclusive prompt modes as a tagged union — against a sam3-shaped FilterConfigBase subclass (text / multi-text / exemplars prompt modes, plus Managed sources and device fields, plus representative hyperparams). Results:

  • prompting emits a discriminated union with discriminator: {propertyName: "prompt_mode", mapping: {...}} and oneOf referring to $defs/{TextPrompt,MultiTextPrompt,Exemplars}
  • ✅ Managed fields (sources, device) stripped from operator-facing schema; --include-managed surfaces them with x-openfilter-managed and x-openfilter-resolve markers
  • sources.items.format == "video-source" (FC-1.5 named format round-trips)
  • ✅ Pydantic-settings env-var sourcing into the tagged union: FILTER_PROMPTING__PROMPT_MODE=text FILTER_PROMPTING__TEXT_PROMPT='person walking' parses into the TextPrompt variant; FILTER_CONFIDENCE_THRESHOLD=0.7 overrides the top-level default
  • ✅ Top-level required: ["prompting"] correctly surfaces fields with no default

This stacks on FILTER-442 for the CLI invocation; the spike was run via
python -m openfilter.cli emit-schema from the FILTER-442 worktree.

Tier-3 fallback

No production filter touched in this PR. Filters that don't subclass FilterConfigBase (i.e. every existing one) are unaffected.


🔗 Related Issues

  • FILTER-441 — this ticket
  • FILTER-440 — Declarative Configuration Contract (parent epic)
  • FILTER-442openfilter emit-schema CLI (stacked, follows this PR)

🖼️ Screenshots or Logs (if applicable)

Sample emitted schema (managed field stripped by default):

class MyFilter(FilterConfigBase):
    sources: list[VideoSource] = Managed([], resolve="orchestrator-generated")
    confidence_threshold: float = Field(default=0.5, ge=0, le=1)

>>> MyFilter.emit_schema()
{
  "title": "MyFilter",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "confidence_threshold": {
      "type": "number", "minimum": 0, "maximum": 1, "default": 0.5
    }
  },
  ...
}

✅ Checklist

  • I have read and agreed to the terms of the LICENSE
  • I have read the CONTRIBUTING guide
  • I have followed the coding style
  • I have signed all commits in compliance with the DCO (git commit -s)
  • I have added or updated tests as needed
  • I have added or updated documentation as needed (docs/declarative-config.md — authoring guide for the typed-config contract; the parent epic FC-12 row is closed by this addition)

@stwilt stwilt requested a review from lucasmundim May 1, 2026 18:24

@lucasmundim lucasmundim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid PR — the design cleanly separates operator-facing from platform-managed config, the pydantic-settings integration is well done, and the 37 tests cover the core contracts well. The _NamedFormat pattern for injecting format: into JSON Schema while keeping the runtime validator is elegant, and the authoring guide is thorough.

A few items worth addressing before merge:

Comment thread docs/declarative-config.md
Comment thread openfilter/filter_runtime/config.py Outdated
Comment thread openfilter/filter_runtime/formats.py Outdated
Comment thread openfilter/filter_runtime/config.py
Comment thread openfilter/filter_runtime/config.py
Comment thread openfilter/filter_runtime/config.py
Comment thread openfilter/filter_runtime/config.py Outdated
Comment thread tests/test_filter_config_base.py
@stwilt stwilt force-pushed the feat/filter-441-typed-filter-config-base branch from e06cfce to b0175e7 Compare May 2, 2026 06:29

@lucasmundim lucasmundim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second-pass review. All first-round items confirmed fixed in b0175e7. The PR is in good shape — none of these are blockers, mostly suggestions and edge-case hardening.

Comment thread pyproject.toml Outdated
Comment thread openfilter/filter_runtime/config.py Outdated
Comment thread openfilter/filter_runtime/config.py
Comment thread openfilter/filter_runtime/config.py
Comment thread openfilter/filter_runtime/config.py
# 📦 Pull Request

## 📋 What does this PR do?

Introduces `FilterConfigBase(BaseSettings)` — the typed, introspectable
successor to the legacy `adict`-based `FilterConfig` — and the
surrounding machinery that lets a filter declare its config contract in
Python and emit it as a JSON Schema (draft 2020-12) the platform can
ingest without pulling the filter image.

Specifically:

- New `FilterConfigBase` in `openfilter/filter_runtime/config.py`.
Subclasses get pydantic-settings env-var sourcing (`FILTER_*` prefix,
`__` nested delimiter), declarative range/enum/pattern/format
constraints, and a `model_json_schema()` export.
- New `openfilter/filter_runtime/formats.py` — `OpenfilterSource` and
`VideoSource` Annotated aliases plus their Python-side validators
(FC-1.5 py).
- FC-2 markers: `x-openfilter-managed`, `x-openfilter-resolve`, and
`x-openfilter-preflight` JSON Schema extension keywords, plus
`Managed(...)` and `Resolve(...)` field helpers. Managed fields are
stripped from the operator-facing schema by default (filter exposes
operator surface only; orchestrator handles managed fields).
- New authoring guide at `docs/declarative-config.md` covering the
typed-config contract end to end (Tier 1/2/3 migration, env-var
sourcing, tagged unions, named formats, the `Managed` / `Resolve`
boundary).
- Tier-3 fallback intact: filters that don't subclass
`FilterConfigBase` keep working unchanged.
- Adds `pydantic~=2.9` and `pydantic-settings~=2.6` as hard deps; lock
file refreshed.

The build-time CLI half (`openfilter emit-schema`) is stacked on top of
this PR as
[FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442).

---

## 🔍 Why is this needed?

Filter authors need a way to declare their config contract once, in
Python, and have the platform pick it up at deploy time without
side-loading filter images. Today the platform infers config from
hand-maintained env-var docs that drift; the platform-side ingest work
([Declarative Configuration Validation](https://plainsight-ai.atlassian.net/browse/FILTER-440))
is blocked on having a real schema artifact to ingest.

This PR is the SDK-side scaffolding for that contract. It ships the
typed base class, the named-format aliases, and the FC-2 boundary
markers that distinguish operator-visible config from
platform-managed / resolved / preflight fields. Once it lands,
individual filters can opt in at their own cadence (Tier 1/2 per the
design doc); unmigrated filters keep working through the openfilter 1.0
cut.

Design doc:
[Declarative Filter Configuration Contract — Goldenrod.2](https://plainsight-ai.atlassian.net/wiki/spaces/ENG/pages/2755919874).

---

## 🧪 How was it tested?

### Unit tests (37, all passing)

`tests/test_filter_config_base.py` covers:

- env-var sourcing with the `FILTER_*` prefix
- tagged unions via `env_nested_delimiter="__"` (sam3 prompt-mode
  fitness test)
- managed-field exclusion in `emit_schema()` and `--include-managed`
  parity
- `Managed` / `Resolve` field helpers and parent/child override
  semantics
- `OpenfilterSource` / `VideoSource` validators (accept/reject + JSON
  Schema `format` keyword)

```
$ uv run --with pytest pytest tests/test_filter_config_base.py -q
.....................................                                    [100%]
37 passed in 0.30s
```

### Integration spike: sam3-shaped config

Validated FILTER-441's load-bearing acceptance criterion — sam3's three
mutually-exclusive prompt modes as a tagged union — against a
sam3-shaped `FilterConfigBase` subclass (text / multi-text / exemplars
prompt modes, plus `Managed` `sources` and `device` fields, plus
representative hyperparams). Results:

- ✅ `prompting` emits a discriminated union with
  `discriminator: {propertyName: "prompt_mode", mapping: {...}}` and
  `oneOf` referring to `$defs/{TextPrompt,MultiTextPrompt,Exemplars}`
- ✅ Managed fields (`sources`, `device`) stripped from operator-facing
  schema; `--include-managed` surfaces them with `x-openfilter-managed`
  and `x-openfilter-resolve` markers
- ✅ `sources.items.format == "video-source"` (FC-1.5 named format
  round-trips)
- ✅ Pydantic-settings env-var sourcing into the tagged union:
  `FILTER_PROMPTING__PROMPT_MODE=text FILTER_PROMPTING__TEXT_PROMPT='person walking'`
  parses into the `TextPrompt` variant; `FILTER_CONFIDENCE_THRESHOLD=0.7`
  overrides the top-level default
- ✅ Top-level `required: ["prompting"]` correctly surfaces fields with
  no default

This stacks on [FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442)
for the CLI invocation; the spike was run via
`python -m openfilter.cli emit-schema` from the FILTER-442 worktree.

### Tier-3 fallback

No production filter touched in this PR. Filters that don't subclass
`FilterConfigBase` (i.e. every existing one) are unaffected.

---

## 🔗 Related Issues

- [FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441) — this ticket
- [FILTER-440](https://plainsight-ai.atlassian.net/browse/FILTER-440) — Declarative Configuration Contract (parent epic)
- [FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442) — `openfilter emit-schema` CLI (stacked, follows this PR)

---

## 🖼️ Screenshots or Logs (if applicable)

Sample emitted schema (managed field stripped by default):

```python
class MyFilter(FilterConfigBase):
    sources: list[VideoSource] = Managed([], resolve="orchestrator-generated")
    confidence_threshold: float = Field(default=0.5, ge=0, le=1)

>>> MyFilter.emit_schema()
{
  "title": "MyFilter",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "confidence_threshold": {
      "type": "number", "minimum": 0, "maximum": 1, "default": 0.5
    }
  },
  ...
}
```

---

## ✅ Checklist

- [x] I have read and agreed to the terms of the [LICENSE](../LICENSE)
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide
- [x] I have followed the [coding style](../CONTRIBUTING.md#coding-style)
- [x] I have signed all commits in compliance with the DCO (`git commit -s`)
- [x] I have added or updated **tests** as needed
- [x] I have added or updated **documentation** as needed
  (`docs/declarative-config.md` — authoring guide for the typed-config
  contract; the parent epic FC-12 row is closed by this addition)

Signed-off-by: stwilt <swilt@plainsight.ai>
@stwilt stwilt force-pushed the feat/filter-441-typed-filter-config-base branch from b0175e7 to d724eb5 Compare May 2, 2026 20:39

@lucasmundim lucasmundim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-3 review — all round-2 items verified in d724eb5:

  • typing-extensions floor relaxed to >=4.6 to match pydantic
  • required: [] omitted when every required field is managed (top-level and nested); new required_handled flag handles dict-iteration order both ways
  • $schema: "https://json-schema.org/draft/2020-12/schema" injected in emit_schema() via setdefault
  • ResolveHint added to __all__ and re-exported from openfilter.filter_runtime
  • json_schema_extra merge behavior locked in by test

The new @deprecated decorator on legacy FilterConfig is well-scoped — verified third-party subclassers still see the warning under python -W error::DeprecationWarning, and openfilter-internal ones are correctly silenced via the module-name regex. The append=False choice is deliberate and well-commented.

46/46 tests in test_filter_config_base.py pass.

@stwilt stwilt merged commit a6dc1c3 into main May 4, 2026
10 checks passed
@stwilt stwilt deleted the feat/filter-441-typed-filter-config-base branch May 4, 2026 04:48
stwilt added a commit that referenced this pull request May 5, 2026
# 📦 Pull Request

## 📋 What does this PR do?

Adds the `openfilter emit-schema <module>[:Class]` CLI subcommand —
the build-time producer half of the FC-3 schema-transport contract.
The command imports a filter module, locates its `FilterConfigBase`
subclass, and writes the emitted JSON Schema (draft 2020-12) to
stdout (default) or `-o <path>`. Filter Dockerfiles invoke this at
build time; the resulting JSON is pushed as a sibling OCI artifact
(`application/vnd.openfilter.schema+json`) and the image manifest is
stamped with a `com.plainsight.openfilter.schema` label carrying the
artifact's sha256 digest.

Specifically:

- New `openfilter/cli/cmd_emit_schema.py` — argparse-driven
  subcommand with `module` / `module:Class` resolution, automatic
  candidate detection (errors out cleanly when a module declares 0
  or 2+ `FilterConfigBase` subclasses), `--include-managed` opt-in
  to surface platform-managed fields, and `-o <path>` output
  redirection.
- Tweak the `openfilter` CLI dispatcher
  (`openfilter/cli/__main__.py`) to translate `cmd-with-hyphens` to
  `cmd_with_hyphens` so subcommands can use their canonical
  hyphenated spellings without breaking the underscore-named Python
  handler convention.
- 5 unit tests in `tests/test_emit_schema_cli.py` covering stdout
  output, `-o <path>`, `--include-managed`, single-class auto-pick,
  and unknown-class failure.

The wiring of the sibling-artifact upload + label-stamping into
`cloudbuild-cascade.yaml` is tracked as a sub-step of
[FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442);
it depends on PLAT-side FC-5 ingest and lands separately.

---

## 🔍 Why is this needed?

[FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441)
landed `FilterConfigBase.emit_schema()` — the in-process serializer
that produces each filter's JSON Schema. This PR is the CLI wrapper
Dockerfiles invoke at build time so the artifact can ride alongside
the image without a Python-runtime hop in the platform's ingest
path.

Once both are in place, the transport contract closes: filter CI
emits the schema, pushes it as a sibling OCI artifact, and stamps
the image manifest's `com.plainsight.openfilter.schema` label with
the sha256 digest. The platform fetches the artifact by digest from
the same registry without pulling the filter image's layers.
Integrity is transitively protected by whatever trust we already
place in the registry serving the image bytes — no separate key
infrastructure needed.

(Cosign-signed attestations were considered for the transport but
deferred to a future-driver-only ticket — see
[FILTER-448](https://plainsight-ai.atlassian.net/browse/FILTER-448),
closed Won't Do — the marginal threat surface didn't justify
standing up KMS for the first internal use.)

---

## 🧪 How was it tested?

5 unit tests in `tests/test_emit_schema_cli.py`:

- `test_emit_schema_cli_stdout` — `module:Class` spec, stdout
  output, `$schema` keyword + managed-field stripping
- `test_emit_schema_cli_writes_file` — `-o <path>` redirection
- `test_emit_schema_cli_include_managed_surfaces_overrides` —
  `--include-managed` round-trips FC-2 markers
- `test_emit_schema_cli_auto_picks_single_class` — module-only spec
  finds the lone `FilterConfigBase` subclass
- `test_emit_schema_cli_fails_on_unknown_class` — typo'd class name
  errors out cleanly

```
$ uv run --with pytest pytest tests/test_emit_schema_cli.py -q
.....                                                                    [100%]
5 passed in 1.79s
```

Combined with [FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441)'s
config-side suite (already merged in #86, 46 tests), the full
FC-1 / FC-1.5 / FC-2 / FC-3-emission contract has 51 tests
exercising it end-to-end.

---

## 🔗 Related Issues

- [FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442) — this ticket
- [FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441) — `FilterConfigBase.emit_schema()` (merged in #86)
- [FILTER-440](https://plainsight-ai.atlassian.net/browse/FILTER-440) — Declarative Configuration Contract (parent epic)
- [FILTER-448](https://plainsight-ai.atlassian.net/browse/FILTER-448) — cosign / KMS signing (closed Won't Do; revisit on concrete compliance/multi-tenancy driver)

---

## 🖼️ Screenshots or Logs (if applicable)

```
$ openfilter emit-schema my_filter.filter --include-managed
{
  "title": "MyFilterConfig",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "sources": {
      "items": {"format": "video-source", "type": "string"},
      "type": "array",
      "x-openfilter-managed": true,
      "x-openfilter-resolve": "orchestrator-generated"
    },
    "confidence_threshold": {
      "type": "number", "minimum": 0, "maximum": 1, "default": 0.5
    },
    ...
  }
}
```

---

## ✅ Checklist

- [x] I have read and agreed to the terms of the [LICENSE](../LICENSE)
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide
- [x] I have followed the [coding style](../CONTRIBUTING.md#coding-style)
- [x] I have signed all commits in compliance with the DCO (`git commit -s`)
- [x] I have added or updated **tests** as needed
- [x] I have added or updated **documentation** as needed
  (`docs/declarative-config.md`'s "Schema emission" section, landed
  with FILTER-441 in #86, already describes this CLI's surface)

Signed-off-by: stwilt <swilt@plainsight.ai>
stwilt added a commit that referenced this pull request May 6, 2026
# 📦 Pull Request

## 📋 What does this PR do?

Adds the `openfilter emit-schema <module>[:Class]` CLI subcommand — the
build-time producer half of the FC-3 schema-transport contract. The
command imports a filter module, locates its `FilterConfigBase`
subclass, and writes the emitted JSON Schema (draft 2020-12) to stdout
(default) or `-o <path>`. Filter Dockerfiles invoke this at build time;
the resulting JSON is pushed as a sibling OCI artifact
(`application/vnd.openfilter.schema+json`) and the image manifest is
stamped with a `com.plainsight.openfilter.schema` label carrying the
artifact's sha256 digest.

Specifically:

- New `openfilter/cli/cmd_emit_schema.py` — argparse-driven subcommand
with `module` / `module:Class` resolution, automatic candidate detection
(errors out cleanly when a module declares 0 or 2+ `FilterConfigBase`
subclasses), `--include-managed` opt-in to surface platform-managed
fields, and `-o <path>` output redirection.
- Tweak the `openfilter` CLI dispatcher (`openfilter/cli/__main__.py`)
to translate `cmd-with-hyphens` to `cmd_with_hyphens` so subcommands can
use their canonical hyphenated spellings without breaking the
underscore-named Python handler convention.
- 5 unit tests in `tests/test_emit_schema_cli.py` covering stdout
output, `-o <path>`, `--include-managed`, single-class auto-pick, and
unknown-class failure.

The wiring of the sibling-artifact upload + label-stamping into
`cloudbuild-cascade.yaml` is tracked as a sub-step of
[FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442); it
depends on PLAT-side FC-5 ingest and lands separately.

---

## 🔍 Why is this needed?

[FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441)
landed `FilterConfigBase.emit_schema()` — the in-process serializer that
produces each filter's JSON Schema. This PR is the CLI wrapper
Dockerfiles invoke at build time so the artifact can ride alongside the
image without a Python-runtime hop in the platform's ingest path.

Once both are in place, the transport contract closes: filter CI emits
the schema, pushes it as a sibling OCI artifact, and stamps the image
manifest's `com.plainsight.openfilter.schema` label with the sha256
digest. The platform fetches the artifact by digest from the same
registry without pulling the filter image's layers. Integrity is
transitively protected by whatever trust we already place in the
registry serving the image bytes — no separate key infrastructure
needed.

(Cosign-signed attestations were considered for the transport but
deferred to a future-driver-only ticket — see
[FILTER-448](https://plainsight-ai.atlassian.net/browse/FILTER-448),
closed Won't Do — the marginal threat surface didn't justify standing up
KMS for the first internal use.)

---

## 🧪 How was it tested?

5 unit tests in `tests/test_emit_schema_cli.py`:

- `test_emit_schema_cli_stdout` — `module:Class` spec, stdout output,
`$schema` keyword + managed-field stripping
- `test_emit_schema_cli_writes_file` — `-o <path>` redirection
- `test_emit_schema_cli_include_managed_surfaces_overrides` —
`--include-managed` round-trips FC-2 markers
- `test_emit_schema_cli_auto_picks_single_class` — module-only spec
finds the lone `FilterConfigBase` subclass
- `test_emit_schema_cli_fails_on_unknown_class` — typo'd class name
errors out cleanly

```
$ uv run --with pytest pytest tests/test_emit_schema_cli.py -q
.....                                                                    [100%]
5 passed in 1.79s
```

Combined with
[FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441)'s
config-side suite (already merged in #86, 46 tests), the full FC-1 /
FC-1.5 / FC-2 / FC-3-emission contract has 51 tests exercising it
end-to-end.

---

## 🔗 Related Issues

- [FILTER-442](https://plainsight-ai.atlassian.net/browse/FILTER-442) —
this ticket
- [FILTER-441](https://plainsight-ai.atlassian.net/browse/FILTER-441) —
`FilterConfigBase.emit_schema()` (merged in #86)
- [FILTER-440](https://plainsight-ai.atlassian.net/browse/FILTER-440) —
Declarative Configuration Contract (parent epic)
- [FILTER-448](https://plainsight-ai.atlassian.net/browse/FILTER-448) —
cosign / KMS signing (closed Won't Do; revisit on concrete
compliance/multi-tenancy driver)

---

## 🖼️ Screenshots or Logs (if applicable)

```
$ openfilter emit-schema my_filter.filter --include-managed
{
  "title": "MyFilterConfig",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "sources": {
      "items": {"format": "video-source", "type": "string"},
      "type": "array",
      "x-openfilter-managed": true,
      "x-openfilter-resolve": "orchestrator-generated"
    },
    "confidence_threshold": {
      "type": "number", "minimum": 0, "maximum": 1, "default": 0.5
    },
    ...
  }
}
```

---

## ✅ Checklist

- [x] I have read and agreed to the terms of the [LICENSE](../LICENSE)
- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) guide
- [x] I have followed the [coding
style](../CONTRIBUTING.md#coding-style)
- [x] I have signed all commits in compliance with the DCO (`git commit
-s`)
- [x] I have added or updated **tests** as needed
- [x] I have added or updated **documentation** as needed
(`docs/declarative-config.md`'s "Schema emission" section, landed with
FILTER-441 in #86, already describes this CLI's surface)

[FILTER-442]:
https://plainsight-ai.atlassian.net/browse/FILTER-442?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Signed-off-by: stwilt <swilt@plainsight.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants