Skip to content

Paramify VER fetchers: review fixes, category-level config, and paramify programs - #22

Merged
21tmccauley merged 10 commits into
mainfrom
the-VDR-json-schema
Aug 4, 2026
Merged

Paramify VER fetchers: review fixes, category-level config, and paramify programs#22
21tmccauley merged 10 commits into
mainfrom
the-VDR-json-schema

Conversation

@21tmccauley

@21tmccauley 21tmccauley commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Builds on the three Paramify FedRAMP VER report fetchers (VER-RPT-AVI, VER-RPT-VDT, VER-TFR-MRH) already on this branch: fixes what a review of them turned up, corrects where their config is declared, normalizes their timestamps, and adds a way to fan them out across a workspace without anyone copying project UUIDs by hand.

Seven commits, each green on its own (checked out and gated individually, so a bisect can't land on a broken one).

1. Fixes to the VER fetchers (b799193)

Correctness, all in the shared module so the AVI/VDT partition can't drift:

  • A pending or rejected RISK_ADJUSTMENT reported finalDisposition: "Partially Mitigated". Mitigation now requires an accepted deviation — claiming partial mitigation on the strength of a decision nobody has made yet is a compliance misstatement.
  • An issue with neither poamId nor id raised KeyError and killed the whole report.
  • PARAMIFY_HTTP_TIMEOUT was int()-parsed at import, so a malformed value aborted the run with a bare ValueError before main() could log anything.
  • A timestamped report_to silently over-included up to a day past the declared period.

Contract — the fetchers declared report_from / report_to / api_base_url / http_timeout under secrets[]. Every declared secret is mandatory (the runner raises when a manifest omits one), so the optional knobs were effectively required, contradicting their documented defaults. They're config now. cert_package_uri, api_base_url and http_timeout went further, to category config — one workspace publishes one certification package URI and talks to one Paramify instance, so these are set once under platforms.paramify.config rather than copied onto every target. Mirrors rippling.yaml, which already declares base_url/page_size at category level. Also declared PARAMIFY_REPORT_TO, which the fetchers read and the README documented but which was declared nowhere, so the runner could never set it.

Evidence payload — _summary now carries a collection block (status + the API-failure ledger). /issues is the only call these fetchers make, so a failure leaves the report arrays empty; the house pattern (20+ fetchers) is to still write the file with the ledger inside it, and without this an empty failed report is indistinguishable from a genuinely clean one to anything reading the payload. The uploader's skip_failed defaults to false, so that artifact does get uploaded.

2. paramify programs (23db1e7)

paramify programs list      # readable name + project UUID for every program
paramify programs target    # select programs, write them as manifest targets

The API identifies programs by UUID; the UI and the people running this use names. target selects interactively (numbers, ranges, all), by --program NAME|ID, or --all; resolves a selector as an exact id, then an exact name, then a unique case-insensitive substring, and refuses an ambiguous match rather than guessing. With no fetcher argument it targets every Paramify-program entry in the manifest, so one command fans out all three reports. Re-running tops the manifest up instead of duplicating targets.

Shared values that don't vary per program (--cert-uri, --report-from) are written to platforms.<category>.config rather than onto every target, and are shown and editable on every interactive run — see §7, which reworked this. --report-from is validated as an ISO date up front: an unparseable one yields an empty report window, which silently drops every closed issue rather than failing.

Targets also carry program_name, so evidence files and uploaded artifacts read as … - Alpha Cloud Services instead of a bare UUID (a UUID prefix stays in the filename — program names aren't guaranteed unique, and a collision would make the runner's dir-diff output discovery report the second invocation as having produced nothing).

The workspace lookup (GET /projects) is ~50 lines of requests in the api facade rather than a dependency on paramify-sdk, which pfy uses: the SDK is pinned by a private git URL and this repo is heading for public release, so a git dependency would break pip install for outside users. Selection, resolution and manifest wiring live in framework.api so the TUI can grow the same capability without reimplementing it; target composes api.add_target(), so it produces exactly the manifest a hand-written manifest add-target would.

3. TUI: config inherited from the category showed as unset (f38f6fb)

The manifest screen read only a fetcher entry's own config block, so a value set once at the category level rendered as required — unset on every entry inheriting it, and the summary column undercounted. api.validate() got this right all along — which is why the CLI called a manifest runnable while the TUI showed a missing required field.

Pre-existing: it would have misreported rippling's base_url/page_size and checkov's soft_fail the same way. Moving the VER fetchers' shared knobs to category config is just what made it reachable.

Fixed with api.effective_config(), which performs the same merge the runner does (platform defaults ← platform values ← per-fetcher values) and returns each field with its value plus the layer it came from. Both the detail pane and the count render that. Category-declared fields now appear at all — cert_package_uri was previously invisible on entries it applies to, despite being required and injected into every invocation.

4. One timestamp format everywhere (5fc52b0)

A generated report mixed three notations, because it drew on three sources and only one was formatted: fetcher-generated values (already canonical), values passed through verbatim from the API (millisecond precision), and report bounds echoed from config (a bare date). All three now go through to_utc_z():

2026-07-30T09:00:00Z

Non-UTC offsets are converted rather than preserved. Unparseable input passes through unchanged — losing a value the source gave us is worse than an off-format one, and schema verification is where that should surface. The bounds are normalized for display only; the raw config values still drive the coverage window, since normalizing first would turn a date-only bound into a midnight instant and drop that day's closures. For the same reason a date-only report_to reports as that day's last second.

Adds tests/test_ver_timestamps.py, the first fetcher-level test in the repo (loads ver_common by path, since fetchers are scripts the runner exec's rather than an importable package).

5. Simplification pass (c3ebf96) — net −87 lines, and two defects

A four-angle quality review (reuse / simplification / efficiency / altitude) over the branch diff. Both defects were introduced and fixed within this branch, so the diff against main shows neither — flagging them here so they get review attention:

  • programs target could write Paramify UUIDs into GitLab targets. program_target_fetchers selected any fanout fetcher declaring a project_id target field, and all three GitLab fetchers declare exactly that. validate() doesn't check target-field completeness, so it surfaced at run time. Now scoped by category too; add_program_targets also no longer writes a program_name the target schema doesn't declare.
  • list_programs sent a Bearer token to whatever PARAMIFY_API_BASE_URL named, including plain http. Now enforces the rule the evidence uploader already had — https only, localhost exempt.

Also fixed a latent inconsistency: categories_needing_config decided "is it set" by truthiness while effective_config, added in the same PR, decided it by membership — so a required field set to "" read as set in the TUI and missing in the CLI. Both are now views over effective_config.

Efficiency: programs target ran 4 discover_fetchers + 3 discover_platforms for one immutable result (each walks and jsonschema-validates all 125 fetcher.yaml, ~165 ms) — now 1 + 1, 578 ms → 152 ms. The TUI's manifest redraw called validate() and effective_config() with no shared discovery, doubling blocking work on every mutation and tab switch — 272 ms → ~166 ms.

Simplification highlights: three config helpers (61 lines, a third copy of the platform←entry merge) → one 26-line filter; render._config_rows' fallback was unreachable and re-implemented that merge minus the platform layer, so it could only ever render the wrong answer this PR set out to fix; three copies of the accepted-deviation predicate → one; the VDT/MRH summaries re-typed the disposition labels as string literals while the DISPOSITION_* constants sat above them, so renaming one would have left both silently reporting zeros.

6. TUI: five dead keys, and the textual line it's actually tested against (dd846fd)

Each of these is reachable in normal use:

  • Pressing the number of the tab you are already on killed every page shortcut. _go_to_tab clears focus and then assigns TabbedContent.active; assigning the value it already holds fires no TabActivated, so nothing re-homed focus — and a page's bindings only resolve while focus is inside that page. a/e/x, ctrl+r, j/k and the arrows all went dead until you pressed escape or a different tab.
  • ctrl+p on the Paramify tab opened Textual's command palette instead of Preview. The palette claims that key as a priority binding, checked ahead of the focused widget, so the page's own binding could never fire. We register no command providers (the palette only offers Textual's built-ins), so it's off. p now works too, mirroring the Manifest tab.
  • enter did nothing on the two tables whose footer said it did something. On a run it now drills into that run's evidence files (where enter opens one); on a manifest row it opens the entry editor.
  • Editing the manifest's output dir lost the path. Textual selects an Input's value on focus, so the first keystroke replaced it wholesale; and an edit not submitted with enter was silently reverted by the next rebuild(). Focus no longer selects, and blur commits.
  • enter in a confirmation dialog meant Yes — Yes is composed first and took the default AUTO_FOCUS, on the dialogs that delete a manifest file, remove an entry, and upload to Paramify. Focus No; y still confirms.

The footer now lists esc (the only way out of a focused text field back to the shortcut keys — an Input consumes every printable key) and the Run tab shows enter/ctrl+r. That last one is advertised rather than rebound: the status table can't hold focus before the first run, so moving focus there would leave ctrl+r dead instead.

The tui extra pins textual>=8,<9 (was >=1.0,<2.0, which nobody ran — select_on_focus and the focus semantics above differ enough that the TUI is not the same app on 1.x). tests/test_tui_keys.py drives the real app through Textual's pilot to hold the contract: what each tab focuses, that the globals survive a repeat tab press, and that enter reaches an action wherever the footer says it does.

7. programs target's shared config is shown and editable every run (2d8a634)

As shipped in §2, --cert-uri and --report-from were prompted for only when genuinely missing, so a manifest that already had them never showed them again. That made the two values a black box: the only way to see what the next run would stamp into every report — or to fix a wrong URI, or roll the report window forward — was to open the manifest and edit platforms.paramify.config by hand. Adding a program and moving the window are the same routine, and the command served only one.

Shared config — one value for every program. Enter keeps what's shown.

  cert_package_uri: set in platforms.paramify
Certification Package Overview URI (used for every program) [https://example.gov/cpo]:

  report_from: set in platforms.paramify
Report period start — ISO date, e.g. 2026-01-01 (used for every program) [2026-01-01]:

The provenance line is not decoration: an entry's own config outranks the category value this command writes, so "where is this coming from" changes what your answer will and won't affect — and the bracketed default can't say it. Enter keeps the value and writes nothing (an unchanged answer already in force everywhere skips the write, so a run that just adds a program leaves the platform block byte-identical); typing over it updates the category value.

Entries that resolve to different values get no default, only a note that they differ — offering one entry's answer as the manifest's would misreport the others. The ISO check on report_from now guards the typed answer as well as the flag, and a rejected one exits before anything is written. --json and a piped stdin are unchanged: nothing prompts, a flag overrides without asking, and only a genuinely missing value is an error.

categories_for_config becomes shared_config_state, which answers what a front-end editing a shared field actually needs — the categories that accept it, the value in force, its sources, whether entries disagree, and what's still missing — instead of a bare category list. Still one view over effective_config(), so "is it set" keeps a single definition (the drift §5 fixed).

Verification

ruff check framework/, mypy framework/, pytest (307 pass, 70 new), and the CLI discovery smoke over all 125 fetchers — green on the tip and on each commit individually.

Behaviour exercised end to end against a stubbed workspace serving /projects and /issues (no live tenant): programs listprograms targetvalidaterun produced 9 invocations across 3 programs × 3 fetchers, all exit 0, each program's file distinct, one evidence set per report, the shared cert URI in all 9 payloads, and all 42 payload timestamps canonical. The failure path too: a dropped /issues call exits 1 and records collection.status: "failed" with the exception in the payload. Interactive prompts were driven through a real pty to confirm each program's values land on the right target.

§6 and §7 were verified on their own terms, not by re-running that stub: the TUI keys through Textual's pilot (tests/test_tui_keys.py), and the reworked prompting through five CLI tests plus a scratch run over the four flows in sequence — first run with nothing set, enter keeping both, typing a new date, and a bad date rejected with the manifest left untouched.

Known gaps — need a live tenant, not a code change

One real run answers all three:

  • /issues is not paginated. Acknowledged in the code. If the endpoint caps a page, every report is silently short. paramify-sdk paginates nothing anywhere either — corroboration, not proof.
  • Issue status is assumed to be exactly {OPEN, CLOSED}. Any third status whose statusDate falls outside the report window is dropped entirely; inside it, it gets no finalDisposition and isOverdue: false regardless of dueDate.
  • LEVEL_TO_NRATING contains "CHILL", which appears nowhere else in the repo. Matching is exact and case-sensitive; an unknown level silently omits currentRating.

Deferred follow-ups

Identified in the review, deliberately not in this PR:

  • The config merge exists in three places (executor._apply_config, api.validate, api.effective_config) and has already drifted — the executor filters None, the others don't. Extracting it touches the runner.
  • No type: date in the config schema, so report_from is validated only on the CLI flag path — not via manifest set-platform-config, the TUI, or a hand-edited manifest.
  • The uploader's _TITLE_KEYS preference list is standing in for a fetcher declaring which target field is its display identity.
  • No schema_binding / vendored FedRAMP schema yet (pairs with feat/schema-verification).
  • MRH filters a point-in-time snapshot by the report window; and the reports use wall-clock now for the 192-day acceptance clock and overdue status rather than report_to, so re-running for a past period doesn't reconstruct that period's state.

🤖 Generated with Claude Code

Soya Kawamura and others added 6 commits July 30, 2026 09:19
Port the three VER-* vulnerability report generators into the framework:
- fetchers/paramify/{accepted_vulnerabilities,vulnerability_detail_report,historical_ver_activity}
- shared logic in _shared/ver_common.py: one accepted-definition + one fetch/
  mapping implementation, so the three reports partition consistently by
  construction (AVI accepted / VDT non-accepted / MRH both).
- _categories/paramify.yaml + category README.

Reads from Paramify's own REST API; writes FedRAMP CR2026 report JSON as the
payload (runner adds the envelope). Keeps a vendor _summary; no schema
verdict in the fetcher (Paramify-side). Verified end-to-end against stage via
paramify run: AVI 1 accepted, VDT 2126 non-accepted, MRH 2127 total
(2126 active / 1 accepted); dispositions 652/2/1/1471; 872 overdue,
873 unevaluated. 227 framework tests pass.
project_id and cert_package_uri move from secrets to target_schema: both are
per-program properties, and a public CPO URI was never a secret. The runner now
invokes each fetcher once per program, and the envelope carries the target.

Output filenames gain a sanitized project_id suffix (shared
sanitize_for_filename in ver_common). The runner discovers outputs by diffing
the evidence dir, so without this the second program would silently overwrite
the first and its outputs list would come back empty.

Verified against stage with two programs via paramify run (6/6 OK):
Wiz (FEDRAMP_REV_5) 1 accepted / 2126 active / 2126 non-accepted, matching
prior single-target runs; and a program with zero issues, which produces a
conformant report with all required fields present and empty arrays.
227 framework tests pass.
Review of the three FedRAMP VER report fetchers (AVI, VDT, MRH) turned up
correctness bugs and a contract mismatch. All fixes are shared-module level, so
the AVI/VDT partition stays consistent by construction.

Correctness:

- A pending or rejected RISK_ADJUSTMENT reported finalDisposition "Partially
  Mitigated". Mitigation now requires an ACCEPTED deviation — asserting partial
  mitigation on the strength of a decision nobody has made yet is a compliance
  misstatement.
- map_vulnerability_detail raised KeyError on an issue with neither poamId nor
  id, killing the whole report. It now emits an empty providerTrackingId, which
  schema verification can flag per-record.
- PARAMIFY_HTTP_TIMEOUT was int()-parsed at import, so a malformed value aborted
  the run with a bare ValueError before main() could log anything. It resolves at
  call time and falls back to the default with a warning.
- Callers pre-truncated dates to 10 chars, so a timestamped report_to silently
  over-included up to a day past the declared period. The window helper now
  decides date-only vs timestamp itself.

Contract:

- report_from/report_to/api_base_url/http_timeout were declared under secrets[].
  Every declared secret is mandatory — the runner raises when a manifest omits
  one — which made the optional knobs required, contradicting the README. They
  are config now.
- cert_package_uri, api_base_url and http_timeout moved to category config
  (fetchers/_categories/paramify.yaml). One workspace publishes one certification
  package URI and talks to one Paramify instance, so these are set once under
  platforms.paramify.config rather than copied onto every target. Mirrors
  rippling.yaml, which already declares base_url/page_size at category level.
- PARAMIFY_REPORT_TO was read by all three fetchers and documented as optional
  but declared nowhere, so the runner — which passes only declared env vars —
  could never set it.
- Added an optional program_name target field for readable evidence filenames.

Evidence payload:

- _summary now carries a `collection` block (status + api_failures). /issues is
  the only call these fetchers make, so a failure leaves the report arrays empty;
  the house pattern is to still write the file with the ledger inside it, and
  without this an empty failed report reads as a genuinely clean one.

Also deduplicated the acceptance rationale (2x) and the unevaluated-backlog
warning (3x) into _shared/ver_common.py, and promoted the cross-module helpers
off underscore-private names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Paramify API identifies programs by project UUID; the product UI and the
people running these fetchers use names. Fanning a fetcher out across a
workspace meant copying UUIDs by hand into targets[].

  paramify programs list      # readable name + UUID for every program
  paramify programs target    # select programs, write them as manifest targets

`target` selects interactively (numbers, ranges, or "all"), by --program
NAME|ID, or --all; resolves a selector as an exact id, an exact name, then a
unique case-insensitive substring, and refuses an ambiguous match rather than
guessing. With no fetcher argument it targets every manifest entry whose
target_schema declares project_id, so one command fans out all three VER
reports. Programs already targeted are skipped, so a re-run tops the manifest up
instead of duplicating entries.

Shared values the targeted fetchers need but that don't vary per program
(--cert-uri, --report-from) are asked for once and written to
platforms.<category>.config, where every fetcher in the category picks them up.
Each is prompted for only when genuinely missing — required, no default, and
absent from both the platform block and the entry's own config. --report-from is
validated as an ISO date up front: an unparseable one yields an empty report
window, which silently drops every closed issue rather than failing.

Implementation notes:

- The workspace lookup (GET /projects) is ~50 lines of requests in the api
  facade rather than a dependency on paramify-sdk, which pfy uses. The SDK is
  pinned by a private git URL and this repo is heading for public release; a git
  dependency would break `pip install` for outside users. It matches the
  requests-based client the evidence uploader already ships.
- Selection, resolution and manifest wiring live in framework.api so the TUI can
  grow the same capability without reimplementing any of it; the CLI only
  renders. `target` composes api.add_target(), so it produces exactly the
  manifest a hand-written `manifest add-target` would.
- Nothing prompts under --json; every error path emits {ok, path, errors}.

The uploader now prefers a target's program_name over its opaque id when titling
an artifact, so per-program artifacts in one evidence set read as
"… - Alpha Cloud Services" instead of a bare UUID. Fetchers whose id is already
readable (gitlab's group/project) declare no program_name and are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest screen read only a fetcher entry's own `config` block, so any value
set once at the category level rendered as "required — unset" on every entry
that inherited it, and the summary column undercounted. api.validate() got this
right all along, which is why `paramify validate` reported a manifest runnable
while the TUI showed a missing required field.

Pre-existing — it would have misreported rippling's base_url/page_size and
checkov's soft_fail the same way. Moving the Paramify VER fetchers' shared knobs
to category config is just what made it reachable.

Adds api.effective_config(), which performs the same merge the runner does
(platform defaults <- platform values <- per-fetcher values) and returns each
field with its value plus the layer it came from. Both the detail pane and the
count now render that, so they can't disagree with each other or with validate.
Batched over every entry so a table redraw scans the fetcher tree once.

Two things fall out of resolving it at the facade rather than patching the
renderer: fields the *category* declares now appear at all (previously
cert_package_uri was invisible on entries it applies to, despite being required
and injected into every invocation), and each value shows its provenance, so
it's clear which layer to change.

The edit form is untouched and stays safe: FieldRow.get_value() returns None for
a blank input and the save path skips it, so a blank inherited field means
"inherit", not "overwrite with empty".

Also records the whole branch under CHANGELOG [Unreleased].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@21tmccauley

Copy link
Copy Markdown
Collaborator Author

Need to refactor to reduce LOC and fix time to be UTC

Tate McCauley and others added 2 commits July 30, 2026 10:14
A generated report mixed three timestamp notations, because it drew on three
sources and only one of them was formatted:

- values the fetcher generates (generatedAt, a defaulted reportPeriod.to) —
  already %Y-%m-%dT%H:%M:%SZ
- values from the Paramify API (createdAt -> detectedAt, evaluationDate ->
  evaluationCompletedAt, and the dueDate quoted in an overdue explanation) —
  passed through verbatim, so millisecond precision landed in the report
- report bounds from config — echoed verbatim, so a bare "2026-01-01" sat next
  to a full timestamp in the same reportPeriod object

All three now go through to_utc_z(), so one document carries one notation:

    2026-07-30T09:00:00Z

A non-UTC offset is converted rather than preserved (…T09:00:00+02:00 becomes
…T07:00:00Z). A value the parser can't read is passed through unchanged rather
than dropped or blanked — losing a value the source gave us is worse than an
off-format one, and schema verification is where that should surface.

The report bounds are normalized for DISPLAY only; the raw config values still
drive fetch_all_issues. Normalizing before the window is computed would turn a
date-only bound into a midnight instant and silently drop that day's closures.
For the same reason a date-only report_to is reported as that day's last second
(2026-06-30 -> 2026-06-30T23:59:59Z): the filter treats a date-only end as
"through the end of that day", so reporting its midnight would understate the
period by a day in a compliance artifact.

Verified end to end against a stub returning the API's real shapes (milliseconds
and a +02:00 offset): all 60 timestamps across 9 artifacts, payload and envelope
metadata, match the canonical form. The one remaining dash-separated value is
metadata.run_id, which is a path-safe identifier naming the run directory (':'
is illegal in Windows paths), not a timestamp field — collected_at beside it
carries the same instant canonically.

Adds tests/test_ver_timestamps.py, the first fetcher-level test in the repo. It
loads ver_common by path, since fetchers are scripts the runner exec's rather
than an importable package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…egory

A four-angle quality pass (reuse, simplification, efficiency, altitude) over the
branch diff. Net -87 lines, one real defect, and one security gap.

Defect — `programs target` could write Paramify UUIDs into GitLab targets:

  program_target_fetchers selected any fanout fetcher declaring a `project_id`
  target field. gitlab_ci_cd_pipeline_config, gitlab_project_summary and
  gitlab_merge_request_summary all declare exactly that, so in a manifest holding
  both categories, `programs target` with no fetcher argument targeted them with
  Paramify program UUIDs. validate() doesn't check target-field completeness, so
  it surfaced at run time. Now scoped by category as well — the honest
  discriminator while this command group is Paramify-specific. A target_schema
  field declaring what it identifies would let it generalize; that's a follow-up.
  add_program_targets likewise no longer writes a `program_name` the targeted
  fetcher's schema doesn't declare.

Security — list_programs sent a Bearer token to whatever PARAMIFY_API_BASE_URL
named, including plaintext http. It now enforces the same rule the evidence
uploader already does (uploader._base_url_error): https only, localhost exempt.

Consistency — categories_needing_config decided "is it set" by truthiness while
effective_config, added in the same PR, decided it by membership. A required
field set to "" read as set in the TUI and missing in the CLI. Both are now views
over effective_config, so there is one definition.

Efficiency:

- `paramify programs target` ran 4 discover_fetchers + 3 discover_platforms for
  one immutable result — each walks and jsonschema-validates all 125 fetcher.yaml
  (~165 ms). Now 1 + 1: measured 578 ms -> 152 ms. Both api helpers already took
  optional pre-discovered maps; the CLI just wasn't passing them.
- The TUI's manifest redraw called validate() and effective_config() with no
  shared discovery, doubling the blocking work on every mutation and tab switch
  (272 ms -> ~166 ms). Adds api.discover() for the one-pass-and-thread pattern.

Simplification:

- _config_field_def + categories_declaring_config + categories_needing_config
  (61 lines, a third copy of the platform<-entry merge) -> one
  categories_for_config filtering effective_config's output (26).
- render._config_rows' `view is None` fallback was unreachable — its only caller
  always passes a view — and re-implemented that merge minus the platform layer,
  so it could only ever render the wrong answer this PR set out to fix.
- Three copies of the accepted-deviation predicate -> _accepted_deviations();
  accepted_deviation's build/sort/index-0 -> max(..., default=None).
- build_vdt_summary and build_mrh_summary counted identically and re-typed the
  disposition labels as literals while DISPOSITION_* constants sat above them —
  renaming one would have left both summaries reporting zeros. Now _detail_counts,
  keyed off the constants.
- report_period_bounds' inner closure took a flag whose first branch was dead on
  the one call that passed False.
- _parse_selection's two numeric branches are one: "4" is the range "4-4".
- _SHARED_CONFIG_PROMPTS needed a second dict just to map its names back to the
  CLI parameters; inlined. list_programs' three never-passed parameters dropped.
- 13 test invocations of the same 5-line argv -> a _target() helper, so each test
  shows only what it varies.

Behaviour re-verified end to end against the stub after every change: 9
invocations across 3 programs x 3 fetchers, all exit 0, 42 payload timestamps all
canonical, reportPeriod and _summary unchanged. 292 tests, ruff and mypy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@21tmccauley 21tmccauley reopened this Jul 30, 2026
@21tmccauley

Copy link
Copy Markdown
Collaborator Author

Need to fix bug where paramify program target only allows uri and date entry on unset. It should prompt every time

Tate McCauley and others added 2 commits August 3, 2026 11:11
Five key/focus bugs, all reachable in normal use, plus the pin that makes the
behaviour they depend on a stated requirement rather than a coincidence.

- Pressing the number of the tab you are already on killed every page shortcut.
  `_go_to_tab` clears focus and then assigns `TabbedContent.active`; assigning
  the value it already holds fires no `TabActivated`, so nothing re-homed focus,
  and a page's bindings only resolve while focus is inside that page. `a`/`e`/
  `x`, `ctrl+r`, `j`/`k` and the arrows all went dead until you pressed escape
  or a different tab. Re-home focus directly in that case.
- `ctrl+p` on the Paramify tab opened Textual's command palette instead of
  Preview — the palette claims it as a *priority* binding, checked ahead of the
  focused widget, so the page binding could never fire. We register no command
  providers, so the palette only offers Textual's own built-ins: turn it off and
  keep the key. `p` now works too, mirroring the Manifest tab.
- `enter` did nothing on the two tables whose footer said it did something: on a
  run it now drills into that run's evidence files (where enter opens one), and
  on a manifest row it opens the entry editor.
- Editing the manifest's output dir lost the path. Textual selects an `Input`'s
  value on focus, so the first keystroke replaced it wholesale; and an edit not
  submitted with `enter` was silently reverted by the next `rebuild()`. Focus no
  longer selects, and blur commits.
- `enter` in a confirmation dialog meant Yes, because Yes is composed first and
  took the default `AUTO_FOCUS` — on the dialogs that delete a manifest file,
  remove an entry, and upload to Paramify. Focus No; `y` still confirms.

The footer now lists `esc` (the only way out of a focused text field back to the
shortcut keys — an `Input` consumes every printable key) and the Run tab shows
`enter/ctrl+r`, since focus opens on the ▶ Run button and Button binds enter.
Advertised rather than rebound: the status table can't hold focus before the
first run, so moving focus there would leave `ctrl+r` dead instead.

The `tui` extra pins `textual>=8,<9` (was `>=1.0,<2.0`, which nobody ran).
`select_on_focus` and the focus semantics above differ enough that the TUI is
not the same app on 1.x. `tests/test_tui_keys.py` drives the real app through
Textual's pilot to hold the contract: what each tab focuses, that the globals
survive a repeat tab press, and that enter reaches an action wherever the footer
says it does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--cert-uri` and `--report-from` were prompted for only when genuinely missing,
so a manifest that already had them never showed them again. That made the two
values a black box: the only way to see what the next run would stamp into every
report — or to correct a wrong URI, or roll the report window forward — was to
open the manifest and edit `platforms.paramify.config` by hand. Adding a program
and moving the window are the same routine, and the command only served one.

Both are now shown on every interactive run, with the value in force as the
prompt default and a line saying where it comes from (`platforms.paramify`, an
entry's own config, or not set yet) — provenance the bracketed default can't
carry, and it matters because an entry-level override outranks the category
value this command writes. Enter keeps the value and writes nothing: an
unchanged answer already in force everywhere skips the write, so a run that just
adds a program leaves the platform block byte-identical. Typing over it updates
the category value.

Entries that resolve to *different* values get no default, only a note that they
differ: offering one entry's answer as the manifest's would misreport the
others. The ISO check on `report_from` now guards the typed answer as well as
the flag, and a rejected one exits before anything is written.

`--json` and a piped stdin are unchanged — nothing prompts, a flag overrides
without asking, and only a genuinely missing value is an error.

Implementation: `categories_for_config` becomes `shared_config_state`, which
answers what a front-end editing a shared field actually needs (the categories
that accept it, the value in force, its sources, whether entries disagree, and
what's still missing) instead of just a category list. Still one view over
`effective_config`, so "is it set" keeps a single definition. Five interactive
CLI tests cover the new path; `_can_prompt` is the seam they patch, since
CliRunner's stdin is not a tty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@21tmccauley
21tmccauley merged commit 3e54b00 into main Aug 4, 2026
7 checks passed
@21tmccauley
21tmccauley deleted the the-VDR-json-schema branch August 4, 2026 18:34
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