Skip to content

feat(ide): engine lens schema CLI and a schema-driven Steps renderer (BACKLOG #235) - #226

Merged
wshallwshall merged 6 commits into
mainfrom
plan-steps-param-schema
Aug 7, 2026
Merged

feat(ide): engine lens schema CLI and a schema-driven Steps renderer (BACKLOG #235)#226
wshallwshall merged 6 commits into
mainfrom
plan-steps-param-schema

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Closes BACKLOG #235. Adds an engine lens schema CLI and an IDE renderer driven by it, including OQ1 enum dropdowns.

The schema is emitted by the engine rather than duplicated in the IDE, so the two cannot drift -- a second hand-maintained copy of a parameter schema is a divergence waiting to happen, and the renderer reading the engine's own output removes the possibility rather than documenting against it.

BACKLOG #248 is queued behind this merging -- brief at .git/mefor-coord/brief-248-NOTE-ROW.md.

Per-lane notes, owner decisions and merge notes: .git/mefor-coord/handoff/LANE-HANDOVER-backlog-session-2e228e.md (not published).

Verified: 11 files; merge-tree against origin/main reports 0 conflicts; working tree clean at handoff.

…t widgets (BACKLOG #235)

The Steps editor's per-op input widgets are now derived from the transform
vocabulary's own Python type hints, instead of a hand-maintained IDE-side table
that drifts from the signatures.

Engine (new messagefoundry/lens_schema.py + a `lens schema [--json]` CLI):
- op_param_schema() derives {op -> [param]} from actions.__all__ and
  diagnostics.__all__ via stdlib inspect + typing (eval_str=True) -- no new
  runtime dependency (ADR 0076 section 6.5). Covers BOTH sources, so no row kind
  bypasses the schema-driven renderer (the diagnostic ops log_note/checkpoint
  included).
- The `lens schema` subcommand lazy-imports the module and starts no server,
  mirroring `hl7schema --json`.
- Per-param contract: {name, kind, required, keyword_only} plus optional choices
  (enum), nullable (X | None), and default (JSON scalar only -- a non-serializable
  sentinel like code_lookup's _UNSET is omitted so json.dumps never raises).

IDE (cli.ts / stepsView.ts / stepsModel.ts / stepsWebview.js):
- cli.ts shells `lens schema` (execFile, the same path as `lens parse`); stepsView
  fetches it once and threads an optional schema through the render chain (undefined
  keeps byte-identical text inputs, so every existing caller is unchanged).
- A pure resolveWidget() maps kind -> widget for EDITABLE params only (gating
  unchanged, ADR 0076 section 5): int/float -> number input, enum -> select,
  everything else and any unmapped op -> the current text input. code/control rows
  stay read-only.
- Retype fix: a number field posts a JS number, preserved through buildEditRequest
  to the engine, so `lens rewrite` renders an int literal (6) rather than retyping
  it to a string ("6") -- a latent corruption on the currently-editable int args.

Tests: tests/test_lens_schema.py (derivation, dual-source coverage, CLI smoke, and
the committed-fixture sync guard) and ide steps-schema.test.ts (renderer +
widget-resolver + edit round-trip), each falsified against a planted defect. Full
pytest -q green (10346 passed, 843 skipped); IDE Extension Host suite green (648).

Deliberate scope: did NOT touch ADD_MENU_CATALOG/TOOLBAR_INSERT_DEFAULTS (structural
insert templates with no Python signature to derive from); did NOT narrow the four
closed-set str params to Literal[...] (owner-gated -- so enum -> dropdown is built
but dormant until an op emits an enum kind).

NOT-DEPLOYED beta: no prior schema contract to preserve, so no compatibility shim.
…t (BACKLOG #235)

Flip #235's status banner only, from not-started to in-progress: the
engine-emitted param schema (the `lens schema` CLI) and the schema-driven IDE
renderer are built and committed (64a0e12). The item is NOT closed -- the
enum-to-dropdown widget is dormant until the owner rules on OQ1 (narrowing the
four closed-set str params to Literal).

Scope: only #235's banner line under its own heading changed. The ranked table
and the four census distribution lines were NOT touched, and the census was NOT
recomputed (not-started -> in-progress is open -> open, so the distribution is
unchanged anyway).
…ps renders enum dropdowns (BACKLOG #235)

OQ1 follow-on to the engine-emitted Steps param schema: narrow the closed-set
string arguments to Literal[...] so `lens schema` projects them as `enum` (with
their choice list) and the IDE renders a dropdown instead of a text input --
the item's headline widget, previously built but dormant.

- actions.py: convert_case mode -> Literal["upper","lower","title"]; pad_field
  side -> Literal["left","right"]; arith_field op -> Literal["+","-","*","/"];
  date_diff_field unit -> Literal["days","years","hours","minutes"]. The runtime
  if/elif ValueError guards are kept: the Literal defends the author-time typed
  caller and drives the dropdown; the guard still catches a dynamically-supplied
  bad value at run time. The intentional two-layer overlap is documented once in
  the module docstring.
- Regenerated ide/src/test/fixtures/lens-schema/op-schema.json (the four ops now
  carry kind "enum" + choices); test_ide_fixture_in_sync keeps it faithful.
- New test_closed_set_param_becomes_enum pins the enum derivation (falsified:
  reverting a Literal flips kind enum->str and reds the test).

No test_actions.py change needed: CI mypy is `mypy messagefoundry
messagefoundry_webconsole` (tests/ is not type-checked) and Literal is not
enforced at run time, so the negative-guard tests still raise ValueError and
pass unchanged. stepsModel.ts already prepends an out-of-set literal as the
selected <option>, so a hand-authored value outside the choice set displays
correctly.

Verify: ruff + ruff format clean; mypy messagefoundry clean (the 21 remaining
are pre-existing optional-extra import errors, none in touched files); IDE
Extension Host suite 649 passing; full pytest 10346 passed / 843 skipped. The
one full-suite failure (test_installed_coord_hooks push_guard.py:
installed-vs-committed hash mismatch) is PRE-EXISTING and environmental -- a
concurrent edit of the shared coord hook by another session; proven independent
of this change by a control arm (it fails identically with all OQ1 changes
stashed, and its hashes moved between two runs 22 min apart).
…wing (BACKLOG #235)

Flip #235's status banner only, from in-progress to closed. With OQ1 landed
(48b829b), all the item's widgets are live: int-to-number, the retype-trap fix,
and enum-to-dropdown for the four closed-set ops. The code-set picker is N/A (no
editable code-set literal to attach to) and code/control rows stay read-only, so
the item's scope is complete.

Scope: only #235's banner line under its own heading changed. The ranked table
and the four census distribution lines were NOT touched, and the census was NOT
recomputed (in-progress -> closed; the coordinator reconciles the distribution).
@wshallwshall
wshallwshall enabled auto-merge (squash) August 6, 2026 00:40
wshallwshall added a commit that referenced this pull request Aug 7, 2026
…ted helpers (BACKLOG #337) (#224)

* fix(checks): handler-security missed getattr indirection and PHI logged in undecorated helpers (BACKLOG #337)

Two execution-verified recall gaps in `_check_handler_security` (ADR 0144), closed and pinned by tests.

Gap 1 -- ambient-authority / impure-transform saw only a literal Name/Attribute chain, so a
`getattr(os, "system")("id")` indirection (the outer call's func is itself a Call) was never resolved
and never flagged. `_dotted_call_name` now splices a *constant* `getattr(mod, "name")` into the chain
(`getattr(os, "system")` -> `os.system`), so the ambient-authority deny-list sees it; the same shared
resolver flags a `getattr(time, "time")()` wall-clock read for impure-transform. A *dynamic*
`getattr(os, name)`, a `globals()["os"]` subscript, and any other call-shaped receiver stay unresolved
(None), so benign reflection is not flagged. The opt-in Semgrep leg still carries no getattr pattern
(recorded as a residual, not fixed here).

Gap 2 -- phi-to-log was decorated-scope only, so it never opened the undecorated
`_<feed>_transforms.py` helpers the decompose-by-role convention (docs/CONNECTIONS.md, #226) steers
field-level PHI handling into. phi-to-log now scans every function body -- a decorated @router/@handler
OR an undecorated helper -- keyed on the first positional parameter as the message symbol.
impure-transform stays decorated-scope ONLY: widening it too would re-flag the shipped
`_pdf_mdm_transforms.py` ingest-time wall-clock fallback and red the read-only samples/config
calibration gate (the trade ADR 0144 records). Proven both directions against the real samples before
landing.

Severity is unchanged and deliberately conservative -- it holds in BOTH sandbox postures. The lint is
advisory by default (required only under --strict-handler-security) and pre-deployment. An evasion
reaches only host actions the ADR 0087 sandbox does not confine: DEFAULT_FORBIDDEN_MODULES
(pipeline/sandbox.py) blocks socket/ssl/asyncio/multiprocessing, the I/O-bearing messagefoundry.*
subpackages and cryptography, but not os or subprocess. So under mode=off the author already holds
in-process execution, and under mode=subprocess an evasion still reaches neither the DEK nor the audit
chain -- no PHI-exposure path, no runtime-behaviour change ("a filter, not a fix"). To be re-scored
upward when ADR 0147 (OS-level default-deny; Proposed, no code) lands. MessageFoundry is a not-deployed
beta, so a miss "would slip past a deploying site's CI", never "PHI is exposed".

Docs in this commit describe the same change: ADR 0144's Decision-section phi-to-log bullet is
corrected to the widened every-function-body scope (it still asserted decorated-scope, contradicting
the amendment and the code) and its amendment blockquote is added; the master-test-plan row for this
test file is refreshed (line count 987 -> 1,079 and the three scoping regimes). The BACKLOG #337 banner
flip is a separate follow-on commit.

Tests (tests/test_checks_handler_security.py): amb_pos_getattr_os_system, amb_neg_getattr_dynamic_attr,
impure_pos_getattr_time_time, phi_pos_undecorated_helper_logs_msg, plus
test_widened_phi_to_log_does_not_widen_impure_transform and
test_phi_to_log_undecorated_helper_non_message_local_is_clean. Each was falsified by reverting the
fix: the two getattr positives red when the splice is removed; the phi positive reds when the loop
reverts to decorated-scope; the impure-transform guard reds when impure-transform is widened (which
also reds the real samples calibration); the discrimination test reds when phi-to-log ignores the
message symbol; the dynamic-getattr negative reds when the splice over-fires on a non-constant attr.

* backlog: flip #337 to done (handler-security getattr + undecorated-helper recall gaps closed)

Banner-only edit of docs/BACKLOG.md item #337: the open `[filed]` banner becomes a closed `[done]`
banner recording the resolution. Isolated from the code+tests+docs commit per the banner-flip rule.

Only #337's banner line changed. The ranked table and the four census distribution lines are
untouched, and the census was NOT recomputed -- the coordinator reconciles it.
@wshallwshall
wshallwshall merged commit 4ea8ee3 into main Aug 7, 2026
34 checks passed
@wshallwshall
wshallwshall deleted the plan-steps-param-schema branch August 7, 2026 02:24
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