Skip to content

feat(analytic-registry): routine declaration + bindings, AnalyticFunctionSpec as its view (RS-W2-2 PR-A) - #547

Closed
dwolfson wants to merge 1 commit into
odpi:mainfrom
dwolfson:feat/rs-w2-2-pr-a-routine-declaration-bindings
Closed

dwolfson wants to merge 1 commit into
odpi:mainfrom
dwolfson:feat/rs-w2-2-pr-a-routine-declaration-bindings

Conversation

@dwolfson

@dwolfson dwolfson commented Nov 8, 2026

Copy link
Copy Markdown
Member

RS-W2-2 PR-A (Wave 2 delivery step (a″), first of three): the routine declaration and bindings as a model, with no behaviour change. A registry entry is now readable as a RoutineDeclaration (data) plus Bindings (how it is reached), and AnalyticFunctionSpec is the compatibility view of one: its function is the one python binding. Decision D2 applied: the schema mirrors egeria-trellis-analytics 0.4's trellis_analytics.routine field for field, and a cross-validation test loads every pyegeria built-in in the library. The resolver (PR-B) and the parameter rules (PR-C) are not in this PR.

Model (pyegeria/view/analytic_registry.py)

  • New, strict (extra="forbid"), same names and rules as the library: RoutineDeclaration, ParamDeclaration (category: content | shape | performance), Binding (python | egeria_query | sql | step), TileSafety, Admission; vocabularies ROUTINE_KINDS, BINDING_KINDS, PARAM_CATEGORIES, INPUT_ROLES. The refusals are the library's sentences: a constant that is also a param, a required param with a default, two python bindings, a non-role input, in on anything but a processor, a generic routine with a binding_note, a tile cap with safe false, no tile reason.
  • RoutineDeclaration.declaration is the W2-1a AnalyticDeclaration; out lives there, as in the library.
  • AnalyticFunctionSpec is the view. It gains the declaration's own fields, all optional, so it carries a declaration whole: bindings, inputs, constants, in (attribute in_), pushdown, tile_safe, test_only (with resource_types, the Admission); AnalyticParam gains category (default content); kind widens to producer | processor | routine | step. function and the python binding are one value: either fills the other, and two that differ, two python bindings, or no binding at all are refused. function may now be empty for an entry with only non-python bindings.
  • spec.routine() / AnalyticFunctionSpec.from_routine(routine, returns=, action=, envelope=) convert both ways; returns, action and envelope are the view's own fields, which a declaration doesn't carry.
  • get_routine_registry() converts every registry entry mechanically (built-in, config, installed, runtime). An entry that breaks a declaration rule is left out and named, with the rule's words, by routine_declaration_errors(). It still runs as before, so a third-party entry can't stop loading because of this PR.
  • declaration.out accepts the library's three forms (RS-W2-1a read-back F2): a field list, a registered shape name, or an inline shape. It also accepts None, the library's word for "not declared". Read it through the new AnalyticDeclaration.out_fields(). A shape name only the library registers, and an inline shape's reused fields (extends/include), read as [] here; PR-B's check 3 says why.

The allow-list applies to binding targets only (ISSUE-173)

format_set_executor.python_binding_allowed(target) is the module allow-list, stated as a check on a python binding's target. _resolve_analytic_function resolves a registered name to the entry's python binding. An entry with no python binding is refused by name, for example Analytic function 'saved_census' has no python binding (its bindings: egeria_query 'Census query'); this pyegeria runs python bindings only. That path couldn't exist before. Its other two cases, a path a registered entry declares and a path under an allowed prefix, are unchanged. They are binding targets (action fetch/analytic steps, binder factories), never report-spec strings: resolve_report_spec_function already accepts registered names only. A test pins that a dotted path under an allowed prefix is still refused from a report spec.

One data change: growth_series.binding_note

growth_series is generic and carried a binding_note ("type_map defaults to assets/terms/governed/products but is fully overridable."). The library refuses that combination, because a note names what is fixed and a generic routine fixes nothing. The note repeated the type_map param's own description, so it is now empty. Nothing renders it: _demo() uses the note only when generic is false, and the demo spec is byte-identical. It still shows in analytic_registry_payload().

Gates (brief §3, PR-A)

  • Micro-tests unchanged and green on Python 3.13: uv run -p 3.13 pytest tests/micro-tests/ gives 2204 passed, 3 skipped (2207 collected). The 3 skips are the two library cross-validation modules, skipped because the library isn't a dependency, and one pre-existing skip. No existing test was edited.
  • With egeria-trellis-analytics 0.4.0 installed (the feat/declared-shapes-0.4 branch, 549d565; scratch venv, Python 3.13, editable pyegeria from this branch): the whole micro-suite gives 2329 passed, 1 skipped (2330 collected), including 99 cross-validation tests across test_routine_declarations_match_library.py and W2-1a's test_declarations_match_library.py. The registry has 31 entries: 28 built-in and 3 from the library (curation_coverage, context_pack, curation_coverage_tile). All 31 convert, and routine_declaration_errors() is {}. That run found that the library writes an undeclared out as None, which the first cut refused; it is accepted now and tested.
  • exec_report_spec byte-identical on the demo specs: all 23 analytic demo specs, in DICT and JSON (46 runs), with a fake client that exercises every routine. I wrote the JSON of every result, plus each spec's model_dump and each registry entry's function, before (upstream/main 48392e6) and after; diff shows no difference. Two runs of the baseline were identical, so the comparison is deterministic.
  • Registry round-trips: every built-in goes spec → routine() → from_routine → model_dump equal to the original (28 tests). A declaration that uses every field (processor with in, inputs, constants, three param categories, tile cap, admission, an egeria_query and a python binding) goes declaration → view → declaration equal, and through model_dump/model_validate by name and by alias.
  • Cross-validation (test_routine_declarations_match_library.py, skipped without library 0.4): every built-in's routine loads as a library RoutineDeclaration, with no DeprecationWarning reaching it. The library's own reading of each pyegeria entry (routine_declaration(spec)) equals pyegeria's spec.routine(). Model field names and the four vocabularies match the library's exactly.

Not in this PR

  • Param categories are not authored. Every built-in param reads content by default. Choosing shape or performance per param, such as page caps or strict, is judgment that PR-C's four rules depend on, so it belongs with PR-C, or a follow-up if the Architect prefers.
  • inputs, constants, pushdown and tile_safe are empty on every built-in. The model carries them; authoring them is W2-3 (roles) and PR-C (constants).
  • PR-B (resolver, runnable/unrunnable_reason), PR-C (parameter rules, D3), and the executor reading target_type_name: per the brief, those are PR-B and PR-C.

ruff is clean on every new and changed line. The 4 findings in format_set_executor.py (unused json, three unused e) are already on upstream/main.

No version bump, no tag. No Egeria, Postgres or 8813 writes.

🤖 Generated with Claude Code

…tionSpec as its view (RS-W2-2 PR-A)

A registry entry is now readable as a RoutineDeclaration (data) plus Bindings
(python | egeria_query | sql | step), mirroring trellis-analytics 0.4's
trellis_analytics.routine field for field (decision D2). AnalyticFunctionSpec
is the compatibility view: `function` is the one python binding; it gains the
declaration's optional fields (bindings, inputs, constants, in, pushdown,
tile_safe, test_only; AnalyticParam.category) so routine()/from_routine()
round-trip. get_routine_registry() converts every entry mechanically and
names any entry that breaks a declaration rule in routine_declaration_errors()
instead of raising.

AnalyticDeclaration.out accepts the library's three forms (field list, shape
name, inline shape) and None, read through out_fields() (RS-W2-1a read-back F2).

The module allow-list is stated as python_binding_allowed(target): it applies
to binding targets only; a report spec still names routines by registered name
(ISSUE-173). An entry with no python binding is refused by name when run.

growth_series drops a binding_note that repeated its type_map description: a
generic routine declares no note (library rule). Nothing renders it.

No behaviour change: all 23 analytic demo specs give byte-identical
exec_report_spec output (DICT and JSON) before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dan Wolfson <dan.wolfson@pdr-associates.com>
@dwolfson

dwolfson commented Nov 8, 2026

Copy link
Copy Markdown
Member Author

Duplicate of #546, which merged this same head (81c7a79) as 0c0df7c.

@dwolfson dwolfson closed this Nov 8, 2026
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.

1 participant